您的位置 首页 java

这一篇 Java 注解,写得太好了

前言

Java 注解是在JDK1.5被引入的技术,配合反射可以在运行期间处理注解,配合apt tool可以在编译器处理注解,在JDK1.6之后,apt tool被整合到了 Javac 里面。

什么是注解

注解其实就是一种标记,常常用于代替冗余复杂的配置(XML、properties)又或者是编译器进行一些检查如JDK自带的 Override、Deprecated 等,但是它本身并不起任何作用,可以说有它没它都不影响程序的正常运行,注解的作用在于 「注解的处理程序」 ,注解处理程序通过捕获被注解标记的代码然后进行一些处理,这就是注解工作的方式。

在java中,自定义一个注解非常简单,通过 @interface 就能定义一个注解,实现如下

 public @interface PrintMsg {}
  

写个测试类给他加上我们写的这个注解吧

 @PrintMsgpublic class AnnotationTest {    public static void main(String[] args) {        System.out.println(" annotation  test OK!");    }}
  

我们发现写与不写这个注解的效果是相同的,这也印证了我们说的注解只是一种 「标记」 ,有它没它并不影响程序的运行。

元注解

在实现这个注解功能之前,我们先了解一下元注解。

元注解:对注解进行注解,也就是对注解进行标记,元注解的背后处理逻辑由apt tool提供,对注解的行为做出一些限制,例如生命周期,作用范围等等。

@Retention

用于描述注解的生命周期,表示注解在什么范围有效,它有三个取值,如下表所示:

类型作用 SOURCE注解只在源码阶段保留,在编译器进行编译的时候这类注解被抹除,常见的@Override就属于这种注解CLASS注解在编译期保留,但是当Java虚拟机加载 class文件 时会被丢弃,这个也是@Retention的 「默认值」 。@Deprecated和@NonNull就属于这样的注解RUNTIME注解在运行期间仍然保留,在程序中可以通过反射获取,Spring中常见的@Controller、@ Service 等都属于这一类

@Target

用于描述注解作用的 「对象类型」 ,这个就非常多了,如下表所示:

类型作用的对象类型 TYPE类、接口、枚举FIELD类属性METHOD方法PARAMETER参数类型CONSTRUCTOR构造方法LOCAL_VARIABLE局部变量ANNOTATION_TYPE注解PACKAGE包TYPE_PARAMETER1.8之后,泛型TYPE_USE1.8之后,除了PACKAGE之外任意类型

@Documented

将注解的元素加入Javadoc中

@Inherited

如果被这个注解标记了,被标记的类、接口会继承父类、接口的上面的注解

@Repeatable

表示该注解可以重复标记

注解的属性

除了元注解之外,我们还能给注解添加属性,注解中的属性以 无参方法的形式定义 ,方法名为属性名,返回值为成员变量的类型,还是以上述注解为例:

首先给这个注解加亿点点细节,生命周期改为Runtime,使得运行期存在可以被我们获取

 @Retention(RetentionPolicy.RUNTIME)public @interface PrintMsg {    int count() default 1;    String name() default "my name is PrintMsg";}@PrintMsg(count = 2020)public class  Annotation Test {    public static void main(String[] args) {        //通过反射获取该注解        PrintMsg annotation = AnnotationTest.class.getAnnotation(PrintMsg.class);        System.out.println(annotation.count());        System.out.println(annotation.name());    }}
  

输出如下:

 2020my name is PrintMsg
  

到这里就有两个疑问了:

  1. getAnnotation获取到的是什么?一个实例?注解是一个类?
  2. 我们明明调用的是count(),name(),但是为什么说是注解的属性?

等下聊

到底什么是注解?

按照注解的生命周期以及处理方式的不同,通常将注解分为 「运行时注解」 「编译时注解」

  • 运行时注解的本质是实现了Annotation接口的特殊接口,JDK在运行时为其创建代理类,注解方法的调用实际是通过AnnotationInvocationHandler的invoke方法,AnnotationInvocationHandler其中维护了一个Map,Map中存放的是方法名与返回值的映射,对注解中自定义方法的调用其实最后就是用方法名去查Map并且放回的一个过程
  • 编译时注解通过注解处理器来支持,而注解处理器的实际工作过程由JDK在编译期提供支持,有兴趣可以看看javac的源码

运行时注解原理详解

之前我们说注解是一种标记,只是针对注解的作用而言,而Java语言层面注解到底是什么呢?以JSL中的一段话开头

简单来说就是,注解只不过是在interface前面加了 @ 符号的特殊接口,那么不妨以 PrintMsg.class 开始来看看,通过javap 反编译 的到信息如下:

 public interface com.hustdj.jdkStudy.annotation.PrintMsg extends java.lang.annotation.Annotation  minor version: 0  major version: 52  flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION  this_class: #1                          // com/hustdj/jdkStudy/annotation/PrintMsg  super_class: #3                         // java/lang/ Object   interfaces: 1, fields: 0, methods: 2, attributes: 2Constant pool:   #1 = Class              #2             // com/hustdj/jdkStudy/annotation/PrintMsg   #2 = Utf8               com/hustdj/jdkStudy/annotation/PrintMsg   #3 = Class              #4             // java/lang/Object   #4 = Utf8               java/lang/Object   #5 = Class              #6             // java/lang/annotation/Annotation   #6 = Utf8               java/lang/annotation/Annotation   #7 = Utf8               count   #8 = Utf8               ()I   #9 = Utf8               AnnotationDefault  #10 =  Integer             1  #11 = Utf8               name  #12 = Utf8               ()Ljava/lang/String;  #13 = Utf8               my name is PrintMsg  #14 = Utf8               SourceFile  #15 = Utf8               PrintMsg.java  #16 = Utf8               RuntimeVisibleAnnotations  #17 = Utf8               Ljava/lang/annotation/Retention;  #18 = Utf8               value  #19 = Utf8               Ljava/lang/annotation/RetentionPolicy;  #20 = Utf8               RUNTIME{  public abstract int count();    descriptor: ()I    flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT    AnnotationDefault:      default_value: I#10  public abstract java.lang.String name();    descriptor: ()Ljava/lang/String;    flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT    AnnotationDefault:      default_value: s#13}SourceFile: "PrintMsg.java"RuntimeVisibleAnnotations:  0: #17(#18=e#19.#20)
  

从第一行就不难看出,注解是一个继承自 Annotation 接口的接口,它并不是一个类,那么 getAnnotation() 拿到的到底是什么呢?不难想到,通过动态代理生成了代理类,是这样的嘛?通过启动参数 -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true 或者在上述代码中添加:

System.getProperties().put(“sun.misc.ProxyGenerator.saveGeneratedFiles”,”true”); 将通过JDK的proxyGenerator生成的代理类保存下来在 com.sun.proxy 文件夹下面找到这个class文件,通过javap反编译结果如下:

 public final class com.sun.proxy.$Proxy1 extends java.lang.reflect.Proxy implements com.hustdj.jdkStudy.annotation.PrintMsg
  

可以看出JDK通过动态代理实现了一个类继承我们自定义的PrintMsg接口,由于这个方法字节码太长了,看起来头疼,利用idea自带的反编译直接在idea中打开该class文件如下:

 public final class $Proxy1 extends Proxy    implements PrintMsg{    public $Proxy1(InvocationHandler invocationhandler)    {        super(invocationhandler);    }    public final boolean equals(Object obj)    {        try        {            return ((Boolean)super.h.invoke(this, m1, new Object[] {                obj            })).booleanValue();        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final String name()    {        try        {            return (String)super.h.invoke(this, m3, null);        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final String toString()    {        try        {            return (String)super.h.invoke(this, m2, null);        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final int count()    {        try        {            return ((Integer)super.h.invoke(this, m4, null)).intValue();        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final Class annotationType()    {        try        {            return (Class)super.h.invoke(this, m5, null);        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    public final int hashCode()    {        try        {            return ((Integer)super.h.invoke(this, m0, null)).intValue();        }        catch(Error _ex) { }        catch(Throwable throwable)        {            throw new UndeclaredThrowableException(throwable);        }    }    private static Method m1;    private static Method m3;    private static Method m2;    private static Method m4;    private static Method m5;    private static Method m0;    static     {        try        {            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {                Class.forName("java.lang.Object")            });            m3 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("name", new Class[0]);            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);            m4 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("count", new Class[0]);            m5 = Class.forName("com.hustdj.jdkStudy.annotation.PrintMsg").getMethod("annotationType", new Class[0]);            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);        }        catch(NoSuchMethodException nosuchmethodexception)        {            throw new NoSuchMethodError(nosuchmethodexception.getMessage());        }        catch(ClassNotFoundException classnotfoundexception)        {            throw new NoClassDefFoundError(classnotfoundexception.getMessage());        }    }}
  

小结

至此就解决了第一个疑问了, 「所谓的注解其实就是一个实现了Annotation的接口,而我们通过反射获取到的实际上是通过JDK动态代理生成的代理类,这个类实现了我们的注解接口」

AnnotationInvocationHandler

那么问题又来了,具体是如何调用的呢?

$Proxy1 的count方法为例

 public final int count(){    try    {        return ((Integer)super.h.invoke(this, m4, null)).intValue();    }    catch(Error _ex) { }    catch(Throwable throwable)    {        throw new UndeclaredThrowableException(throwable);    }}
  

跟进super

 public class Proxy implements java.io.Serializable {    protected InvocationHandler h;}
  

这个InvocationHandler是谁呢?通过在 Proxy(InvocationHandler h) 方法上打断点追踪结果如下:

这一篇 Java 注解,写得太好了

原来我们对于 count 方法的调用传递给了 AnnotationInvocationHandler

看看它的 invoke 逻辑

 public Object invoke(Object var1, Method var2, Object[] var3) {    //var4-方法名    String var4 = var2.getName();    Class[] var5 = var2.getParameterTypes();    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {        return this.equalsImpl(var3[0]);    } else if (var5.length != 0) {        throw new AssertionError("Too many parameters for an annotation method");    } else {        byte var7 = -1;        switch(var4.hashCode()) {            case -1776922004:                if (var4.equals("toString")) {                    var7 = 0;                }                break;            case 147696667:                if (var4.equals("hashCode")) {                    var7 = 1;                }                break;            case 1444986633:                if (var4.equals("annotationType")) {                    var7 = 2;                }        }        switch(var7) {            case 0:                return this.toStringImpl();            case 1:                return this.hashCodeImpl();            case 2:                return this.type;            default:                //因为我们是count方法,走这个分支                Object var6 = this.memberValues.get(var4);                if (var6 == null) {                    throw new IncompleteAnnotationException(this.type, var4);                } else if (var6 instanceof ExceptionProxy) {                    throw ((ExceptionProxy)var6).generateException();                } else {                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {                        var6 = this.cloneArray(var6);                    }     //返回var6                    return var6;                }        }    }}
  

这个memberValues是啥?

 private final Map<String, Object> memberValues;
  

他是一个map,存放的是方法名(String)与值的键值对

这里以 count() 方法的invoke执行为例

这一篇 Java 注解,写得太好了

可以看到它走了default的分支,从上面的map中取到了,我们所定义的2020,那这个 memberValues 是什么时候解析出来的呢?

通过查看方法调用栈,我们发现在下图这个时候 count name 还没有赋值

这一篇 Java 注解,写得太好了

在方法中加入断点重新调试得到如下结果

这一篇 Java 注解,写得太好了

2020出现了,再跟进 parseMemberValue 方法中,再次重新调试

这一篇 Java 注解,写得太好了

再跟进 parseConst 方法

这一篇 Java 注解,写得太好了

康康javap反编译的字节码中的常量池吧

 #71 = Integer            2020
  

好巧啊,正好是2020!!

因此发现最后是从 ConstantPool 中根据偏移量来获取值的,至此另一个疑问也解决了,我们在注解中设置的方法,最终在调用的时候,是从一个以<方法名,属性值>为键值对的map中获取属性值,定义成方法只是为了在反射调用作为参数而已,所以也可以将它看成属性吧。

总结

运行时注解的产生作用的步骤如下:

  1. 对annotation的反射调用使得动态代理创建实现该注解的一个类
  2. 代理背后真正的处理对象为 AnnotationInvocationHandler ,这个类内部维护了一个map,这个map的键值对形式为<注解中定义的方法名,对应的属性名>
  3. 任何对annotation的自定义方法的调用(抛开动态代理类继承自object的方法),最终都会实际调用 AnnotatiInvocationHandler 的invoke方法,并且该invoke方法对于这类方法的处理很简单,拿到传递进来的方法名,然后去查map
  4. map中memeberValues的初始化是在 AnnotationParser 中完成的,是勤快的,在方法调用前就会初始化好,缓存在map里面
  5. AnnotationParser最终是通过ConstantPool对象从常量池中拿到对应的数据的,再往下ConstantPool对象就不深入了

编译时注解初探

由于编译时注解的很多处理逻辑内化在Javac中,这里不做过多探讨,仅对《深入理解JVM》中的知识点进行梳理和总结。

在JDK5中,Java语言提供了对于注解的支持,此时的注解只在程序运行时发挥作用,但是在JDK6中,JDK新加入了一组 插入式注解处理器 的标准API,这组API使得我们对于注解的处理可以提前至编译期,从而影响到前端编译器的工作!!常用的Lombok就是通过注解处理器来实现的

「自定义简单注解处理器」

实现自己的注解处理器,首先需要继承抽象类 javax.annotation. process ing.AbstractProcessor ,只有 process() 方法需要我们实现, process() 方法如下:

 //返回值表示是否修改Element元素public abstract boolean process(Set<? extends TypeElement> annotations,                                RoundEnvironment roundEnv);
  
  • annotations:这个注解处理器处理的注解集合
  • roundEnv:当前round的抽象语法树结点,每一个结点都为一个Element,一共有18种Element包含了Java中 的所有元素:
  • PACKAGE(包)
  • ENUM(枚举)
  • CLASS(类)
  • ANNOTATION_TYPE(注解)
  • INTERFACE(接口)
  • ENUM_CONSTANT(枚举常量)
  • FIELD(字段)
  • PARAMETER(参数)
  • LOCAL_VARIABLE(本地变量)
  • EXCEPTION_PARAMETER(异常)
  • METHOD(方法)
  • CONSTRUCTOR(构造方法)
  • STATIC_INIT(静态代码块)
  • INSTANCE_INIT(实例代码块)
  • TYPE_PARAMETER(参数化类型,泛型尖括号中的)
  • RESOURCE_VARIABLE(资源变量,try-resource)
  • MODULE(模块)
  • OTHER(其他)

此外还有一个重要的实例变量 processingEnv ,它提供了上下文环境,需要创建新的代码,向编译器输出信息,获取其他工具类都可以通过它

实现一个简单的编译器注解处理器也非常简单,继承 AbstractProcessor 实现 process() 方法,在 process() 方法中实现自己的处理逻辑即可,此外需要两个注解配合一下:

  • @SupportedAnnotationTypes:该注解处理器处理什么注解
  • @SupportedSourceVersion:注解处理器支持的语言版本

「实例」

 @SupportedAnnotationTypes("com.hustdj.jdkStudy.annotation.PrintMsg")@SupportedSourceVersion(SourceVersion.RELEASE_8)public class PrintNameProcessor extends AbstractProcessor {    @Override    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {        Messager messager = processingEnv.getMessager();        for (Element element : roundEnv.getRootElements()) {            messager.printMessage(Diagnostic.Kind.NOTE,"my name is "+element.toString());        }        //不修改语法树,返回false        return false;    }}
  

输出如下:

 G:ideaIUideaProjectscookcodesrcmainjava>javac comhustdjjdkStudyannotationPrintMsg.javaG:ideaIUideaProjectscookcodesrcmainjava>javac comhustdjjdkStudyannotationPrintNameProcessor.javaG:ideaIUideaProjectscookcodesrcmainjava>javac -processor com.hustdj.jdkStudy.annotation.PrintNameProcessor comhustdjjdkStudyannotationAnnotationTest.java警告: 来自注释处理程序 'com.hustdj.jdkStudy.annotation.PrintNameProcessor' 的受支持 source 版本 'RELEASE_8' 低于 -source '1.9'注: my name is com.hustdj.jdkStudy.annotation.AnnotationTest1 个警告
  

最后给大家送下福利,大家可以在后台私信 面试 ,即可以获取一份我整理的最新Java面试题资料。

文章来源:智云一二三科技

文章标题:这一篇 Java 注解,写得太好了

文章地址:https://www.zhihuclub.com/184674.shtml

关于作者: 智云科技

热门文章

发表回复

您的电子邮箱地址不会被公开。

网站地图