@TryLoveCatch
2023-03-07T08:18:59.000000Z
字数 12350
阅读 1096
Java知识体系
注解是一种元数据(描述数据的数据),注解本质是一个继承了Annotation 的特殊接口。
描述作用,不会直接生效,需要在编译前/运行时获取注解信息
@Target(ElementType.METHOD)@Retention(RetentionPolicy.SOURCE)public @interface Override {}public interface Override extends Annotation{}
元注解是修饰其他注解的注解。
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface ContentView {//属性叫 value ,在使用时可以直接传参数即可,不必显式的指明键值对,是一种快捷方法int value();}
我们可以使用 default xxx 为注解的某个属性指定默认值,这样即使不指定某个属性,编译器也不会报错。这通常可以节约很多时间,比如这样:
@Retention(RetentionPolicy.SOURCE)@Target(ElementType.TYPE)public @interface Author {String name() default "shixinzhang";String date();}
注解处理器分为两种:
运行时注解需要使用 注解 + 反射 ,非常简单。
我们先自定义一个 ContentView 注解,表示当前布局对应的 layout 文件:
@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface ContentView {//属性叫 value ,在使用时可以直接传参数即可,不必显式的指明键值对,是一种快捷方法int value() ;}
然后用它修饰一个 Activity:
@ContentView(R.layout.activity_annotation)public class AnnotationTestActivity extends BaseActivity {@Overrideprotected void onCreate(@Nullable final Bundle savedInstanceState) {super.onCreate(savedInstanceState); //调用父类的 onCreate}}
在 BaseActivity 中反射获取当前类使用的注解,拿到注解的值,就可以直接设置布局了:
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);annotationProcess();}//读取注解,进行处理private void annotationProcess() {Class c = this.getClass();//遍历所有子类for (; c != Context.class; c = c.getSuperclass()) {//找到使用 ContentView 注解的类ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);if (annotation != null) {try { //有可能出错的地方都要 try-catch//获取 注解中的属性值,为 Activity 设置布局this.setContentView(annotation.value());} catch (RuntimeException e) {e.printStackTrace();}return;}}}
这样就简单实现了运行时根据注解动态设置布局的功能。
使用编译时注解简单实现类似 ButterKnife 的效果
处理编译时注解需要使用 APT。
APT 即 Annotation Processing Tool,注解处理工具,它可以在编译时检测源代码文件,找到符合条件的注解修饰的内容,然后进行相应的处理。
我们在使用 ButterKnife,gradle 依赖中的 apt 就是指定在编译时调用它们的注解处理器:
compile "com.jakewharton:butterknife:$rootProject.butterknifeVersion"apt "com.jakewharton:butterknife-compiler:$rootProject.butterknifeVersion"
编译时注解的使用一般分为三步:
这里,我们写一个类似 ButterKnife 使用注解实现 findViewById 的 demo。
大概思路就是这样子:
注解处理器所在的 module 必须是 Java Library,因为要用到特有的 javax;
注解处理器需要依赖 注解 module,所以注解所在的 module 也要是 Java Library;
运行时绑定的类要操作 Activity 或者 View,所以需要为 Android Library。
New 一个 Module,选择为 Java library,我们起名为 ioc-annotation
@Retention(RetentionPolicy.CLASS)@Target(ElementType.FIELD)public @interface BindView {int value();}
编译时注解的 Retention 为 RetentionPolicy.CLASS,即只在编译时保留。
修饰目标为 ElementType.FIELD,即成员变量。
我们需要在 Activity 中调用一个绑定的方法,它的作用调用生成类方法,来完成findviewbyid的过程。类似 ButterKnife:
@Overrideprotected void onCreate(@Nullable final Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_annotation);ViewBinder.bind(this);}
New 一个 Module,选择为 Android library,我们起名为 ioc,创建 ViewBinder
*** Description:* <br> 从生成类中为当前 Activity/View 中的 View findViewById*/public class ViewBinder {private static final String SUFFIX = "$$ViewInjector";//Activity 中调用的方法public static void bind(Activity activity) {bind(activity, activity);}/*** 1.寻找对应的代理类* 2.调用接口提供的绑定方法** @param host* @param root*/@SuppressWarnings("unchecked")private static void bind(final Object host, final Object root) {if (host == null || root == null) {return;}Class<?> aClass = host.getClass();String proxyClassFullName = aClass.getName() + SUFFIX; //拼接生成类的名称try {Class<?> proxyClass = Class.forName(proxyClassFullName);ViewInjector viewInjector = (ViewInjector) proxyClass.newInstance();if (viewInjector != null) {viewInjector.inject(host, root);}} catch (ClassNotFoundException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();}}}
MyActivity$$ViewInjector,这个就是我们约定好的生产的类名。
ViewInjector,是一个接口,主要用来反射调用。
public interface ViewInjector<T> {void inject(T t, Object source);}
注解处理器的作用是读取注解、生成代码,按照我们约定好的类名来生成类,并且包含findViewById的代码。
具体的参考链接,这里就不赘述了。
public class AnnotationTestActivity extends BaseActivity {@BindView(R.id.tv_content)public TextView mTextView;@BindView(R.id.tv_bottom_content)public TextView mBottomTextView;@Overrideprotected void onCreate(@Nullable final Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_annotation);ViewBinder.bind(this);}//...}
点击 Build -> Rebuild Project,然后在 app -> build -> generated -> source -> apt -> flavor名字 -> 使用注解的包名下,看到生成类。
自定义一个运行时注解
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)public @interface HelloAnnotation {String say() default "Hi";}
然后在Main函数中解析注解
@HelloAnnotation(say = "Do it!")public class TestMain {public static void main(String[] args) {HelloAnnotation annotation = TestMain.class.getAnnotation(HelloAnnotation.class);//获取TestMain类上的注解对象System.out.println(annotation.say());//调用注解对象的say方法,并打印到控制台}}
运行程序,输出结果如下:
Do it!
我们来看一下HelloAnnotation的字节码
$ javap -verbose HelloAnnotationWarning: Binary file HelloAnnotation contains com.kevin.java.annotation.runtimeAnnotation.HelloAnnotationClassfile /home/kevin/Workspace/IdeaProjects/JavaLearn/out/production/JavaLearn/com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation.classLast modified Aug 6, 2016; size 496 bytesMD5 checksum a6c87f863669f6ab9050ffa310160ea5Compiled from "HelloAnnotation.java"public interface com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation extends java.lang.annotation.Annotationminor version: 0major version: 52flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATIONConstant pool:#1 = Class #18 // com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation#2 = Class #19 // java/lang/Object#3 = Class #20 // java/lang/annotation/Annotation#4 = Utf8 say#5 = Utf8 ()Ljava/lang/String;#6 = Utf8 AnnotationDefault#7 = Utf8 Hi#8 = Utf8 SourceFile#9 = Utf8 HelloAnnotation.java#10 = Utf8 RuntimeVisibleAnnotations#11 = Utf8 Ljava/lang/annotation/Target;#12 = Utf8 value#13 = Utf8 Ljava/lang/annotation/ElementType;#14 = Utf8 TYPE#15 = Utf8 Ljava/lang/annotation/Retention;#16 = Utf8 Ljava/lang/annotation/RetentionPolicy;#17 = Utf8 RUNTIME#18 = Utf8 com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation#19 = Utf8 java/lang/Object#20 = Utf8 java/lang/annotation/Annotation{public abstract java.lang.String say();descriptor: ()Ljava/lang/String;flags: ACC_PUBLIC, ACC_ABSTRACTAnnotationDefault:default_value: s#7}SourceFile: "HelloAnnotation.java"RuntimeVisibleAnnotations:0: #11(#12=[e#13.#14])1: #15(#12=e#16.#17)
看第7行,很明显,HelloAnnotation就是继承了Annotation的接口。
再看第10行,flag字段中,我们可以看到,有个ACC_ANNOTATION标记,说明是一个注解,所以
注解本质是一个继承了Annotation的特殊接口。
看一下Annotation接口声明
package java.lang.annotation;public interface Annotation {boolean equals(Object var1);int hashCode();String toString();Class<? extends Annotation> annotationType();}
我们运行TestMain,并打断点,看一下HelloAnnotation具体是什么类的对象:

可以看到HelloAnnotation注解的实例是jvm生成的动态代理类的对象。
我们来看一下这个生成的代理类:
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by Fernflower decompiler)//package com.sun.proxy;import com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;import java.lang.reflect.UndeclaredThrowableException;public final class $Proxy1 extends Proxy implements HelloAnnotation {private static Method m1;private static Method m2;private static Method m4;private static Method m3;private static Method m0;public $Proxy1(InvocationHandler var1) throws {super(var1);}public final boolean equals(Object var1) throws {try {return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();} catch (RuntimeException | Error var3) {throw var3;} catch (Throwable var4) {throw new UndeclaredThrowableException(var4);}}public final String toString() throws {try {return (String)super.h.invoke(this, m2, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final Class annotationType() throws {try {return (Class)super.h.invoke(this, m4, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final String say() throws {try {return (String)super.h.invoke(this, m3, (Object[])null);} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}public final int hashCode() throws {try {return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();} catch (RuntimeException | Error var2) {throw var2;} catch (Throwable var3) {throw new UndeclaredThrowableException(var3);}}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.kevin.java.annotation.runtimeAnnotation.HelloAnnotation").getMethod("annotationType", new Class[0]);m3 = Class.forName("com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation").getMethod("say", new Class[0]);m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);} catch (NoSuchMethodException var2) {throw new NoSuchMethodError(var2.getMessage());} catch (ClassNotFoundException var3) {throw new NoClassDefFoundError(var3.getMessage());}}}
从第14行我们可以看到,我们自定义的注解HelloAnnotation是一个接口,而$Proxy1这个Java生成的动态代理类就是它的实现类。
- 现在我们知道了HelloAnnotation注解(接口)是一个继承了Annotation接口的特殊接口。
- 而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1,该类就是HelloAnnotation注解(接口)的具体实现类。
既然是Proxy,那么就应该有一个InvocationHandler来处理代理,通过断点,我们可以知道这个对应的就是AnnotationInvocationHandler:
class AnnotationInvocationHandler implements InvocationHandler, Serializable {private final Class<? extends Annotation> type;private final Map<String, Object> memberValues;private transient volatile Method[] memberMethods = null;....AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {this.type = type;this.memberValues = memberValues;}public Object invoke(Object proxy, Method method, Object[] args) {String member = method.getName();Class<?>[] paramTypes = method.getParameterTypes();// Handle Object and Annotation methodsif (member.equals("equals") && paramTypes.length == 1 &¶mTypes[0] == Object.class)return equalsImpl(args[0]);assert paramTypes.length == 0;if (member.equals("toString"))return toStringImpl();if (member.equals("hashCode"))return hashCodeImpl();if (member.equals("annotationType"))return type;// Handle annotation member accessorsObject result = memberValues.get(member);if (result == null)throw new IncompleteAnnotationException(type, member);if (result instanceof ExceptionProxy)throw ((ExceptionProxy) result).generateException();if (result.getClass().isArray() && Array.getLength(result) != 0)result = cloneArray(result);return result;}......}
我们一眼就可以看到一个有意思的名字: memberValues,这是一个Map,而断点中可以看到这是一个 LinkedHashMap,key为注解的属性名称,value即为注解的属性值。
- 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。
- 而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1。
- 通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。
- 该方法会从memberValues这个Map中索引出对应的值。
- 而memberValues的来源是Java常量池。
简单来说,注解就是接口+Map,然后通过动态代理将他们组合起来。
apt不再神秘
Android APT 实例讲解
Android开源系列-组件化框架Arouter-(三)APT技术详解
KotlinPoet简介

apt怎么参与到javac的编译过程的?
apt与javac约定在META-INF/services/javax.annotation.processing.Processor文件中注册apt插件。
这个逻辑,让我感觉APT是基于SPI实现的
apt不能修改已有代码的执行流程,如果要实现类似“日志打点”等在原代码的基础上插入一些功能,一般需要通过aspectj来实现。这类功能是在编译后期(javac之后)修改字节码文件来实现。
implementation "com.squareup:javapoet:1.11.1"
compile 'com.squareup:kotlinpoet:1.12.0'
Java 进阶巩固:什么是注解以及运行时注解的使用
使用编译时注解简单实现类似 ButterKnife 的效果
Android 如何编写基于编译时注解的项目