[关闭]
@TryLoveCatch 2023-03-07T08:18:59.000000Z 字数 12350 阅读 665

Java知识体系之注解

Java知识体系


注解

注解是一种元数据(描述数据的数据),注解本质是一个继承了Annotation 的特殊接口。
描述作用,不会直接生效,需要在编译前/运行时获取注解信息

  1. @Target(ElementType.METHOD)
  2. @Retention(RetentionPolicy.SOURCE)
  3. public @interface Override {
  4. }
  5. public interface Override extends Annotation{
  6. }

元注解

元注解是修饰其他注解的注解。

自定义注解

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.TYPE)
  3. public @interface ContentView {
  4. //属性叫 value ,在使用时可以直接传参数即可,不必显式的指明键值对,是一种快捷方法
  5. int value();
  6. }

我们可以使用 default xxx 为注解的某个属性指定默认值,这样即使不指定某个属性,编译器也不会报错。这通常可以节约很多时间,比如这样:

  1. @Retention(RetentionPolicy.SOURCE)
  2. @Target(ElementType.TYPE)
  3. public @interface Author {
  4. String name() default "shixinzhang";
  5. String date();
  6. }

自定义注解规则

注解的作用

注解的用法

  1. 自定义注解:规定处理对象类型,保存阶段,以及包含的值
  2. 使用注解修饰我们想要的处理的类、方法或者属性
  3. 读取注解,使用注解处理器处理

注解处理器

注解处理器分为两种:

运行时处理器

Java 进阶巩固:什么是注解以及运行时注解的使用

运行时注解需要使用 注解 + 反射 ,非常简单。

我们先自定义一个 ContentView 注解,表示当前布局对应的 layout 文件:

  1. @Retention(RetentionPolicy.RUNTIME)
  2. @Target(ElementType.TYPE)
  3. public @interface ContentView {
  4. //属性叫 value ,在使用时可以直接传参数即可,不必显式的指明键值对,是一种快捷方法
  5. int value() ;
  6. }

然后用它修饰一个 Activity:

  1. @ContentView(R.layout.activity_annotation)
  2. public class AnnotationTestActivity extends BaseActivity {
  3. @Override
  4. protected void onCreate(@Nullable final Bundle savedInstanceState) {
  5. super.onCreate(savedInstanceState); //调用父类的 onCreate
  6. }
  7. }

在 BaseActivity 中反射获取当前类使用的注解,拿到注解的值,就可以直接设置布局了:

  1. @Override
  2. protected void onCreate(@Nullable Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. annotationProcess();
  5. }
  6. //读取注解,进行处理
  7. private void annotationProcess() {
  8. Class c = this.getClass();
  9. //遍历所有子类
  10. for (; c != Context.class; c = c.getSuperclass()) {
  11. //找到使用 ContentView 注解的类
  12. ContentView annotation = (ContentView) c.getAnnotation(ContentView.class);
  13. if (annotation != null) {
  14. try { //有可能出错的地方都要 try-catch
  15. //获取 注解中的属性值,为 Activity 设置布局
  16. this.setContentView(annotation.value());
  17. } catch (RuntimeException e) {
  18. e.printStackTrace();
  19. }
  20. return;
  21. }
  22. }
  23. }

这样就简单实现了运行时根据注解动态设置布局的功能。

编译时处理器

使用编译时注解简单实现类似 ButterKnife 的效果
处理编译时注解需要使用 APT。
APT 即 Annotation Processing Tool,注解处理工具,它可以在编译时检测源代码文件,找到符合条件的注解修饰的内容,然后进行相应的处理。
我们在使用 ButterKnife,gradle 依赖中的 apt 就是指定在编译时调用它们的注解处理器:

  1. compile "com.jakewharton:butterknife:$rootProject.butterknifeVersion"
  2. apt "com.jakewharton:butterknife-compiler:$rootProject.butterknifeVersion"

编译时注解的使用一般分为三步:

  1. 创建注解
  2. 编译时使用注解处理器生成代码
  3. 运行时调用生成的代码

这里,我们写一个类似 ButterKnife 使用注解实现 findViewById 的 demo。
大概思路就是这样子:

  1. 先写一个注解,这个注解修饰一个成员变量,同时指定这个变量对应的 id
  2. 然后写个注解处理器,读取当前类的所有被注解修饰的成员对象和 id,生成对应的 findViewById 代码
  3. 最后写个运行时绑定的类,初始化当前类的成员

注解处理器所在的 module 必须是 Java Library,因为要用到特有的 javax;
注解处理器需要依赖 注解 module,所以注解所在的 module 也要是 Java Library;
运行时绑定的类要操作 Activity 或者 View,所以需要为 Android Library。

创建注解

New 一个 Module,选择为 Java library,我们起名为 ioc-annotation

  1. @Retention(RetentionPolicy.CLASS)
  2. @Target(ElementType.FIELD)
  3. public @interface BindView {
  4. int value();
  5. }

编译时注解的 Retention 为 RetentionPolicy.CLASS,即只在编译时保留。
修饰目标为 ElementType.FIELD,即成员变量。

创建运行时绑定的类

我们需要在 Activity 中调用一个绑定的方法,它的作用调用生成类方法,来完成findviewbyid的过程。类似 ButterKnife:

  1. @Override
  2. protected void onCreate(@Nullable final Bundle savedInstanceState) {
  3. super.onCreate(savedInstanceState);
  4. setContentView(R.layout.activity_annotation);
  5. ViewBinder.bind(this);
  6. }

New 一个 Module,选择为 Android library,我们起名为 ioc,创建 ViewBinder

  1. **
  2. * Description:
  3. * <br> 从生成类中为当前 Activity/View 中的 View findViewById
  4. */
  5. public class ViewBinder {
  6. private static final String SUFFIX = "$$ViewInjector";
  7. //Activity 中调用的方法
  8. public static void bind(Activity activity) {
  9. bind(activity, activity);
  10. }
  11. /**
  12. * 1.寻找对应的代理类
  13. * 2.调用接口提供的绑定方法
  14. *
  15. * @param host
  16. * @param root
  17. */
  18. @SuppressWarnings("unchecked")
  19. private static void bind(final Object host, final Object root) {
  20. if (host == null || root == null) {
  21. return;
  22. }
  23. Class<?> aClass = host.getClass();
  24. String proxyClassFullName = aClass.getName() + SUFFIX; //拼接生成类的名称
  25. try {
  26. Class<?> proxyClass = Class.forName(proxyClassFullName);
  27. ViewInjector viewInjector = (ViewInjector) proxyClass.newInstance();
  28. if (viewInjector != null) {
  29. viewInjector.inject(host, root);
  30. }
  31. } catch (ClassNotFoundException e) {
  32. e.printStackTrace();
  33. } catch (InstantiationException e) {
  34. e.printStackTrace();
  35. } catch (IllegalAccessException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. }

MyActivity$$ViewInjector,这个就是我们约定好的生产的类名。

ViewInjector,是一个接口,主要用来反射调用。

  1. public interface ViewInjector<T> {
  2. void inject(T t, Object source);
  3. }
创建注解处理器

注解处理器的作用是读取注解、生成代码,按照我们约定好的类名来生成类,并且包含findViewById的代码。
具体的参考链接,这里就不赘述了。

使用
  1. public class AnnotationTestActivity extends BaseActivity {
  2. @BindView(R.id.tv_content)
  3. public TextView mTextView;
  4. @BindView(R.id.tv_bottom_content)
  5. public TextView mBottomTextView;
  6. @Override
  7. protected void onCreate(@Nullable final Bundle savedInstanceState) {
  8. super.onCreate(savedInstanceState);
  9. setContentView(R.layout.activity_annotation);
  10. ViewBinder.bind(this);
  11. }
  12. //...
  13. }

点击 Build -> Rebuild Project,然后在 app -> build -> generated -> source -> apt -> flavor名字 -> 使用注解的包名下,看到生成类。

原理

Java注解(Annotation)原理详解

例子

自定义一个运行时注解

  1. @Target(ElementType.TYPE)
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface HelloAnnotation {
  4. String say() default "Hi";
  5. }

然后在Main函数中解析注解

  1. @HelloAnnotation(say = "Do it!")
  2. public class TestMain {
  3. public static void main(String[] args) {
  4. HelloAnnotation annotation = TestMain.class.getAnnotation(HelloAnnotation.class);//获取TestMain类上的注解对象
  5. System.out.println(annotation.say());//调用注解对象的say方法,并打印到控制台
  6. }
  7. }

运行程序,输出结果如下:

  1. Do it!

注解本质

我们来看一下HelloAnnotation的字节码

  1. $ javap -verbose HelloAnnotation
  2. Warning: Binary file HelloAnnotation contains com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation
  3. Classfile /home/kevin/Workspace/IdeaProjects/JavaLearn/out/production/JavaLearn/com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation.class
  4. Last modified Aug 6, 2016; size 496 bytes
  5. MD5 checksum a6c87f863669f6ab9050ffa310160ea5
  6. Compiled from "HelloAnnotation.java"
  7. public interface com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation extends java.lang.annotation.Annotation
  8. minor version: 0
  9. major version: 52
  10. flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION
  11. Constant pool:
  12. #1 = Class #18 // com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation
  13. #2 = Class #19 // java/lang/Object
  14. #3 = Class #20 // java/lang/annotation/Annotation
  15. #4 = Utf8 say
  16. #5 = Utf8 ()Ljava/lang/String;
  17. #6 = Utf8 AnnotationDefault
  18. #7 = Utf8 Hi
  19. #8 = Utf8 SourceFile
  20. #9 = Utf8 HelloAnnotation.java
  21. #10 = Utf8 RuntimeVisibleAnnotations
  22. #11 = Utf8 Ljava/lang/annotation/Target;
  23. #12 = Utf8 value
  24. #13 = Utf8 Ljava/lang/annotation/ElementType;
  25. #14 = Utf8 TYPE
  26. #15 = Utf8 Ljava/lang/annotation/Retention;
  27. #16 = Utf8 Ljava/lang/annotation/RetentionPolicy;
  28. #17 = Utf8 RUNTIME
  29. #18 = Utf8 com/kevin/java/annotation/runtimeAnnotation/HelloAnnotation
  30. #19 = Utf8 java/lang/Object
  31. #20 = Utf8 java/lang/annotation/Annotation
  32. {
  33. public abstract java.lang.String say();
  34. descriptor: ()Ljava/lang/String;
  35. flags: ACC_PUBLIC, ACC_ABSTRACT
  36. AnnotationDefault:
  37. default_value: s#7}
  38. SourceFile: "HelloAnnotation.java"
  39. RuntimeVisibleAnnotations:
  40. 0: #11(#12=[e#13.#14])
  41. 1: #15(#12=e#16.#17)

看第7行,很明显,HelloAnnotation就是继承了Annotation的接口。
再看第10行,flag字段中,我们可以看到,有个ACC_ANNOTATION标记,说明是一个注解,所以

注解本质是一个继承了Annotation的特殊接口。

看一下Annotation接口声明

  1. package java.lang.annotation;
  2. public interface Annotation {
  3. boolean equals(Object var1);
  4. int hashCode();
  5. String toString();
  6. Class<? extends Annotation> annotationType();
  7. }

代理

我们运行TestMain,并打断点,看一下HelloAnnotation具体是什么类的对象:

可以看到HelloAnnotation注解的实例是jvm生成的动态代理类的对象。
我们来看一下这个生成的代理类:

  1. //
  2. // Source code recreated from a .class file by IntelliJ IDEA
  3. // (powered by Fernflower decompiler)
  4. //
  5. package com.sun.proxy;
  6. import com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation;
  7. import java.lang.reflect.InvocationHandler;
  8. import java.lang.reflect.Method;
  9. import java.lang.reflect.Proxy;
  10. import java.lang.reflect.UndeclaredThrowableException;
  11. public final class $Proxy1 extends Proxy implements HelloAnnotation {
  12. private static Method m1;
  13. private static Method m2;
  14. private static Method m4;
  15. private static Method m3;
  16. private static Method m0;
  17. public $Proxy1(InvocationHandler var1) throws {
  18. super(var1);
  19. }
  20. public final boolean equals(Object var1) throws {
  21. try {
  22. return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue();
  23. } catch (RuntimeException | Error var3) {
  24. throw var3;
  25. } catch (Throwable var4) {
  26. throw new UndeclaredThrowableException(var4);
  27. }
  28. }
  29. public final String toString() throws {
  30. try {
  31. return (String)super.h.invoke(this, m2, (Object[])null);
  32. } catch (RuntimeException | Error var2) {
  33. throw var2;
  34. } catch (Throwable var3) {
  35. throw new UndeclaredThrowableException(var3);
  36. }
  37. }
  38. public final Class annotationType() throws {
  39. try {
  40. return (Class)super.h.invoke(this, m4, (Object[])null);
  41. } catch (RuntimeException | Error var2) {
  42. throw var2;
  43. } catch (Throwable var3) {
  44. throw new UndeclaredThrowableException(var3);
  45. }
  46. }
  47. public final String say() throws {
  48. try {
  49. return (String)super.h.invoke(this, m3, (Object[])null);
  50. } catch (RuntimeException | Error var2) {
  51. throw var2;
  52. } catch (Throwable var3) {
  53. throw new UndeclaredThrowableException(var3);
  54. }
  55. }
  56. public final int hashCode() throws {
  57. try {
  58. return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
  59. } catch (RuntimeException | Error var2) {
  60. throw var2;
  61. } catch (Throwable var3) {
  62. throw new UndeclaredThrowableException(var3);
  63. }
  64. }
  65. static {
  66. try {
  67. m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
  68. m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
  69. m4 = Class.forName("com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation").getMethod("annotationType", new Class[0]);
  70. m3 = Class.forName("com.kevin.java.annotation.runtimeAnnotation.HelloAnnotation").getMethod("say", new Class[0]);
  71. m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
  72. } catch (NoSuchMethodException var2) {
  73. throw new NoSuchMethodError(var2.getMessage());
  74. } catch (ClassNotFoundException var3) {
  75. throw new NoClassDefFoundError(var3.getMessage());
  76. }
  77. }
  78. }

从第14行我们可以看到,我们自定义的注解HelloAnnotation是一个接口,而$Proxy1这个Java生成的动态代理类就是它的实现类。

  • 现在我们知道了HelloAnnotation注解(接口)是一个继承了Annotation接口的特殊接口。
  • 而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1,该类就是HelloAnnotation注解(接口)的具体实现类。

AnnotationInvocationHandler

既然是Proxy,那么就应该有一个InvocationHandler来处理代理,通过断点,我们可以知道这个对应的就是AnnotationInvocationHandler:

  1. class AnnotationInvocationHandler implements InvocationHandler, Serializable {
  2. private final Class<? extends Annotation> type;
  3. private final Map<String, Object> memberValues;
  4. private transient volatile Method[] memberMethods = null;
  5. ....
  6. AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
  7. this.type = type;
  8. this.memberValues = memberValues;
  9. }
  10. public Object invoke(Object proxy, Method method, Object[] args) {
  11. String member = method.getName();
  12. Class<?>[] paramTypes = method.getParameterTypes();
  13. // Handle Object and Annotation methods
  14. if (member.equals("equals") && paramTypes.length == 1 &&
  15. paramTypes[0] == Object.class)
  16. return equalsImpl(args[0]);
  17. assert paramTypes.length == 0;
  18. if (member.equals("toString"))
  19. return toStringImpl();
  20. if (member.equals("hashCode"))
  21. return hashCodeImpl();
  22. if (member.equals("annotationType"))
  23. return type;
  24. // Handle annotation member accessors
  25. Object result = memberValues.get(member);
  26. if (result == null)
  27. throw new IncompleteAnnotationException(type, member);
  28. if (result instanceof ExceptionProxy)
  29. throw ((ExceptionProxy) result).generateException();
  30. if (result.getClass().isArray() && Array.getLength(result) != 0)
  31. result = cloneArray(result);
  32. return result;
  33. }
  34. ...
  35. ...
  36. }

我们一眼就可以看到一个有意思的名字: memberValues,这是一个Map,而断点中可以看到这是一个 LinkedHashMap,key为注解的属性名称,value即为注解的属性值。

  • 注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。
  • 而我们通过反射获取注解时,返回的是Java运行时生成的动态代理对象$Proxy1。
  • 通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。
  • 该方法会从memberValues这个Map中索引出对应的值。
  • 而memberValues的来源是Java常量池。

简单来说,注解就是接口+Map,然后通过动态代理将他们组合起来。

APT

apt不再神秘
Android APT 实例讲解
Android开源系列-组件化框架Arouter-(三)APT技术详解
KotlinPoet简介


apt怎么参与到javac的编译过程的?
apt与javac约定在META-INF/services/javax.annotation.processing.Processor文件中注册apt插件。

这个逻辑,让我感觉APT是基于SPI实现的

apt不能修改已有代码的执行流程,如果要实现类似“日志打点”等在原代码的基础上插入一些功能,一般需要通过aspectj来实现。这类功能是在编译后期(javac之后)修改字节码文件来实现。

  1. implementation "com.squareup:javapoet:1.11.1"
  1. compile 'com.squareup:kotlinpoet:1.12.0'

参考

Java 进阶巩固:什么是注解以及运行时注解的使用
使用编译时注解简单实现类似 ButterKnife 的效果
Android 如何编写基于编译时注解的项目

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注