Java注解的使用和深入理解

请添加图片描述

注解的定义

Annotation注解,是一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。

注解本身没有任何意义,单独的注解就是一种注释,他需要结合其他如反射、插桩等技术才有意义。

Java注解(Annotation)又称Java标注,是JDK1.5引入的一种注释机制。是元数据的一种形式,提供有关于程序但不属于程序本身的数据。注解对它们注解的代码的操作没有直接影响。

对比注释:

注释是给程序员看的,注解是给程序看的。(注解不会影响代码的正常执行)

注解的声明

与接口的声明类似,声明注解只是在interface前加上@符号,其上多了两行代码(元注解,下文详说)

在其内部声明属性,此属性必须带括号(先记住,下文再讲)

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {
    int value();
    String id() ;
}

元注解

在使用之前,对注解上面声明的两行代码进行解析,注解在注解上面的注解就是元注解

Java5.0定义了四种元注解:

  • @Target
  • @Retention
  • @Documented
  • @Inherited

常用的就是上面两个

Target

标志此注解注解在何处,传入ElementType

//注解使用的位置
public enum ElementType {
	//类
    TYPE,
    //属性
    FIELD,
    //方法
    METHOD,
    //参数
    PARAMETER,
    //构造
    CONSTRUCTOR,
    LOCAL_VARIABLE,
    //如果此注解的Target为ANNOTATION_TYPE,则标志此注解为元注解,可注解注解
    ANNOTATION_TYPE,
    //包
    PACKAGE,
    TYPE_PARAMETER,
    TYPE_USE;

    private ElementType() {
    }
}

Retention

标志注解的保留级别,后续进行讲解

public enum RetentionPolicy {
    //源码阶段
    SOURCE,
    //类阶段
    CLASS,
    //运行时结合反射使用
    RUNTIME;

    private RetentionPolicy() {
    }
}

注解的使用

声明一个普通的java类,使用上文声明的注解进行注解,上文注解的声明中的元注解targetTYPE,则只能注解类。

@MyAnnotation(value = 1, id = "")
public class MyClass {
}

在使用时必须给注解所有的变量赋值

注意点:

1.若注解有且仅有一个属性value,则可以省略名称,例如@MyAnnotation(1)即可使用

2.若想注解在多处使用可在Target后拼接类型,例如@Target({ElementType.TYPE, ElementType.FIELD})

3.注解的值可以有默认值,有默认值则不强求在使用时赋值,例如String id() default “”; id默认为空

元注解Retention的细节

笔者的理解:

java程序的生命周期.java-> .class -> 运行时

SOURCE可以使注解停留在.java上,而且只会停留在.java

javac在编译时,传入的是.java文件的流,javac会对字符串进行解析翻译成class,停留在.java则意味着javac可在编译时扫描到对应注解的字符串,能扫描到则意味着可对其进行特殊处理,比如语法检查IDE提供的功能),APTjavac提供的功能),下文对其进行讲解。

上述类MyClass使用SOURCE产生的字节码:

public class com/hbsd/lib/MyClass {

  // compiled from: MyClass.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/hbsd/lib/MyClass; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

CLASSRUNTIME可以使注解停留在.class

停留在.class上意味着后续虚拟机在执行时可以感知到,如果是CLASS级别,虚拟机会进行忽略,看得见但是装看不见RUNTIME级别虚拟机则可真正看见。CLASS级别的用途是ASM字节码插桩,既然.class上有此注解信息,插桩也是扫描.class文件,扫描到存在此注解则可对注解处进行操作,RUNTIME时则需结合反射使用。

上述类使用CLASSRUNTIME产生的字节码:

public class com/hbsd/lib/MyClass {

  // compiled from: MyClass.java

  //CLASS生成,后标记invisible
  @Lcom/hbsd/lib/MyAnnotation;(value=1, id="") //invisible 无形的,看不见的
  //RUNTIME生成,后无标记
  @Lcom/hbsd/lib/MyAnnotation;(value=1, id="")
    
  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 5 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/hbsd/lib/MyClass; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1
}

用处总结

级别技术说明
SOURCEAPT,源码检查APT:在编译期能够获取注解与注解声明的类包括类中所有成员信息,一般用于生成额外的辅助类。 源码检查:规定属性范围
CLASS字节码增强在编译出Class后,通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
RUNTIME反射在程序运行期间,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判定。

SOURCE

source有两种用处APT,源码检查

APT

APT即为Annotation Processing Tooljavac的一个工具,中文意思为编译时注解处理器。APT可以用来在编译时扫描和处理注解。

扫描到注解即可对其进行处理,目前最广泛的处理是动态生成一些代码和类。其和反射最大的区别就是再编译期执行,反射是在运行时。

APT运行时机,gradle的构建任务中

Task :app:compileDebugJavaWithJavac

使用

1.创建自己的注解处理器

//只对com.hbsd.lib.MyAnnotation进行处理
@SupportedAnnotationTypes("com.hbsd.lib.MyAnnotation")
public class MyProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
		//处理。。。
        return false;
    }
}

2.注册注解处理器

src下创建resources->META-INF->services->javax.annotation.processing.Processor目录

在最底层文件里写上自己的注解处理器的全路径名

3.在android模块导入java依赖

annotationProcessor project(': ... ')

注意事项

1.APT必须是java模块

2.可使用auto-service自动绑定,则不需要2步骤

注解处理器模块导入依赖

compileOnly'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor'com.google.auto.service:auto-service:1.0-rc4'

MyProcessor类上需要加上

@AutoService(Processor.class)

此种实现兼容性不好,需要适配auto-service版本

源码检查

先看一段代码

@DrawableRes
int drawable;
//爆红
//drawable = R.layout.activity_main;
//不爆红
drawable = R.color.colorAccent;

上述代码R.layout.activity_mainR.color.colorAccent都为int,那为何R.layout.activity_main会爆红呢

因为IDE的源码检查机制,此机制就是使用SOURCE级别的注解实现的。

笔者所了解的有两种限制方式

枚举
//定义枚举
public enum MyValue{
    VALUE1, 
    VALUE2
}

//方法声明
public void test(MyValue value) {

}

//调用
test(MyValue.VALUE1);
注解
@IntDef({1, 2})
@Target({ElementType.PARAMETER,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {
    
}
//方法声明
public void test(@MyAnnotation MyValue value) {

}
//调用
test(1);

//爆红
test(3);
两者优劣

在枚举中VALUE1VALUE2被声明成对象

//枚举字节码片段
// access flags 0x4019
public final static enum Lcom/hbsd/lib/MyValue; VALUE1

// access flags 0x4019
public final static enum Lcom/hbsd/lib/MyValue; VALUE2

注解只是简单使用两个int常量

枚举是两个对象,其每个对象对象头已经是12字节,而int只是4字节,所占内存注解更少,因此对变量进行限制时使用注解较好。

CLASS

字节码插桩

笔者并没有学习插桩的实现细节,只了解大致的原理,如下:

字节码也有独特的格式,class时期的注解在class文件上是存在的,因此解析class文件能够扫描到相应的信息,信息存储要修改的位置和内容,能拿到这些信息就可根据位置和内容进行修改,是增还是删就看具体的业务逻辑了。

RUNTIME

结合反射使用

主要用于运行时结合反射获取注解中的值,对其数据进行操作处理

定义注解MyAnnotation

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

MyClass

public class MyClass {

    @MyAnnotation("zjm")
    private int test;

    public static void main(String args[]) {
        //获取当前类
        Class clazz = MyClass.class;
        //获取当前类全部属性
        Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            //是否被注解
            if (fields[i].isAnnotationPresent(MyAnnotation.class)) {
                //拿到注解的代理类所生成的对象
                MyAnnotation myAnnotation = fields[i].getAnnotation(MyAnnotation.class);
                //获取值
                System.out.println(myAnnotation.value());
            }
        }
    }
}

//打印zjm

不知道读者有没有疑惑,为何接口没有实现也可以获取其中的值,这就要分析其内部的原理

运行时原理

结论:注解本质是接口,在其内部声明属性,其实都是方法,然后通过动态代理,在调用此方法时直接返回相对的值.

为何是接口

MyAnnotation的字节码

public interface com.hbsd.lib.MyAnnotation extends java.lang.annotation.Annotation

发现其继承自Annotation

java.lang.annotation.Annotation

public interface Annotation {
    boolean equals(Object var1);

    int hashCode();

    String toString();

    Class<? extends Annotation> annotationType();
}

上述方法都会在动态代理中进行实现

验证动态代理

动态代理本文不详细解释,动态代理请看笔者另一篇文章,也不过多分析中间的api调用,重点在于注解的原理是不是动态代理

public class MainActivity extends AppCompatActivity {
    @MyAnnotation("zjm")
    private int test;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Class clazz = MainActivity.class;
        Field[] fields = clazz.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            fields[i].setAccessible(true);
            if (fields[i].isAnnotationPresent(MyAnnotation.class)) {
                //1
                MyAnnotation myAnnotation = fields[i].getAnnotation(MyAnnotation.class);
                System.out.println(myAnnotation.value());
            }
        }
    }
}

debug,给ProxynewProxyInstance打上断点。debug运行,果不其然在newProxyInstance停下

oncreat的注释1处进入断点,调用栈如下:

这篇文章默认读者熟悉动态代理

Proxy#newProxyInstance

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h){
    ...
}

重点为第二个和第三个参数、

interfaces如下(截图截不全,但我i保证是上文的MyAnnotation):

h如下:

往上回溯调用栈的第二个方法

AnnotationFactory#createAnnotation

public static <A extends Annotation> A createAnnotation(Class<? extends Annotation> annotationType,
                                                        AnnotationMe1mber[] elements) {
    AnnotationFactory factory = new AnnotationFactory(annotationType, elements);
    //第三个参数为AnnotationFactory类型则其肯定有回调处理方法
    return (A) Proxy.newProxyInstance(annotationType.getClassLoader(),
                                      new Class[]{annotationType}, factory);
}

回调处理方法如下:

AnnotationFactory#invoke

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    String name = method.getName();
    Class[] params = method.getParameterTypes();
    if (params.length == 0) {
        //默认实现的方法
        if ("annotationType".equals(name)) {
            return klazz;
        } else if ("toString".equals(name)) {
            return toString();
        } else if ("hashCode".equals(name)) {
            return hashCode();
        }

        // this must be element value request
        AnnotationMember element = null;
        //根据方法名字找到对应的element
        for (AnnotationMember el : elements) {
            if (name.equals(el.name)) {
                element = el;
                break;
            }
        }
        if (element == null || !method.equals(element.definingMethod)) {
            throw new IllegalArgumentException(method.toString());
        } else {
            //返回该element的值
            Object value = element.validateValue();
            if (value == null) {
                throw new IncompleteAnnotationException(klazz, name);
            }
            return value;
        }
    } else if (params.length == 1 && params[0] == Object.class && "equals".equals(name)) {
        return Boolean.valueOf(equals(args[0]));
    }
    throw new IllegalArgumentException("Invalid method for annotation type: " + method);
}

此时分析完毕

总结:当程序员调用getAnnotation,则会通过某种方法保存值,然后通过动态代理回调invoke,最终返回其值。

拓展 待研究(值的传递过程)

InvocationHandlerelements是如何从native传递上来的?

读者有能力的话可以再分析native层的原理,笔者能力有限,未能研究透彻。下面列出笔者的研究思路,抛砖引玉。

重点在于值是如何传递的

dex字节码

研究两者字节码运行的区别,应该可以得出答案

上述test属性最终生成一下dex字节码

.field private test:I
    .annotation runtime Lcom/hbsd/myannotation/MyAnnotation;
        value = "zjm"
    .end annotation
.end field

普通属性的dex字节码如下

.field private value:I

native层函数

native的调用过程捋清楚,getAnnotationNative最终会调用createAnnotation,跟踪createAnnotation的参数来源,也可得出结论。

调用栈第二函数createAnnotation的在well_known_classes.cc中进行jni映射

well_known_classes.cc

libcore_reflect_AnnotationFactory_createAnnotation = CacheMethod(env, libcore_reflect_AnnotationFactory, true, "createAnnotation", "(Ljava/lang/Class;[Llibcore/reflect/AnnotationMember;)Ljava/lang/annotation/Annotation;");

调用栈第三个函数getAnnotationNative的在java_lang_reflect_Field.cc中进行jni映射

FAST_NATIVE_METHOD(Field, getAnnotationNative,
                   "(Ljava/lang/Class;)Ljava/lang/annotation/Annotation;")

java_lang_reflect_Field.cc#Field_getAnnotationNative

static jobject Field_getAnnotationNative(JNIEnv* env, jobject javaField, jclass annotationType) {
    ScopedFastNativeObjectAccess soa(env);
    StackHandleScope<1> hs(soa.Self());
    ArtField* field = soa.Decode<mirror::Field>(javaField)->GetArtField();
    if (field->GetDeclaringClass()->IsProxyClass()) {
        return nullptr;
    }
    Handle<mirror::Class> klass(hs.NewHandle(soa.Decode<mirror::Class>(annotationType)));
    return soa.AddLocalReference<jobject>(annotations::GetAnnotationForField(field, klass));
}

读者有研究结果可私信我哟,谢谢好哥哥们

原 创 不 易 , 还 希 望 各 位 大 佬 支 持 一 下 \textcolor{blue}{原创不易,还希望各位大佬支持一下}

👍 点 赞 , 你 的 认 可 是 我 创 作 的 动 力 ! \textcolor{green}{点赞,你的认可是我创作的动力!}

⭐️ 收 藏 , 你 的 青 睐 是 我 努 力 的 方 向 ! \textcolor{green}{收藏,你的青睐是我努力的方向!}

✏️ 评 论 , 你 的 意 见 是 我 进 步 的 财 富 ! \textcolor{green}{评论,你的意见是我进步的财富!}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值