java注解的实现原理(1)

注解的本质就是一个继承了Annotation接口的接口

写在前面,在前面总结了java反射和动态代理的一些知识,同时之前没有仔细研究注解这块,只知道注解的实现原理是基于动态代理的,主要作用有一下:

  • 1.编译检查:例如使用@SupperssWarnings,@Override都具有编译检查的作用。
  • 2.可以帮助生成文档,例如@Return @Param等注解。
  • 3.在框架中替换之前的xml文件使用注解开发web,例如spring中的各种注解。

之前的理解一直差不多这种,最近刚好不忙想相似的了解一下java注解的实现原理,在开始看自定义注解中,看到很多博客里面写的都是如下这样:

自定义注解Info.java

1
2
3
4
5
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Info{
String value();
}

使用自定义的注解:People.java

1
2
3
4
5
6
7
8
9
10
11
12
public class People{
@Info("张三")
private String name;

public String getName(){
return this.name;
}

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

然后一般到这里就完了,但是当我照着这样写了一遍再用测试类来运行生成People类对象并调用getName()方法时,我们并得不到我们设置的张三,同时我也看不到这个动态代理在哪里。为此有翻阅了好久的资料发现注解不是这样用的。我们自定义的注解其实还需要一个中间配置类来配置的我的注解解析。

如下我们自定义了2个注解@LoadProperty 用来配置加载我们的配置文件,@ConfigField用来配置下面的字段赋值。中间用来配合注解解析的类AnnoResolve.java,使用这两个注解的User.java和测试主函数存在的类TestUser.java。具体代码实现如下所示:

@LoadProperty 用来配置加载我们的配置文件

1
2
3
4
5
6
7
8
import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LoadProperty {
String value(); // 配置文件的路径
}

@ConfigField用来配置下面的字段赋值

1
2
3
4
5
6
7
8
import java.lang.annotation.*;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ConfigField{
String value();
}

使用以上两个注解的User.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
package com.ths.annotaion;

@LoadProperty("D:\\JAVA_HOME\\bevisStudy\\src\\com\\ths\\annotaion\\config.properties")
public class User {
//在类中使用注解

@ConfigField("user.id")
private String id;

@ConfigField("name")
private String name;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

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

中间配置类AnnoResolve.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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class AnnoResolve {

public static <T> void loadProperty(T t){
// 通过传入的user对象来获取User的Class对象cls
Class<? extends Object> cls =t.getClass();
// 通过isAnnotationPresent()方法判断LoadProperty注解是否存在于此元素上面
boolean hasLoadPropertyAnno = cls.isAnnotationPresent(LoadProperty.class);
if(hasLoadPropertyAnno){
//为属性赋值
try {
configField(cls,t);
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}

private static <T> void configField(Class<? extends Object> cls,T t) throws IOException, IllegalAccessException {
// 获取到cls资源上的注解代理类,使用其中的value()方法得到其中的配置文件路径
String filePath = cls.getAnnotation(LoadProperty.class).value();
Properties properties = new Properties();
// InputStream is = AnnoResolve.class.getClassLoader().getResourceAsStream(filePath);
InputStream is = new FileInputStream(filePath);
System.out.println(is);
properties.load(is);
// 通过反射获取到user上面的字段
Field[] fields = cls.getDeclaredFields();
for (Field field : fields) {
// 遍历找到字段上含有注解的字段
boolean hasConfigField = field.isAnnotationPresent(ConfigField.class);
String fieldValue = null;
// 如属性上有注解,使用注解的值作为key去配置文件中查找
if(hasConfigField){
// 获取注解的值
Object annoValue = field.getAnnotation(ConfigField.class).value();
fieldValue = properties.getProperty(annoValue.toString());
// 如属性上没有值
}else{
fieldValue = properties.getProperty(field.getName());
}
// 如果是私有成员变量需要设置为true
field.setAccessible(true);
field.set(t,fieldValue);
}
is.close();
}

}

测试注解在User中使用的TestUser.java

1
2
3
4
5
6
7
8
9
public class TestUser {

public static void main(String[] args) {
User user = new User();
AnnoResolve.loadProperty(user);
System.out.println(user.getId());
System.out.println(user.getName());
}
}

其中还要写一个config.properties配置文件

image-20210720151551278

其所有在的位置就是在User中需要在注解上传入的值。通过运行TestUser就可以得到如下结果:

image-20210720151650255

到此说明通过注解的方法,我们确实给我们创建的User对象赋了我们在配置文件中设置的字符,自定义注解使用成功,下面我们就详细分析一下定义注解的一个过程:

  • 1.注解的定义方式使用@interface关键字,注解里面的都是定义的方法,后面会在通过动态代理的方法实现该注解的代理类,然后调用代理类中的value()方法来获取对应的值,一个注解中定义有多个函数时,在使用注解是需要显示的赋值如下所示。

    1
    2
    3
    4
    5
    public Test{
    // 加入定义的InfoTest注解里面有两个函数value1和value2,使用时需要显式的进行赋值
    @InfoTest(value1="name",value2="test")
    private String name;
    }
  • 2.在自定义注解时,注解上面的是java的一些元注解,作用如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    @Target()  //表示该注解使用的位置,一般有类,方法,字段,构造函数
    ElementType.FIELD 使用在属性字段上
    ElementType.Type 使用在类上
    ElementType.METHOD 使用在方法上
    ElementType.PARAMETER 使用咋方法的参数上
    ElementType.CONSTRUCTOR 使用在构造函数上
    ElementType.ANNOTATION_TYPE 使用在注解上
    ElementType.PACKAGE 使用在包上
    ElementType.LOCAL_VARIABLE 使用在本地局部变量上

    @Retention() // 定义该注解的生命周期
    SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
    CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
    RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;

    @Doucumented // 表示该注解会写入到文档中

    @Inherited // 表示定义的这个注解当标注在一个类上时,当其有其他子类来继承时,子类也会自动继承标注在父类上的注解
  • 3.注解的配置类这一部分其实是真正注解给注解的类赋值的实现过程。

    • a.首先通过传入的user获取到User的Class 对象cls,判断类上面是否有LoadProperty注解,同时可以通过cls.getAnnotation(传入注解的Class对象)可以得到java动态代理生成该注解的代理类,然后使用对应的value()方法获取到前面,我们使用注解传入的配置文件路径。(这一部分的实现源码,在后面的博客中继续分析)
    • b.有的话,通过cls对象反射获取到user中所有的成员字段,遍历所有的字段,使用同样的isAnnotationPresent()方法判断在该元素上时候有我们需要的注解,用需要的方法来取我们需要用到的值。
    • c.通过cls对象使用发射,如果是字段使用set方法来需要的字段赋值刚才通过注解获取到的值。(配置到方法上的以后再看)

以上只是自定义注解的使用整体理解,对于其中的更近一层的细究在后面继续进行。如在getAnnotation()在java源码中是如何生成动态代理中。在程序使用debug你会发现通过getAnnotation()返回的对象属性是$Prxoy1,不想继续往下看,这样记住返回出来的值就是一个该注解的一个动态代理的代理类。