反射:框架的灵魂
本章内容:

1.Junit测试
2.注解
3.反射

1.Junit测试

        Junit白盒测试步骤:

  1. 定义一个测试类
    类名:被测试的类名Test :CalculatorTest
    包名:xxx.xxx.test cn.Tencent.test
  2. 定义测试方法:可以独立运行
    方法名:test测试的方法名 testAdd()
    返回值:void
    参数列表:空参
  3. 给方法加@Test (注解,与之前的覆写@Override一样
  4. 导入junit的依赖(使用时一定要导入junit的依赖,可以在maven中的pom.xml文件导入junit)
1
2
3
4
5
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
  1. 判定:我们一般会使用断言操作来处理结果
    Assert.assertEquals(期望的结果,程序运行的结果)

  2. Junit的@Before与@After
    @Before:初始化方法,用于资源的申请,所有的测试方法在执行前都会先自动执行该方法
    @After:释放资源方法,用于资源的释放,所有的测试方法在执行后都会自动执行该方法

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class CalculatorTest {

@Before
public void init(){
System.out.println("初始化资源。。。");
}

@After
public void close(){
System.out.println("释放资源。。。");
}

@Test
public void testAdd(){
int a = 8;
int b = 7;
int c = a+b;
// 断言操作
Assert.assertEquals(15,c);
System.out.println(a+"+"+b+"="+c);
}
}

运行结果:
初始化资源。。。
8+7=15
释放资源。。。

2.反射

  • 框架:半成品软件,可以在框架的基础上进行软件开发,简化编码
  • 反射:将类的各个组成部分封装为其它对象,这就是反射机制(可以在程序运行过程在,操作这些对象;可以解耦,提高程序的可扩展性)

  • 获取Class对象(字节码文件对象)的方式:

    • Class.forName(“包名.类名”):将字节码文件加载进内存,返回Class对象(多数用于配置文件,将类名定义在配置文件在,读取文件
    • 类名.class:通过类名的属性class获取(多用于参数的传递
    • 对象.getClass():getClass()方法在Object类中定义着(多用于对象的获取字节码的方式

    结论:同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
package org.AhriLove;

public class ReflectDemo {
/*
* 反射类:
* 1.包名.类名
* */
public static void main(String[] args) throws ClassNotFoundException {
// 1. 全类名
Class cls1 = Class.forName("org.AhriLove.Person");
System.out.println(cls1);
// 2. 类名.class
Class cls2 = Person.class;
System.out.println(cls2);
// 3.对象.getClass()
Person p = new Person("chd",18);
Class<? extends Person> cls3= p.getClass();
System.out.println(cls3);
// 判断是否是相等时
System.out.println(cls1 == cls2);
System.out.println(cls1 == cls3);
System.out.println(p.getAge());
/*
* 运行结果:
class org.AhriLove.Person
class org.AhriLove.Person
class org.AhriLove.Person
true
true
18 * */
}
}
class Person {
private String name;
private int age;
public String lover;

public Person() {}

public String getName() {return name;}

public void setName(String name) {this.name = name;}

public int getAge() {return age;}

public void setAge(int age) {this.age = age;}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public void eat() { System.out.println("eat.....");}

public void eat(String food) {
System.out.println("eat....." + food);
}

public static void show(){
System.out.println("hello world!!!");
}
}

  • Class对象的功能:Declared表示暴力反射,忽略访问修饰符,在反射面前是没有什么隐私的

    ① 获取成员变量们

    • Field[] getFields():获取所有的public修饰的成员变量

    • Field getField(String name):获取指定名称的public修饰的成员变量

    • Field[] getDeclaredFields():获取所有的成员变量,不考虑修饰符

    • Field getDeclaredField(String name):获取指定名称的成员变量,不考虑修饰符

    ② 获取构造方法们

    • Constructor<?>[] getConstructors()
    • Constructor getConstructors(类<?>…parameterTypes)
    • Constructor getDeclaredConstructors(类<?>…parameterTypes)
    • Constructor<?>[] getDeclaredConstructors()

    ③ 获取成员方法们

    • Method[] getMethods()
    • Method getMethods(String name,类<?>…parameterTypes)
    • Method[] getDeclaredMethods()
    • Method getDeclaredMethods(String name,类<?>…parameterTypes)

    ④ 获取类名

    • String name
  1. Field:成员变量
1
2
3
4
操作:
1. 设置值 void set(Object obj,Object value)
2. 获取值 get(Object obj)
3. 忽略访问权限修饰符的安全检查 setAccessible(true):暴力反射,在反射机制中不存在什么public,private等之类的权限概念
  1. Constructor:构造方法
1
2
创建对象: T newInstance(Object... initargs)
如果使用空参数构造方法创建对象,操作可以简化:Class对象的newInstance方法
  1. Method:方法对象
1
2
执行方法:Object invoke(Object obj,Object... args)
获取方法名称:

举例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package org.AhriLove;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflerctDemo2{

public static void main(String[] args) throws Exception {
// 0.获取字节码对象
Class personClass = Person.class;

// 1.Field[] getFields()获取所有的public修饰的成员变量
Field[] fields = personClass.getFields();
for (Field field:fields) {
System.out.println(field);
}

System.out.println("-----------------");

// 2.获取构造方法
Constructor constructor = personClass.getConstructor(String.class,int.class);
System.out.println(constructor);
// 全参创建对象
Object people = constructor.newInstance("成都",150);
System.out.println(people);

// 空参创建对象
Constructor constructor2 = personClass.getConstructor();
Object people2 = constructor2.newInstance();
System.out.println(people2);

System.out.println("-----------------");

// 3. 获取指定名称的方法
Method eat_method = personClass.getMethod("eat");
Person person = new Person();
// 执行方法
eat_method.invoke(person);


Method eat_method2 = personClass.getMethod("eat",String.class);
// 执行方法
eat_method2.invoke(person,"冰欺凌");

System.out.println("------------------");

// 获取所有的public修饰的方法
Method[] methods = personClass.getMethods();
for (Method method:methods) {
System.out.println(method);
// 获取方法名称
System.out.println(method.getName());
}

System.out.println("--------------");
// 4.获取类名
String className = personClass.getName();
System.out.println(className);
}
}

反射实际应用: 写一个“框架”,可以帮我们创建任意类的对象,并且执行其中的任意方法,实现原理

  • 将需要创建的对象的全类名和执行的方法定义在配置文件中
  • 在程序加载读取配置文件
  • 使用反射技术来加载文件进内存
  • 创建对象
  • 执行方法
  1. 在src目录下创建一个配置pro.properties文件
    1
    2
    className=org.AhriLove.Person
    methodName=eat

2.开始完成框架类

3.注解

注解的本质其实就是一个接口interface

使用注解

注解:放在Java源码的类、方法、字段、参数前的一种特殊的“注释”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 自定义一个注解(新建一个Report.java的接口)
package com.gearsnet;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
// 注解名为Report
public @interface Report {
// int类型的type()方法
int type() default 0;

String level() default "info";

String value() default "";

String[] str1() default "";
...

}

注释会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据”。

  1. 注解的作用

从JVM的角度开看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定

Java的注解可以分为三类:

  • 第一类是由编译器使用的注解,例如:

@Override:让编译器检查该方法是否正确地实现了覆写;
@SuppressWarnings:告诉编译器忽略此处代码产生的警告。
这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。

  • 第二类是由工具处理.class文件使用的注解

比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。

  • 第三类是在程序运行期能够读取的注解

它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。

定义一个注解时,还可以定义配置参数,配置参数可以包括:

1
2
3
4
所有的基本类型
String
枚举类型
基本类型,String以及枚举的数组

基本格式:数据类型 属性名() default 默认值;
注意:虽然叫做属性,但是实际上是一个方法,因为注解本质上也是一个接口,接口是比抽象方法更抽象的抽象方法,它不存在属性名(字段)


因为配置参数必须是常量,所以,上述限制保证了注解在定义时就已经确定了每个参数的值。

注解的配置参数可以有默认值,缺少某个配置参数时将使用默认值。

此外,大部分注解会有一个名为value的配置参数,对此参数赋值,可以只写常量,相当于省略了value参数。如果只写注解,相当于全部使用默认值。

举例说明:
@Check就是一个注解。第一个@Check(min=0, max=100, value=55)明确定义了三个参数,第二个@Check(value=99)只定义了一个value参数,它实际上和@Check(99)是完全一样的。最后一个@Check表示所有参数都使用默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Hello {
@Check(min=0, max=100, value=55)
public int n;

@Check(value=99)
public int p;

@Check(99) // @Check(value=99)
public int x;

@Check
public int y;
}

定义注解

Java语言使用@interface语法来定义注解(Annotation),它的格式如下:

1
2
3
4
5
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

注解的参数类似无参数方法,可以用default设定一个默认值(强烈推荐)。最常用的参数应当命名为value。

元注解:实际上就是注解的注解

有一些注解可以修饰其他注解,这些注解就称为元注解(meta annotation)。Java标准库已经定义了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。

@Target

最常用的元注解是@Target,使用@Target可以描述Annotation能够作用的位置:

  • 作用于类或接口: ElementType.TYPE
  • 作用于成员变量: ElementType.FIELD
  • 作用于方法:ElementType.METHOD
  • 作用于构造方法:ElementType.CONSTRUCTOR
  • 作用于方法参数:ElementType.PARAMETER

例如,定义注解@Report可用在方法上,我们必须添加一个@Target(ElementType.METHOD):

1
2
3
4
5
6
@Target(ElementType.METHOD)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

定义注解@Report可用在方法或字段上,可以把@Target注解参数变为数组{ ElementType.METHOD, ElementType.FIELD }:

1
2
3
4
5
6
7
@Target({
ElementType.METHOD,
ElementType.FIELD
})
public @interface Report {
...
}

实际上@Target定义的value是ElementType[]数组,只有一个元素时,可以省略数组的写法。

@Retention

元注解@Retention定义了Annotation的生命周期:

  • 仅编译期:RetentionPolicy.SOURCE;
  • 仅class文件:RetentionPolicy.CLASS;
  • 运行期,当前描述的注解会保留到class字节码文件中,并被jvm读取到:RetentionPolicy.RUNTIME。

如果@Retention不存在,则该Annotation默认为CLASS。因为通常我们自定义的Annotation都是RUNTIME,所以,务必要加上@Retention(RetentionPolicy.RUNTIME)这个元注解:

1
2
3
4
5
6
7
// 作用于运行期阶段
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

@Repeatable

使用@Repeatable这个元注解可以定义Annotation是否可重复。这个注解应用不是特别广泛

@Inherited

使用@Inherited定义子类是否可继承父类定义的Annotation。@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效:

1
2
3
4
5
6
7
@Inherited
@Target(ElementType.TYPE)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

在使用的时候,如果一个类用到了@Report:

1
2
3
@Report(type=1)
public class Person {
}

则它的子类默认也定义了该注解:

1
2
public class Student extends Person {
}

注解总结

如何定义Annotation,我们总结一下定义Annotation的步骤:

  • 第一步,用@interface定义注解:注解名为Report,新建一个名为Report的接口
1
2
3
Report.java
public @interface Report {
}
  • 第二步,添加参数、默认值:

格式为:数据类型 属性名() default 默认值;

1
2
3
4
5
6
public @interface Report {
// 数据类型 属性名() default 默认值
int type() default 0;
String level() default "info";
String value() default "";
}

把最常用的参数定义为value(),推荐所有参数都尽量设置默认值。

  • 第三步,用元注解配置注解:
1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

其中,必须设置@Target和@Retention,@Retention一般设置为RUNTIME,因为我们自定义的注解通常要求在运行期读取
一般情况下,不必写@Inherited和@Repeatable。

处理注解

我们接下来思考如何解析获取注解中的内容~通过优化反射上面的案例:创建任意类的对象,并且执行其中的任意方法

  1. 创建一个自定义的注解: Report.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.gearsnet;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
// String类型的className属性(实际上为抽象方法)
String className() default "";
String methodName() default "";
}
  1. 创建一个Person对象:Person.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Person {
private String name;
private int age;
public String lover;

public Person() {}

public String getName() {return name;}

public void setName(String name) {this.name = name;}

public int getAge() {return age;}

public void setAge(int age) {this.age = age;}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public static void show(){
System.out.println("hello world!!!");
}
}
  1. 创建一个AnnotationDemo.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.gearsnet;
import java.lang.reflect.Method;

/*
* 注解赋值:与注解定义类的属性相同
* 数组的赋值:使用一个大括号来包裹数组集
* */
@Report(type = 1,level = "best",str1 = {"abc","chd"},className = "com.gearsnet.Person",methodName = "show")
public class AnnotationDemo {
public static void main(String[] args) throws Exception {
// 1. 获取该类的字节码文件对象
Class<AnnotationDemo> annotation = AnnotationDemo.class;

// 2. 获取注解内容
Report report = annotation.getAnnotation(Report.class);

// 3. 调用注解对象中的抽象方法
System.out.println(report.className());
System.out.println(report.methodName());
// 4.加载进内存
Class cls = Class.forName(report.className());
// 5. 创建对象
Object obj = cls.newInstance();
// 6.获取方法的对象
Method method = cls.getMethod(report.methodName());
// 7.执行方法(执行Person对象中的show方法)
method.invoke(obj);
/*
* 打印内容:
com.gearsnet.Person
show
hello world!!!
* */
}
}

最终完成了写一个“框架”,可以帮我们创建任意类的对象,并且执行其中的任意方法

小结:

  1. 大多数时候,我们会使用注解,而不是定义注解
  2. 注解给谁用?
    • 编译器使用
    • 给解析程序使用
  3. 注解不是程序的一部分,相当于一个标签

 评论

联系我 | Contact with me

Copyright © 2019-2020 谁知你知我,我知你知深。此恨经年深,比情度日久

博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议