注解反射和动态代理

注解

注解的定义

注解的作用或者意义是什么?

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

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

自定义注解

使用@interface自定义注解

\x7e~~ Java
@Retention(RetentionPolicy.ROUTIME) //保留时
@Target({ElementType.TYPE}) //作用目标
public @interface Lance {
int a();
String b();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114

### 元注解

在定义注解时,注解类也能够使用其他的注解声明。在JDK1.5中提供了**用来对注解类进行注解的注解类,我们称之为meta-annotation(元注解)**。

声明的注解允许作用于哪些节点使用**`@Target`**声明:

* `TYPE`
* `FIELD`
* `METHOD`
* `PARAMETER`
* `CONSTRUCTOR`
* `LOCAL_VARIABLE`
* `ANNOTATION_TYPE`
* `PACKAGE`
* `TYPE_PARAMETER`
* `TYPE_USE`
* `MODULE`
* `RECORD_COMPONENT`

保留级别有**`@Retention`**声明。其中保留级别如下

* `RetentionPolicy.SOURCE`

标记的注解仅保留在源级别,并被编译器忽略

* `RetentionPolicy.CLASS`

标记的注解在编译时由编译器保留,但Java虚拟机(JVM)会忽略

* `Retention.RUNTIME`

标记的注解由JVM保留,因此运行时环境可以使用它

SOURCE\<CLASS\<ROUTIME, 即CLASS包含了SOURCE, ROUTIME包含SOURCE、CLASS>

**注意:**

**CLASS级别的注解保留到class,会被jvm抛弃,但在Android中运行的是dex、class级别注解会被抛弃,runtime注解也和Java一样,被虚拟机识别解析**



实际应用场景



### 注解的应用场景

根据注解的保留级别不同,对注解的使用自然存在不同场景。**由注解的三个不同保留级别可知,注解作用于:源码、字节码、运行时,以下是一些案例**

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

###### 限制参数取值范围

使用枚举的缺点:每个值都会编译成一个对象(对象占用内存运算:对象头 + 实际数据 + 对齐),更耗内存

自定义注解:又可以节省内存,又可以限制入参





## APT注解处理器

(annotation processor tools)注解处理器

**注意:**

* 常用的第三方库中应用:glide arouter butterknife ann hilt...

* `-processor <class1>[,<class2>,<class3>...]`

要运行的注释处理程序的名称; 绕过默认的搜索进程

* `javac -procerssor apt.jar xxx.java`

apt其实就可以看成javac的一个插件

* `-processor <class1>[,<class2>,<class3>...]`

要运行的注释处理程序的名称; 绕过默认的搜索进程

* apt:不会打包进入APK,只会在编译时参与编译

##### 自己实现APT

创建模块

![image-20231104140619388](https://blog-img-bitebyte.oss-cn-chengdu.aliyuncs.com/img/image-20231104140619388.png)

添加依赖

![image-20231104140701415](https://blog-img-bitebyte.oss-cn-chengdu.aliyuncs.com/img/image-20231104140701415.png)



注意:创建的是Java模块,而不是Android模块,Android模块不能给Java模块依赖

###### 创建注解类

在annotation模块中创建注解:

![image-20231104140852713](https://blog-img-bitebyte.oss-cn-chengdu.aliyuncs.com/img/image-20231104140852713.png)

~~~ java
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface MyClass {

}
创建实现类

在compile模块下创建注解处理器的实现类

1
2
3
4
5
6
public class TestProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
}

process方法,由javac调用,写什么代码就做什么事情

注册
  • 在compile模块的main文件夹下创建:resource -> META-INF -> services -> javax.annotation.prrocessing.Processor中进行注册

  • 写上APT实现类

    1
    com.wuliner.compile.TestProcessor
配置允许APT处理的注解

以下两种方法选一种就可以了

  • 重写该实现类的getSuppotedAnnotationTypes方法
1
2
3
4
5
6
7
8
/**
* 允许此注解处理器处理的注解
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
  • 使用@SupportedAnnotationTypes()注解

    括号中写注解类的全类名,如@SupportedAnnotationTypes({"com.wuliner.annotation.MyClass"})

1
2
3
4
5
6
7
8
9
10
@SupportedAnnotationTypes({"com.wuliner.annotation.MyClass"})
public class CwProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
Messager messager = processingEnv.getMessager();
messager.printMessage(Diagnostic.Kind.NOTE, "=========>");
return false;
}
}
引入APT所在模块

build.gradle

  • Java中使用annotationProcessor,如annotationProcessor project('compiler')

  • kolin中使用kapt

注意:apt不会打包进入APK,只会在编译时参与编译

APT的使用
  1. 在类名前添加注解,如@MyClass

  2. 在Processor实现类中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    @SupportedAnnotationTypes({"com.wuliner.annotation.MyClass"})
    public class CwProcessor extends AbstractProcessor {

    /**
    * javac 调用此方法
    * @param set
    * @param roundEnvironment
    * @return
    */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
    //写什么代码就做什么事情
    //process:javac编译时的一个回调
    Messager messager = processingEnv.getMessager();
    //打印日志
    messager.printMessage(Diagnostic.Kind.NOTE, "=========>");
    return false;
    }

    /**
    * 允许此注解处理器处理的注解
    * @return
    */
    // @Override
    // public Set<String> getSupportedAnnotationTypes() {
    // return super.getSupportedAnnotationTypes();
    // }
    }

    注:

    使用Message打印日志:

    1
    2
    3
    Messager messager = processingEnv.getMessager();
    //打印日志
    messager.printMessage(Diagnostic.Kind.NOTE, "=========>");
    引入注解模块

    使用annotationProcessor引入注解处理器模块, 若使用kotlin,则使用kapt

    1
    annotationProcessor(project(":compile"))

    apt不会打包进入apk,只会在编译时参与编译

    在Build窗口中查看日志

    image-20230911091721939

    为什么APT中process方法会执行多次?

    Java将源码编译成class文件的过程:

    Java将源码编译为class文件的过程

    java编译过程:词法分析、语法分析、填充符号表、注解处理器处理注解、语义分析、解语法糖、生成字节码。

    注解处理器可以增删改抽象语法树的任意元素。执行到注解处理器,都会重新执行词法分析、语法分析、填充符号表步,直到注解处理器不再对语法树进行修改为止,每一次的循环过程都称为一次Round。

    process第一个参数set集合是要处理的注解集合,如果这个集合为null了,就不需要处理了。写代码就这样去做。

    1
    2
    3
    if(!set.isEmpty()){
    //…执行处理
    }

    或者通过roundEnvironment.processingOver()将round结束

字节码增强(插桩)

字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以动态地对运行中的程序做修改,也可以跟踪JVM运行中程序的状态。

此外,我们平时使用的动态代理、AOP也于字节码增强密切相关,它们实质上还是利用各种手段生成或修改符合规范的字节码文件。

综上所述,掌握字节码增强后可以高效的定位并快速修复一些棘手的问题(如线上性能问题,方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率

反射

反射实现findViewById

通过反射获取类的成员
1
2
3
4
5
public static void injectView(Activity activity) {
Class<? extends Activity> cls = acctivity.getClass();
//获得此类所有的成员
Filed[] declaredFields = cls.getDeclaredFields();
}

注意:

  • Filed

    获得自己+父类的成员(不包括private,只能是public)

  • DecleredFile

    只能获得自己的成员(不包括父类,所有作用域)

可以通过getSuperClass()获取父类的class文件

创建注解

创建注解用来标识,用于筛选

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

在需要标识的属性上使用注解

这一传入了TextView对应的id,用来实现findViewById的功能

1
2
@InjectView(R.id.tv)
private TextView tv;
筛选Field
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
 public class InjectUtils {
public static void injectView(Activity activity) {
Class<? extends Activity> cls = activity.getClass();

//获得此类所有的成员
Field[] declaredFields = cls.getDeclaredFields();
for (Field filed : declaredFields) {
// 判断属性是否被InjectView注解声明
if (filed.isAnnotationPresent(InjectView.class)) {
InjectView injectView = filed.getAnnotation(InjectView.class);
//获得了注解中设置的id
int id = injectView.value();
View view = activity.findViewById(id);
//反射设置 属性的值
filed.setAccessible(true); //设置访问权限,允许操作private的属性
try {
//反射赋值
filed.set(activity, view);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
}
  • 先通过FiledisAnnotationPresent()方法,传入自定义注解的class文件用来判断属性是否被InjectView注解声明

  • 再通过getAnnotation()方法,传入注解的class文件,获得对应的注解类

  • 再通过这个类获取对应的属性,获得注解中设置的id后,获取对应的View

  • 然后通过反射设置属性的值

    先用filed.setAccessible(true)设置访问权限,允许操作private的属性

    通过filed.set(activity, view),将TextView值设置为对应的View

调用方法
image-20231118140800129

通过反射获取泛型的真实类型

Type类

  • TypeVariable

    泛型类型变量。可以泛型上下限等信息

  • ParameterType

    具体的泛型类型,可以获得元数据中泛型签名类型(泛型真实类型)

  • GenericArrayType

    当需要描述的类型是泛型类的数据时,比如List[],Map[],此接口会作为Type的实现

  • WildcardType

    通配符泛型,获得上下限信息

1
2
Type type = new TypeRefrence<Response<Data>>().getType(); //不行正常运行
Type type = new TypeRefrence<Response<Data>>(){}.getType(); //添加{}后,可以

有花括号,代表是匿名内部类,没有添加的话就代表一个对象