zoukankan      html  css  js  c++  java
  • 77

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

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

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

    public @interface PrintMsg {
    }
    写个测试类给他加上我们写的这个注解吧

    @PrintMsg
    public 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_PARAMETER 1.8之后,泛型
    TYPE_USE 1.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 AnnotationTest {
    public static void main(String[] args) {
    //通过反射获取该注解
    PrintMsg annotation = AnnotationTest.class.getAnnotation(PrintMsg.class);
    System.out.println(annotation.count());
    System.out.println(annotation.name());
    }
    }
    输出如下:

    2020
    my name is PrintMsg
    到这里就有两个疑问了:

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

    到底什么是注解?
    按照注解的生命周期以及处理方式的不同,通常将注解分为运行时注解和编译时注解

    运行时注解的本质是实现了Annotation接口的特殊接口,JDK在运行时为其创建代理类,注解方法的调用实际是通过AnnotationInvocationHandler的invoke方法,AnnotationInvocationHandler其中维护了一个Map,Map中存放的是方法名与返回值的映射,对注解中自定义方法的调用其实最后就是用方法名去查Map并且放回的一个过程
    编译时注解通过注解处理器来支持,而注解处理器的实际工作过程由JDK在编译期提供支持,有兴趣可以看看javac的源码
    运行时注解原理详解
    之前我们说注解是一种标记,只是针对注解的作用而言,而Java语言层面注解到底是什么呢?以JSL中的一段话开头

    An annotation type declaration specifies a new annotation type, a special kind of interface type. To distinguish an annotation type declaration from a normal interface declaration, the keyword interface is preceded by an at-sign (@).

    简单来说就是,注解只不过是在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: 2
    Constant 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());
        }
    }
    
    • 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
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114

    }
    小结
    至此就解决了第一个疑问了,所谓的注解其实就是一个实现了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)方法上打断点追踪结果如下:

    image.png

    原来我们对于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;
                }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    }
    这个memberValues是啥?

    private final Map<String, Object> memberValues;
    他是一个map,存放的是方法名(String)与值的键值对

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

    image.png

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

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

    image.png

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

    image.png

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

    image.png

    再跟进parseConst方法

    image.png

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

    #71 = Integer 2020
    好巧啊,正好是2020!!

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

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

    对annotation的反射调用使得动态代理创建实现该注解的一个类
    代理背后真正的处理对象为AnnotationInvocationHandler,这个类内部维护了一个map,这个map的键值对形式为<注解中定义的方法名,对应的属性名>
    任何对annotation的自定义方法的调用(抛开动态代理类继承自object的方法),最终都会实际调用AnnotatiInvocationHandler的invoke方法,并且该invoke方法对于这类方法的处理很简单,拿到传递进来的方法名,然后去查map
    map中memeberValues的初始化是在AnnotationParser中完成的,是勤快的,在方法调用前就会初始化好,缓存在map里面
    AnnotationParser最终是通过ConstantPool对象从常量池中拿到对应的数据的,再往下ConstantPool对象就不深入了
    编译时注解初探
    由于编译时注解的很多处理逻辑内化在Javac中,这里不做过多探讨,仅对《深入理解JVM》中的知识点进行梳理和总结。

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

    自定义简单注解处理器

    实现自己的注解处理器,首先需要继承抽象类javax.annotation.processing.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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    }
    输出如下:

    G:ideaIUideaProjectscookcodesrcmainjava>javac comhustdjjdkStudyannotationPrintMsg.java

    G:ideaIUideaProjectscookcodesrcmainjava>javac comhustdjjdkStudyannotationPrintNameProcessor.java

    G: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.AnnotationTest
    1 个警告

  • 相关阅读:
    Thinkphp整合最新Ueditor编辑器
    git管理和自动部署项目
    [转]桶
    Linux下ThinkPHP网站目录权限设置
    ThoughtWorks FizzBuzzWhizz 代码实现
    windows bat脚本实现ftp自动下载 删除
    MCM/ICM2018美国大学生数学建模大赛D题翻译
    解决C/C++语言中全局变量重复定义的问题
    使用fabric解决百度BMR的spark集群各节点的部署问题
    解决百度BMR的spark集群开启slaves结点的问题
  • 原文地址:https://www.cnblogs.com/gd11/p/14217779.html
Copyright © 2011-2022 走看看