文章目录
注解的定义
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
类,使用上文声明的注解进行注解,上文注解的声明中的元注解target
为TYPE
,则只能注解类。
@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
提供的功能),APT(javac
提供的功能),下文对其进行讲解。
上述类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
}
CLASS
和RUNTIME
可以使注解停留在.class
上
停留在.class
上意味着后续虚拟机在执行时可以感知到,如果是CLASS
级别,虚拟机会进行忽略,看得见但是装看不见,RUNTIME
级别虚拟机则可真正看见。CLASS
级别的用途是ASM字节码插桩,既然.class
上有此注解信息,插桩也是扫描.class
文件,扫描到存在此注解则可对注解处进行操作,RUNTIME
时则需结合反射使用。
上述类使用CLASS
和RUNTIME
产生的字节码:
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
}
用处总结
级别 | 技术 | 说明 |
---|---|---|
SOURCE | APT,源码检查 | APT:在编译期能够获取注解与注解声明的类包括类中所有成员信息,一般用于生成额外的辅助类。 源码检查:规定属性范围 |
CLASS | 字节码增强 | 在编译出Class后,通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。 |
RUNTIME | 反射 | 在程序运行期间,通过反射技术动态获取注解与其元素,从而完成不同的逻辑判定。 |
SOURCE
source
有两种用处APT
,源码检查
APT
APT
即为Annotation Processing Tool
,javac
的一个工具,中文意思为编译时注解处理器。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_main
和R.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);
两者优劣
在枚举中VALUE1
和VALUE2
被声明成对象
//枚举字节码片段
// 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
,给Proxy
的newProxyInstance
打上断点。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
,最终返回其值。
拓展 待研究(值的传递过程)
InvocationHandler
的elements
是如何从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}{评论,你的意见是我进步的财富!} 评论,你的意见是我进步的财富!