注解与反射以及动态代理
注解
注解的定义
Annotation(注解)就是Java提供了一种元程序中的元素关联任何信息和着任何元数据(metadata)的途径和方法。
基本规则:Annotation不能影响程序代码的执行,无论增加、删除 Annotation,代码都始终如一的执行。
什么是元数据
- 元数据以标签的形式存在于Java代码中。
- 元数据描述的信息是类型安全的,即元数据内部的字段都是有明确类型的。
- 元数据需要编译器之外的工具额外的处理用来生成其它的程序部件。
- 元数据可以只存在于Java源代码级别,也可以存在于编译之后的Class文件内部。
注解的原理
当Java源代码被编译时,编译器的一个插件annotation处理器则会处理这些annotation。处理器可以产生报告信息,或者创建附加的Java源文件或资源。如果annotation本身被加上了RententionPolicy的运行时类,则Java编译器则会将annotation的元数据存储到class文件中。然后,Java虚拟机或其他的程序可以查找这些元数据并做相应的处理。当然除了annotation处理器可以处理annotation外,我们也可以使用反射自己来处理annotation
注解的分类
系统内置标准注解
- @Override(标记注解类型)
- @Deprecated(标志已过时),
- @SuppressWarnings(抑制编译器警告)
元注解
在定义注解时,注解类也能够使用其他的注解声明。对注解类型进行注解的注解类,我们称之为 meta
annotation(元注解)。一般的,我们在定义自定义注解时,需要指定的元注解有两个 :
另外还有**@Documented** 与 @Inherited 元注解,前者用于被javadoc工具提取成文档,后者表示允许子类继承父类中定义的注解。
@Target
注解标记另一个注解,以限制可以应用注解的 Java 元素类型。目标注解指定以下元素类型之一作为其值:
ElementType.ANNOTATION_TYPE 可以应用于注解类型。
ElementType.CONSTRUCTOR 可以应用于构造函数。
ElementType.FIELD 可以应用于字段或属性。
ElementType.LOCAL_VARIABLE 可以应用于局部变量。
ElementType.METHOD 可以应用于方法级注解。
ElementType.PACKAGE 可以应用于包声明。
ElementType.PARAMETER 可以应用于方法的参数。
ElementType.TYPE 可以应用于类的任何元素。
@Retention
注解指定标记注解的存储方式:
RetentionPolicy.SOURCE - 标记的注解仅保留在源级别中,并被编译器忽略。
RetentionPolicy.CLASS - 标记的注解在编译时由编译器保留,但 Java 虚拟机(JVM)会忽略。
RetentionPolicy.RUNTIME - 标记的注解由 JVM 保留,因此运行时环境可以使用它。
@Retention 三个值中 SOURCE < CLASS < RUNTIME,即CLASS包含了SOURCE,RUNTIME包含SOURCE、CLASS。下文会介绍他们不同的应用场景。
Android support annotations
- Nullness注解
- Resource Type 注解(@StringRes,@ColorInt等)
- Threading 注解(@UiThread UI线程,
,@MainThread 主线程,@WorkerThread 子线程,
@BinderThread 绑定线程
) - Overriding Methods 注解: @CallSuper
注解的应用场景
按照**@Retention** 元注解定义的注解存储方式,注解可以被在三种场景下使用:
SOURCE
RetentionPolicy.SOURCE ,作用于源码级别的注解,可提供给IDE语法检查、APT等场景使用。
IDE语法检查
在Android开发中, support-annotations 与 androidx.annotation) 中均有提供 @IntDef 注解,此注解的定义如下:
1 | //源码级别注解 |
Java中Enum(枚举)的实质是特殊单例的静态成员变量,在运行期所有枚举类作为单例,全部加载到内存中。比常量多5到10倍的内存占用。
APT技术
APT全称为:”Anotation Processor Tools”,意为注解处理器。顾名思义,其用于处理注解。编写好的Java源文件,需要经过 javac 的编译,翻译为虚拟机能够加载解析的字节码Class文件。注解处理器是 javac 自带的一个工具,用来在编译时期扫描处理注解信息。你可以为某些注解注册自己的注解处理器。 注册的注解处理器由 javac调起,并将注解信息传递给注解处理器进行处理。
注解处理器是对注解应用最为广泛的场景。在Glide、EventBus3、Butterknifer、Tinker、ARouter等等常用框架中都有注解处理器的身影。但是你可能会发现,这些框架中对注解的定义并不是 SOURCE 级别,更多的是 CLASS 级别,别忘了:CLASS包含了SOURCE,RUNTIME包含SOURCE、CLASS。
注解处理器的创建步骤
Android Studio创建一个java library
自定义一个注解(Annotation),用于存储元数据
1
2
3
4
public Print {
}创建一个自定义Annotation Processor继承于AbstractProcessor
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
public class MyProcessor extends AbstractProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
public synchronized void init(ProcessingEnvironment processingEnv) {
System.out.println("Hello APT");
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Hello APT");
super.init(processingEnv);
}
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> strings = new HashSet<>();
strings.add(Print.class.getCanonicalName());
return super.getSupportedAnnotationTypes();
}
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}@AutoService(MyProcessor.class) :向javac注册我们这个自定义的注解处理器,这样,在javac编译时,才会调用到我们这个自定义的注解处理器方法。
AutoService这里主要是用来生成
META-INF/services/javax.annotation.processing.Processor文件的。如果不加上这个注解,那么,你需要自己进行手动配置进行注册,具体手动注册方法如下:
创建一个META-INF/services/javax.annotation.processing.Processor文件, 其内容是一系列的自定义注解处理器完整有效类名集合:1
com.lbz.apt_processor.MyProcessor
- 编写process方法
CLASS
字节码增强,在编译出Class后,通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
RUNTIME
在编译出Class后,通过修改Class数据以实现修改代码逻辑目的。对于是否需要修改的区分或者修改为不同逻辑的判断可以使用注解。
反射
什么是反射
反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的,并且能够获得此类的引用。于是我们直接对这个类进行实例化,之后使用这个类对象进行操作。
反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。这时候,我们使用 JDK 提供的反射 API 进行反射调用。反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。是Java被视为动态语言的关键。
Java反射机制主要提供了以下功能:
在运行时构造任意一个类的对象
在运行时获取或者修改任意一个类所具有的成员变量和方法
在运行时调用任意一个对象的方法(属性)
获得 Class 对象
1 | //获取Class的方式一 |
判断是否为某个类的实例
一般地,我们用 instanceof 关键字来判断是否为某个类的实例。同时我们也可以借助反射中 Class 对象的isInstance() 方法来判断是否为某个类的实例,它是一个 native 方法:
1 | public native boolean isInstance(Object obj); |
判断是否为某个类的类型
1 | public boolean isAssignableFrom(Class<?> cls) |
创建实例
通过反射来生成对象主要有两种方式。
使用Class对象的newInstance()方法来创建Class对象对应类的实例。
1
2Class<?> c = String.class;
Object str = c.newInstance();
- 先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。
1 | //获取String所对应的Class对象 |
获取构造器信息
得到构造器的方法
1 | Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的public构造函数(包括父类) |
获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:
1 | public T newInstance(Object ... initargs) |
获取类的成员变量(字段)信息
获得字段信息的方法
1 | Field getField(String name) -- 获得命名的公共字段 |
调用方法
获得方法信息的方法
1 | Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法 Method[] getMethods() -- 获得类的所有公共方法 |
当我们从类中获取了一个方法后,我们就可以用 invoke() 方法来调用这个方法。 invoke 方法的原型为:
1 | public Object invoke(Object obj, Object... args) |
利用反射创建数组
数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference 其中的Array类为
java.lang.reflflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是:
1 | public static Object newInstance(Class<?> componentType, int length); |
反射获取泛型真实类型
当我们对一个泛型类进行反射时,需要的到泛型中的真实数据类型,来完成如json反序列化的操作。此时需要通过 Type 体系来完成。 Type 接口包含了一个实现类(Class)和四个实现接口,他们分别是:
- TypeVariable
泛型类型变量。可以泛型上下限等信息;
- ParameterizedType
具体的泛型类型,可以获得元数据中泛型签名类型(泛型真实类型)
- GenericArrayType
当需要描述的类型是泛型类的数组时,比如List[],Map[],此接口会作为Type的实现。
- WildcardType
通配符泛型,获得上下限信息;
TypeVariable
1 | class Main<K extends Comparable & Serializable, V> { |
ParameterizedType
1 | class Main { |
GenericArrayType
1 | class Main { |
WildcardType
1 | class Main { |
Gson反序列化
在进行GSON反序列化时,存在泛型时,可以借助 TypeToken 获取Type以完成泛型的反序列化。但是为什么TypeToken 要被定义为抽象类呢?
因为只有定义为抽象类或者接口,这样在使用时,需要创建对应的实现类,此时确定泛型类型,编译才能够将泛型signature信息记录到Class元数据中。
动态代理
参考https://juejin.cn/post/6974018412158664734
静态代理
定义
代理模式给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。通俗的来讲代理模式就是我们生活中常见的中介。
目的
- 通过引入代理对象的方式来间接访问目标对象,防止直接访问目标对象给系统带来的不必要复杂性;
- 通过代理对象对访问进行控制;
图解
代理模式一般会有三个角色:
抽象角色:指代理角色和真实角色对外提供的公共方法,一般为一个接口
真实角色:需要实现抽象角色接口,定义了真实角色所要实现的业务逻辑,以便供代理角色调用。也就是真正的业务逻辑在此。
代理角色:需要实现抽象角色接口,是真实角色的代理,通过真实角色的业务逻辑方法来实现抽象方法,并可以附加自己的操作。将统一的流程控制都放到代理角色中处理!
静态代理缺点
- 1、重复性: 需要代理的业务或方法越多,重复的模板代码越多;
- 2、脆弱性: 一旦改动基础接口,代理类也需要同步修改(因为代理类也实现了基础接口)。
动态代理
在运行时再创建代理类和其实例,因此显然效率更低。要完成这个场景,需要在运行期动态创建一个Class。JDK提供了 Proxy 来完成这件事情。基本使用如下:
- 创建接口,定义目标列要完成的功能。
- 创建目标类实现接口。
- 创建InvocationHandler接口的实现类,在invoke方法中完成代理类的功能。
- 调用目标方法
- 增强功能
- 使用Proxy类的静态方法,创建代理对象,并把返回值转为接口类型。
1 | import java.lang.reflect.InvocationHandler; |
实际上, Proxy.newProxyInstance 会创建一个Class,与静态代理不同,这个Class不是由具体的.java源文件编译而来,即没有真正的文件,只是在内存中按照Class格式生成了一个Class。
动态代理源码分析
本文以jdk提供的Proxy作为分析,Android的实现和jdk的实现大同小异,差别最后再说。
1 | public static Object newProxyInstance(ClassLoader loader, |
为什么动态代理需要传入classLoader?
1⃣️需要校验传入的接口是否可被当前的类加载器加载,假如无法加载,证明这个接口与类加载器不是同一个,按照双亲委派模型,那么类加载层次就被破坏了
2⃣️需要类加载器去根据生成的类的字节码去通过defineClass方法生成类的class文件,也就是说没有类加载的话是无法生成代理类的
我们主要看一下怎么获取到代理类的
1 | Class<?> cl = getProxyClass0(loader, intfs); |
1 | private static final WeakCache<ClassLoader, Class<?>[], Class<?>> |
1 | private static Class<?> getProxyClass0(ClassLoader loader, |
接下来,我们看看ProxyClassFactory如何创建代理类
1 | private static final class ProxyClassFactory |
接下来我们看一下具体是怎么获取代理类的二进制文件
1 | ProxyGenerator.generateProxyClass( |
1 | public static byte[] generateProxyClass(final String var0, Class<?>[] var1, int var2) { |
发现可以通过 saveGeneratedFiles参数来决定是否把代理类代存到本地,点击它来查看在哪进行设置
1 | private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles")); |
我们可以把sun.misc.ProxyGenerator.saveGeneratedFiles这个属性设为true来把生成的类保存到本地,再运行,就会看到系统帮我们生成的代理类。以下为部分截取
1 | static { |
在初始化时,获得 method 备用。而这个代理类中所有方法的实现变为:
1 | public final void message(String var1) throws { |
这里的 h 其实就是 InvocationHandler 接口,所以我们在使用动态代理时,传递的 InvocationHandler 就是一个监听,在代理对象上执行方法,都会由这个监听回调出来。
最后用一张图作为总结
和Android提供的Proxy对比
1 | /* |
可以看到Android并没有通过ProxyGenerator生成代理类对象,而是直接交给native方法去处理。
静态代理和动态代理对比
- 共同点:两种代理模式实现都在不改动基础对象的前提下,对基础对象进行访问控制和扩展,符合开闭原则。
- 不同点:静态代理存在重复性和脆弱性的缺点;而动态代理(搭配泛型参数)可以实现了一个代理同时处理 N 种基础接口,一定程度上规避了静态代理的缺点。从原理上讲,静态代理的代理类 Class 文件在编译期生成,而动态代理的代理类 Class 文件在运行时生成,代理类在 coding 阶段并不存在,代理关系直到运行时才确定。