java注解的实现原理(1)

java 注解的实现原理(1)

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

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

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

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

自定义注解 Info.java

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

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

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 用来配置加载我们的配置文件

import java.lang.annotation.*;

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

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

import java.lang.annotation.*;

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

使用以上两个注解的 User.java

package com.ths.annotaion;

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

<span class="hljs-meta">@ConfigField("user.id")</span>
<span class="hljs-keyword">private</span> String id;

<span class="hljs-meta">@ConfigField("name")</span>
<span class="hljs-keyword">private</span> String name;

<span class="hljs-keyword">public</span> String <span class="hljs-title function_">getId</span><span class="hljs-params">()</span> {
    <span class="hljs-keyword">return</span> id;
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setId</span><span class="hljs-params">(String id)</span> {
    <span class="hljs-built_in">this</span>.id = id;
}

<span class="hljs-keyword">public</span> String <span class="hljs-title function_">getName</span><span class="hljs-params">()</span> {
    <span class="hljs-keyword">return</span> name;
}

<span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">setName</span><span class="hljs-params">(String name)</span> {
    <span class="hljs-built_in">this</span>.name = name;
}

}

中间配置类 AnnoResolve.java

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

<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> &lt;T&gt; <span class="hljs-keyword">void</span> <span class="hljs-title function_">configField</span><span class="hljs-params">(Class&lt;? extends Object&gt; cls,T t)</span> <span class="hljs-keyword">throws</span> IOException, IllegalAccessException {
    <span class="hljs-comment">// 获取到cls资源上的注解代理类,使用其中的value()方法得到其中的配置文件路径</span>
    <span class="hljs-type">String</span> <span class="hljs-variable">filePath</span> <span class="hljs-operator">=</span> cls.getAnnotation(LoadProperty.class).value();
    <span class="hljs-type">Properties</span> <span class="hljs-variable">properties</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Properties</span>();

// 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

public class TestUser {
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> {
    <span class="hljs-type">User</span> <span class="hljs-variable">user</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">User</span>();
    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()方法来获取对应的值,一个注解中定义有多个函数时,在使用注解是需要显示的赋值如下所示。

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

    @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,不想继续往下看,这样记住返回出来的值就是一个该注解的一个动态代理的代理类。