Java注解原理解析

注解出现的背景

以前[XML]是各大框架的青睐者,它以松耦合的方式完成了框架中几乎所有的配置,但是随着项目越来越庞大,[XML]的内容也越来越复杂,维护成本变高。

于是就有人提出来一种标记式高耦合的配置方式,即[注解]。方法、类、字段属性上都可以注解,反正几乎需要配置的地方都可以进行注解。

注解可以提供更大的便捷性,易于维护修改,但耦合度高,而 XML 相对于注解则是相反的。追求低耦合就要抛弃高效率,追求高效率必然会遇到耦合。

注解的本质

按照官网解释:The common interface extended by all annotation types。所有的注解类型都继承自这个普通的接口(Annotation)。

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

一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。

而解析一个类或者方法的注解往往有两种形式,一种是编译期直接的扫描一种是运行期反射。反射的事情我们待会说,而编译器的扫描指的是编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。

典型的就是注解 @Override,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。

这一种情况只适用于那些编译器已经熟知的注解类,比如 JDK 内置的几个注解,而你自定义的注解,编译器是不知道你这个注解的作用的,当然也不知道该如何处理,往往只是会根据该注解的作用范围来选择是否编译进字节码文件,仅此而已。

 元注解

[元注解]是用于修饰注解的注解,通常用在注解的定义上,例如:

@Target(ElementType.METHOD) // 元注解
@Retention(RetentionPolicy.SOURCE) // 元注解
public @interface Override {
}

JAVA 中有以下几个[元注解]:

  • @Target:注解的作用目标
  • @Retention:注解的生命周期
  • @Documented:注解是否应当被包含在 JavaDoc 文档中
  • @Inherited:是否允许子类继承该注解

[元注解]一般用于指定某个注解生命周期以及作用目标等信息。

JAVA 的内置三大注解

除了上述四种元注解外,JDK 还为我们预定义了另外三种内置注解,它们是:

  • @Override
  • @Deprecated
  • @SuppressWarnings

1. @Override注解

@Target(ElementType.METHOD) // 元注解,只能作用于方法上
@Retention(RetentionPolicy.SOURCE) // 元注解,当前注解编译期可见,不会写入class文件,编译结束后丢弃
public @interface Override {
}

@override是一种典型的[标记式注解],仅被编译器可知,编译器在对 java 文件进行编译成字节码的过程中,一旦检测到某个方法上被修饰了该注解,就会去匹对父类中是否具有一个同样方法签名的函数,如果不是,自然不能通过编译。

2. @Deprecated 

@Documented
@Retention(RetentionPolicy.RUNTIME) // 永久保存,可以反射获取
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) 
// 作用于构造器、属性字段、本地局部变量、方法、包、方法参数、类接口和枚举类
public @interface Deprecated {
}

[标记式注解],永久存在,可以修饰所有的类型。作用是标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除。

3. @SuppressWarnings

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}

@SuppressWarnings 主要用来压制 java 的警告,它有一个 value 属性需要你主动的传值,这个 value 代表一个什么意思呢,这个 value 代表的就是需要被压制的警告类型。例如:@SuppressWarning(value = "deprecated"),压制使用被弃用方法的警告。

注解与反射

[注解的本质就是一个继承了 Annotation 接口的接口],现在我们就来从虚拟机的层面看看,注解的本质到底是什么。

首先,我们自定义一个注解类型:

@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Hello {
    String value();
}

这里我们指定了 Hello 这个注解只能修饰字段和方法,并且该注解永久存活,以便我们反射获取。

虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,就可以被写进字节码文件。属性表有以下几种:

  • RuntimeVisibleAnnotations:运行时可见的注解
  • RuntimeInVisibleAnnotations:运行时不可见的注解
  • RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
  • RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
  • AnnotationDefault:注解类元素的默认值

给大家看虚拟机的这几个注解相关的属性表的目的在于,让大家从整体上构建一个基本的印象,注解在字节码文件中是如何存储的。

所以,对于一个类或者接口来说,Class 类中提供了以下一些方法用于反射注解。

  • getAnnotation:返回指定的注解
  • isAnnotationPresent:判定当前元素是否被指定注解修饰
  • getAnnotations:返回所有的注解
  • getDeclaredAnnotation:返回本元素的指定注解
  • getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的

设置虚拟机启动参数,用于捕获 JDK 动态代理类,便于分析。

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

运行main 函数,生成代理类的class文件。

public class Test {
    @Hello("hello")
    public static void main(String[] args) throws NoSuchMethodException {
        // 获取class对象
        Class clazz = Test.class;
        // 获取main方法对象
        Method method = clazz.getMethod("main", String[].class);
        // 获取main方法上的hello注解
        Hello hello = method.getAnnotation(Hello.class);
        // 获取value值
        String value = hello.value();
    }
}

注解本质上是继承了 Annotation 接口的接口,通过反射 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。

我们运行程序后,会看到输出目录里有这么一个代理类,反编译之后是这样的:通过反编译文件我们能分析出如何通过动态代理获取注解的value值。

public final class $Proxy1 extends Proxy
    implements Hello
{

    public $Proxy1(InvocationHandler invocationhandler)
    {
        super(invocationhandler);
    }

    // .....省略equals、toString、annotationType、hashCode等从Annotation接口继承而来的方法
    
    // 自定义的value方法
    public final String value()
    {
        try
        {
            return (String)super.h.invoke(this, m3, null);
        }
        catch(Error _ex) { }
        catch(Throwable throwable)
        {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m0;
    private static Method m3;

    static 
    {
        try
        {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] {
                Class.forName("java.lang.Object")
            });
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m4 = Class.forName("com.isoftstone.ican.mag.trusted.workbench.test.Hello").getMethod("annotationType", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            m3 = Class.forName("com.isoftstone.ican.mag.trusted.workbench.test.Hello").getMethod("value", new Class[0]);
        }
        catch(NoSuchMethodException nosuchmethodexception)
        {
            throw new NoSuchMethodError(nosuchmethodexception.getMessage());
        }
        catch(ClassNotFoundException classnotfoundexception)
        {
            throw new NoClassDefFoundError(classnotfoundexception.getMessage());
        }
    }
}

代理类实现接口 Hello 并重写其所有方法,包括 value 方法以及接口 Hello 从 Annotation 接口继承而来的方法。构造方法接受一个InvocationHandler实例,是其子类AnnotationInvocationHandler的实例

AnnotationInvocationHandler 是 JAVA 中专门用于处理注解的 Handler

class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private static final long serialVersionUID = 6182022883658399397L;
    private final Class<? extends Annotation> type;
    // memberValues key是注解属性名称,value是该属性当初被赋上的值。
    // 我们自定义注解的"hello"值就存在此map中{"value" : "hello"}
    private final Map<String, Object> memberValues;
    private transient volatile Method[] memberMethods = null;

    AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
        Class[] var3 = var1.getInterfaces();
        if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
            this.type = var1;
            this.memberValues = var2;
        } else {
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        }
    }

    public Object invoke(Object var1, Method var2, Object[] var3) {
        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 {
            switch (var4) {
                case "toString":
                    return this.toStringImpl();
                case "hashCode":
                    return this.hashCodeImpl();
                case "annotationType":
                    return this.type;
                default:
                    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);
                        }
                        return var6;
                    }
            }
        }
    }
}

m3 = Class.forName("com.isoftstone.ican.mag.trusted.workbench.test.Hello").getMethod("value", new Class[0]); 即value方法

value()方法调用过程:$Proxy1.value() --> (String)super.h.invoke(this, m3, null) -->  invoke(Object var1, Method var2, Object[] var3) --> String var4 = var2.getName() 获取方法名"value" --> Object var6 = this.memberValues.get(var4) 通过value获取属性值"hello"。即通过通过方法名返回注解属性值。

参考:JAVA 注解的基本原理 - Single_Yam - 博客园

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值