java注解原理 :反射 & 动态代理(待更新)
文章目录
1、java注解原理
参考
- java注解本质就是继承了Annotation接口的接口 https://www.cnblogs.com/yangming1996/p/9295168.html
1)注解本质(继承接口的接口)
「java.lang.annotation.Annotation」接口中有这么一句话,用来描述『注解』。
The common interface extended by all annotation types
所有的注解类型都继承自这个普通的接口(Annotation)
这句话有点抽象,但却说出了注解的本质。我们看一个 JDK 内置注解的定义:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这是注解 @Override
的定义,其实它本质上就是:
public interface Override extends Annotation{
}
没错,注解的本质就是一个继承了 Annotation 接口的接口。Annotation
接口定义如下:
public interface Annotation {
boolean equals(Object obj); //Returns: true if the specified object represents an annotation that is logically equivalent to this one, otherwise false
int hashCode(); //Returns: the hash code of this annotation
String toString();
Class<? extends Annotation> annotationType(); //Returns: the annotation type of this annotation 注意Class<T>中的T可以是泛型类,或者是泛型接口
}
一个注解准确意义上来说,只不过是一种特殊的注释而已,如果没有解析它的代码,它可能连注释都不如。
而解析一个类或者方法的注解往往有两种形式,一种是编译期直接的扫描,一种是运行期反射(解析方式主要取决于@Retention注解)。反射的事情我们待会说,而编译器的扫描指的是编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。
典型的就是注解
@Override
,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。这一种情况只适用于那些编译器已经熟知的注解类,比如 JDK 内置的几个注解,而你自定义的注解,编译器是不知道你这个注解的作用的,当然也不知道该如何处理,往往只是会根据该注解的作用范围来选择是否编译进字节码文件,仅此而已。
2)元注解(修饰注解的注解)
参考
- 自定义注解和元注解https://www.cnblogs.com/boywwj/p/7714404.html
- java注解本质就是继承了Annotation接口的接口 https://www.cnblogs.com/yangming1996/p/9295168.html
『元注解』是用于修饰注解的注解,通常用在注解的定义上,例如:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
这是我们 @Override
注解的定义,你可以看到其中的 @Target
,@Retention
两个注解就是我们所谓的『元注解』,『元注解』一般用于指定某个注解生命周期以及作用目标等信息。
JAVA 中有以下几个『元注解』:
- @Target:注解的作用目标
- @Retention:注解的生命周期
- @Documented:注解是否应当被包含在 JavaDoc(API) 文档中
- @Inherited:是否允许子类继承该注解
a)@Target(作用目标)
@Target
用于指明被修饰的注解最终可以作用的目标是谁,也就是指明,你的注解到底是用来修饰方法的?修饰类的?还是用来修饰字段属性等。
其定义如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
/**
* Returns an array of the kinds of elements an annotation type
* can be applied to.
* @return an array of the kinds of elements an annotation type
* can be applied to
*/
ElementType[] value();
}
其中Target注解上的@Target值为ANNOTATION_TYPE
,还有其他值(value值为数组类型):
@Target(value = {ElementType.FIELD})
其中,ElementType 是一个枚举类型,有以下一些值:
- ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
- ElementType.FIELD:允许作用在属性字段上
- ElementType.METHOD:允许作用在方法上
- ElementType.PARAMETER:允许作用在方法参数上
- ElementType.CONSTRUCTOR:允许作用在构造器上
- ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
- ElementType.ANNOTATION_TYPE:允许作用在注解上
- ElementType.PACKAGE:允许作用在包上
这里强调一点,@Target
在使用时要满足以下两种书写形式中的一种,否则会编译报错:
@Target(ElementType.ANNOTATION_TYPE)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.FIELD})
b)@Retention(JVM有关)
@Retention
用于指明当前注解的生命周期,它的基本定义如下(value值为单值类型):
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
/**
* Returns the retention policy.
* @return the retention policy
*/
RetentionPolicy value();
}
例如
@Retention(value = RetentionPolicy.RUNTIME)
这里的 RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:
- RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
- RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
- RetentionPolicy.RUNTIME:永久保存,方便使用反射获取。
过程:编译 -> 写入class文件 -> 类加载 -> 反射获取
@Retention
注解指定了被修饰的注解的生命周期,一种是只能在编译期可见,编译后会被丢弃;一种会被编译器编译进 class 文件中,无论是类或是方法,乃至字段,他们都是有属性表的,而 JAVA 虚拟机也定义了几种注解属性表用于存储注解信息,但是这种可见性不能带到方法区,类加载时会予以丢弃;最后一种则是永久存在的可见性。
c)@Documented & @Inherited
@Documented
注解修饰的注解,当我们执行 JavaDoc 文档打包时会被保存进 doc 文档,反之将在打包时丢弃。@Inherited
注解修饰的注解是具有可继承性的,也就说我们的注解修饰了一个类,而该类的子类将自动继承父类的该注解。
3)Java 3大内置注解
除了上述四种元注解外,JDK 还为我们预定义了另外三种注解,它们是:
- @Override
- @Deprecated
- @SuppressWarnings
a)@Override
@Override
注解想必是大家很熟悉的了,它的定义如下:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
它没有任何的属性,所以并不能存储任何其他信息。它只能作用于方法之上,编译结束后将被丢弃(SOURCE)。
所以你看,它就是一种典型的**『标记式注解』**,仅被编译器可知,编译器在对 java 文件进行编译成字节码的过程中,一旦检测到某个方法上被修饰了该注解,就会去匹对父类中是否具有一个同样方法签名的函数,如果不是,自然不能通过编译。
b)@Deprecated
@Deprecated
的基本定义如下:
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE}) public @interface Deprecated { }
依然是一种**『标记式注解』**,永久存在(RUNTIME),可以修饰所有的类型,作用是,标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除。
当然,编译器并不会强制要求你做什么,只是告诉你 JDK 已经不再推荐使用当前的方法或者类了,建议你使用某个替代者。
c)@SuppressWarnings
@SuppressWarnings
主要用来压制 java 的警告,它的基本定义如下:
它有一个 value 属性需要你主动的传值,这个 value 代表一个什么意思呢,这个 value 代表的就是需要被压制的警告类型。例如:
public static void main(String[] args) { Date date = new Date(2018, 7, 11); }
这么一段代码,程序启动时编译器会报一个警告。
Warning:(8, 21) java: java.util.Date 中的 Date(int,int,int) 已过时
而如果我们不希望程序启动时,编译器检查代码中过时的方法,就可以使用 @SuppressWarnings 注解并给它的 value 属性传入一个参数值来压制编译器的检查。
@SuppressWarning(value = "deprecated") public static void main(String[] args) { Date date = new Date(2018, 7, 11); }
这样你就会发现,编译器不再检查 main 方法下是否有过时的方法调用,也就压制了编译器对于这种警告的检查。
当然,JAVA 中还有很多的警告类型,他们都会对应一个字符串,通过设置 value 属性的值即可压制对于这一类警告类型的检查。
4)注解解析(从JVM上解析注解本质)
参考
- Java注解(Annotation)原理详解 https://blog.csdn.net/lylwo317/article/details/52163304
- java注解本质就是继承了Annotation接口的接口 https://www.cnblogs.com/yangming1996/p/9295168.html
a)Java的反射(不用new)
- 反射之中包含了一个「反」字,所以想要解释反射就必须先从「正」开始解释。一般情况下,我们使用某个类时必定知道它是什么类,是用来做什么的。
Apple apple = new Apple(); //直接初始化,「正射」
- 而反射则是一开始并不知道我要初始化的类对象是什么,自然也无法使用 new 关键字来创建对象了。这时候,我们使用 JDK 提供的反射 API 进行反射调用:
Class clz = Class.forName("com.chenshuyi.reflect.Apple"); Method method = clz.getMethod("setPrice", int.class);
- 反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。
我的理解:
- Java的反射的实现在某种程度上得益于Java的多态性的实现(特别是继承、接口实现 --> 向上(父类向子类)类型转换,而且还涉及到泛型)
- 反射并不单纯针对java中继承于Class的子类,下面要讲到的Class通过实现
AnnotatedElement
接口,进而在Annotation
并非继承于Class
的前提下,也能实现Class
利用AnnotatedElement
接口完成反射注解(此过程涉及到动态代理)。- 基于接口的反射得到的是注解的实现类,即Proxy代理类;而基于
Class
子类的反射则是直接调用父类中的反射方法,获取未知类型中的具体构造。- Java的接口,注解,枚举其实都可以看成是java类,最终都会被编译成
.class
字节码文件。
b)反射注解(RUNTIME & Class类实现AnnotatedElement & 动态代理)
上述内容我们介绍了注解使用上的细节,也简单提到,「注解的本质就是一个继承了 Annotation 接口的接口」,现在我们就来从虚拟机的层面看看,注解的本质到底是什么(在该小节的最后可以从自定义注解的字节码中看出注解的本质)。
① Class类如何反射注解 - 实现AnnotatedElement接口
首先,我们自定义一个注解类型:
@Target(value = {ElementType.FIELD,ElementType.METHOD})
@Retention(value = RetentionPolicy.RUNTIME)
public @interface HelloAnnotation {
String myValue() default "Hello";
}
这里我们指定了 Hello 这个注解只能修饰字段和方法,并且该注解永久存活,以便我们反射获取。
之前我们说过,虚拟机规范定义了一系列和注解相关的属性表,也就是说,无论是字段、方法或是类本身,如果被注解修饰了,就可以被写进字节码文件。属性表有以下几种:
- RuntimeVisibleAnnotations:运行时可见的注解
- RuntimeInVisibleAnnotations:运行时不可见的注解
- RuntimeVisibleParameterAnnotations:运行时可见的方法参数注解
- RuntimeInVisibleParameterAnnotations:运行时不可见的方法参数注解
- AnnotationDefault:注解类元素的默认值
给大家看虚拟机的这几个注解相关的属性表的目的在于,让大家从整体上构建一个基本的印象,注解在字节码文件中是如何存储的。
所以,对于一个类或者接口来说,Class 类中提供了以下一些方法用于反射注解。
- getAnnotation:返回指定的注解
- isAnnotationPresent:判定当前元素是否被指定注解修饰
- getAnnotations:返回所有的注解
- getDeclaredAnnotation:返回本元素的指定注解
- getDeclaredAnnotations:返回本元素的所有注解,不包含父类继承而来的。
Note:这里简述一下Class<T>
是如何反射注解的。
- 一般情况下,继承于class类的子类可以通过class基类的方法反射获取得到子类的property,method等,但是对于注解,子类如何通过父类得到呢?这里的class类需要对AnnotatedElement接口进行实现,这个接口提供要返回的
Annotation
的方法。- Java反射机制解析注解主要是通过
java.lang.reflect
包下的提供的AnnotatedElement
接口,Class<T>
实现了该接口定义的方法,返回本元素/所有的注解(Annotation接口)。
AnnotatedElement
是所有注解元素的父接口,所有的注解元素都可以通过某个类反射获取AnnotatedElement对象,该对象有以下4个方法来访问Annotation信息。AnnotatedElement
接口定义如下:public interface AnnotatedElement { default boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) { return getAnnotation(annotationClass) != null; } <T extends Annotation> T getAnnotation(Class<T> annotationClass); Annotation[] getAnnotations(); default <T extends Annotation> T getDeclaredAnnotation(Class<T> annotationClass) {} default <T extends Annotation> T[] getDeclaredAnnotationsByType(Class<T> annotationClass) { ... } Annotation[] getDeclaredAnnotations(); }
这个接口有default方法,Class类可直接使用。
② 反编译的代理类中认识注解本质 - 反射返回动态代理对象 & 注解实现类
方法、字段中相关反射注解的方法基本是类似的,下面看一个完整的例子。
首先,设置一个虚拟机启动参数,用于捕获 JDK 动态代理类。
VM options = -Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
也可以在Main函数上输入:
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
参考 https://blog.csdn.net/zyq8514700/article/details/99892329
然后 main 函数如下:
public class Main {
@HelloAnnotation(myValue = "Hello World")
public static void main(String[] args) throws NoSuchMethodException {
System.setProperty("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
Class clzz = Main.class;
Method main_method = clzz.getMethod("main",String[].class);
// clzz.getAnnotation()//不是用类来获取该注解,应该通过main方法获取Hello注解
HelloAnnotation hello = main_method.getAnnotation(HelloAnnotation.class); //获取Main方法上的注解
System.out.println(hello.myValue()); //myValue对外可以看成是键值对,在注解内看成是返回键值的方法。
}
}
---
Hello World
注解本质上是继承了 Annotation 接口的接口,而当你通过反射,也就是我们这里的 getAnnotation 方法去获取一个注解类实例的时候,其实 JDK 是通过动态代理机制生成一个实现我们注解(接口)的代理类。我们运行程序后,会看到项目根目录里有这么一个代理类,该代理类$Proxy1
是HelloAnnotation注解的实现类。
class字节码文件反编译之后是这样的(IDEA默认使用FernFlower 对class文件自动反编译):
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);
}
//Annotation接口方法
public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}
//Annotation接口方法
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);
}
}
//Annotation接口方法
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);
}
}
//Annotation接口方法
public final String myValue() throws {
try {
return (String)super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
//Annotation接口方法
public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m2 = Class.forName("java.lang.Object").getMethod("toString");
m4 = Class.forName("com.wang.annotationTest.HelloAnnotation").getMethod("annotationType");
m3 = Class.forName("com.wang.annotationTest.HelloAnnotation").getMethod("myValue");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}
代理类$Proxy1
是接口 HelloAnnotation
的实现类,重写该接口的所有方法,包括 value 方法以及接口 Hello 从 Annotation 接口继承而来的方法。并继承了Proxy
类:
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
...
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
...
}
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);
}
...
}
最后在**调用代理类对象方法(equal,hashcode,annotationType等)**时,主要是通过调用传给Proxy的AnnotationInvocationHandler
实例的invoke()
来实现。其实现的接口InvocationHandler
定义如下:
public interface InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}
这个关键的 InvocationHandler 实例是:AnnotationInvocationHandler
;这里主要看var7
的switch
语句,其对函数名称进行判断和标号,以及Object var6 = this.memberValues.get(var4);
调用自定义注解的方法。
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
private static final long serialVersionUID = 6182022883658399397L;
public Object invoke(Object var1, Method var2, Object[] var3) {
String var4 = var2.getName();
Class[] var5 = var2.getParameterTypes();
if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
return this.equalsImpl(var3[0]);
} else if (var5.length != 0) {
throw new AssertionError("Too many parameters for an annotation method");
} else {
byte var7 = -1;
switch(var4.hashCode()) {
case -1776922004:
if (var4.equals("toString")) {
var7 = 0;
}
break;
case 147696667:
if (var4.equals("hashCode")) {
var7 = 1;
}
break;
case 1444986633:
if (var4.equals("annotationType")) {
var7 = 2;
}
}
switch(var7) {
case 0:
return this.toStringImpl();
case 1:
return this.hashCodeImpl();
case 2:
return this.type;
default:
Object var6 = this.memberValues.get(var4);
if (var6 == null) {
throw new IncompleteAnnotationException(this.type, var4);
} else if (var6 instanceof ExceptionProxy) {
throw ((ExceptionProxy)var6).generateException();
} else {
if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
var6 = this.cloneArray(var6);
}
return var6;
}
}
}
}
如果当前调用的方法是 toString,equals,hashCode,annotationType 的话,AnnotationInvocationHandler
实例中已经预定义好了这些方法的实现,直接调用即可。
那么假如 var7 没有匹配上这四种方法,说明当前的方法调用的是自定义注解字节声明的方法,例如我们 Hello 注解的 value 方法。这种情况下,将从我们的注解 map 中获取这个注解属性对应的值。
③ 从字节码认识注解本质 - 继承Annotation接口的接口
我们可以通过@HelloAnnotation
注解的字节码,探索注解的本质:
1)进入编译后的target文件夹中,找到
HelloAnnotation
的class类2)
javap -verbose .\HelloAnnotation.class
: javap 的作用是根据 class 字节码文件,反解析出当前类对应的code区(汇编指令)、本地变量表、异常表和代码行偏移量映射表、常量池等等信息。3)
@HelloAnnotation
字节码Last modified 2021年12月11日; size 498 bytes MD5 checksum 22dad8d7128883a1ada63f6dc2589748 Compiled from "HelloAnnotation.java" public interface com.wang.annotationTest.HelloAnnotation extends java.lang.annotation.Annotation //注释:自定义注解继承于Annotation接口 minor version: 0 major version: 52 flags: (0x2601) ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT, ACC_ANNOTATION this_class: #1 // com/wang/annotationTest/HelloAnnotation super_class: #2 // java/lang/Object interfaces: 1, fields: 0, methods: 1, attributes: 2 Constant pool: #1 = Class #19 // com/wang/annotationTest/HelloAnnotation #2 = Class #20 // java/lang/Object #3 = Class #21 // java/lang/annotation/Annotation #4 = Utf8 myValue #5 = Utf8 ()Ljava/lang/String; #6 = Utf8 AnnotationDefault #7 = Utf8 Hello #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 FIELD #15 = Utf8 METHOD #16 = Utf8 Ljava/lang/annotation/Retention; #17 = Utf8 Ljava/lang/annotation/RetentionPolicy; #18 = Utf8 RUNTIME #19 = Utf8 com/wang/annotationTest/HelloAnnotation #20 = Utf8 java/lang/Object #21 = Utf8 java/lang/annotation/Annotation { public abstract java.lang.String myValue(); descriptor: ()Ljava/lang/String; flags: (0x0401) ACC_PUBLIC, ACC_ABSTRACT AnnotationDefault: default_value: s#7 "Hello" } SourceFile: "HelloAnnotation.java" RuntimeVisibleAnnotations: 0: #11(#12=[e#13.#14,e#13.#15]) java.lang.annotation.Target( value=[Ljava/lang/annotation/ElementType;.FIELD,Ljava/lang/annotation/ElementType;.METHOD] ) 1: #16(#12=e#17.#18) java.lang.annotation.Retention( value=Ljava/lang/annotation/RetentionPolicy;.RUNTIME )
看到第7行。很明显,HelloAnnotation就是继承了Annotation的接口。再看第10行,flag字段中,我们可以看到,有个ACC_ANNOTATION
标记,说明是一个注解,所以注解本质是一个继承了Annotation的特殊接口。
c)java动态代理(代理模式 + 动态生成代理类 + 方法重写 + AOP)
代理模式是常用的java设计模式,他的特征是代理类与委托类有同样的接口,代理类主要负责为委托类预处理消息、过滤消息、把消息转发给委托类,以及事后处理消息等。代理类与委托类之间通常会存在关联关系,一个代理类的对象与一个委托类的对象关联,代理类的对象本身并不真正实现服务,而是通过调用委托类的对象的相关方法,来提供特定的服务。简单的说就是,我们在访问实际对象时,是通过代理对象来访问的,代理模式就是在访问实际对象时引入一定程度的间接性,因为这种间接性,可以附加多种用途。
① 静态代理
静态代理:由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口,被代理类,代理类等确定下来。在程序运行之前,代理类的.class文件就已经生成。
根据上面代理模式的类图,来写一个简单的静态代理的例子。我这儿举一个比较粗糙的例子,假如一个班的同学要向老师交班费,但是都是通过班长把自己的钱转交给老师。这里,班长就是代理学生上交班费,
班长就是学生的代理。
首先,我们创建一个Person接口。这个接口就是学生(被代理类),和班长(代理类)的公共接口,他们都有上交班费的行为。这样,学生上交班费就可以让班长来代理执行。
/** * 创建Person接口 * @author Gonjan */ public interface Person { //上交班费 void giveMoney(); }
Student类实现Person接口。Student可以具体实施上交班费的动作。
public class Student implements Person { private String name; public Student(String name) { this.name = name; } @Override public void giveMoney() { System.out.println(name + "上交班费50元"); } }
StudentsProxy类,这个类也实现了Person接口,但是还另外持有一个学生类对象,由于实现了Peson接口,同时持有一个学生对象,那么他可以代理学生类对象执行上交班费(执行giveMoney()方法)行为。
/** * 学生代理类,也实现了Person接口,保存一个学生实体,这样既可以代理学生产生行为 * @author Gonjan * */ public class StudentsProxy implements Person{ //被代理的学生 Student stu; public StudentsProxy(Person stu) { // 只代理学生对象 if(stu.getClass() == Student.class) { this.stu = (Student)stu; } } //代理上交班费,调用被代理学生的上交班费行为 public void giveMoney() { System.out.println("张三最近学习有进步!"); stu.giveMoney(); } }
下面测试一下,看如何使用代理模式:
public class StaticProxyTest { public static void main(String[] args) { //被代理的学生张三,他的班费上交有代理对象monitor(班长)完成 Person zhangsan = new Student("张三"); //生成代理对象,并将张三传给代理对象 Person monitor = new StudentsProxy(zhangsan); //班长代理上交班费 monitor.giveMoney(); } }
运行结果:
这里并没有直接通过张三(被代理对象)来执行上交班费的行为,而是通过班长(代理对象)来代理执行了。这就是代理模式。
② 动态代理(生成代理类 & 传入句柄实现类 & InvocationHandler.invoke功能增强 & 方法重写)
我的理解:
- 代理要实现的是对被代理类的新的封装,通过代理类向外暴露的方法,对被代理类中的方法进行增强。
- 动态代理其实就是在代码运行时,为被代理类自动生成一个代理类,该代理类通过
InnovationHandler
的实例(自定义实现类)的invoke
方法,实现被代理类中原始方法(eg:giveMoney)的调用(对传入的Method进行invoke)和增强。注意该方法是接口中的,被代理类是对该方法的实现;而在代理类中,则对该原始方法进行重写,其中会调用InnovationHandler.invoke()
,实现功能增强。这有点像子类在初始化时先使用父类的构造方法(super())- 至于动态代理如何在运行时将句柄实例和被代理类封装成代理类的,这细节问题就不太清楚了。
代理类在程序运行时创建的代理方式被成为动态代理。 我们上面静态代理的例子中,代理类(studentProxy)是自己定义好的,在程序运行之前就已经编译完成。然而动态代理,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理, 动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。 比如说,想要在每个代理的方法前都加上一个处理方法:
public void giveMoney() { //调用被代理方法前加入处理方法 beforeMethod(); stu.giveMoney(); }
如果出了giveMonney还有很多其他的方法,那就需要写很多次beforeMethod方法,麻烦。
在java的
java.lang.reflect
包下提供了一个Proxy类和一个InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。创建一个动态代理对象步骤,具体代码见后面:
- 创建一个
InvocationHandler
对象//创建一个与代理对象相关联的InvocationHandler InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu);
- 使用Proxy类的
getProxyClass
静态方法生成一个动态代理类stuProxyClassClass<?> stuProxyClass = Proxy.getProxyClass(Person.class.getClassLoader(), new Class<?>[] {Person.class});
- 获得stuProxyClass 中一个带InvocationHandler参数的构造器constructor
Constructor<?> constructor = PersonProxy.getConstructor(InvocationHandler.class);
- 通过构造器constructor来创建一个动态实例stuProxy
Person stuProxy = (Person) cons.newInstance(stuHandler);
就此,一个动态代理对象就创建完毕,当然,上面四个步骤可以通过Proxy类的newProxyInstances方法来简化:
//创建一个与代理对象相关联的InvocationHandler InvocationHandler stuHandler = new MyInvocationHandler<Person>(stu); //创建一个代理对象stuProxy,代理对象的每个执行方法都会替换执行Invocation中的invoke方法 Person stuProxy= (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler);
到这里肯定都会很疑惑,这动态代理到底是如何执行的,是如何通过代理对象来执行被代理对象的方法的,先不急,我们先看看一个简单的完整的动态代理的例子。还是上面静态代理的例子,班长需要帮学生代交班费。
/** * 创建Person接口 * @author Gonjan */ public interface Person { //上交班费 void giveMoney(); }
创建需要被代理的实际类:
public class Student implements Person { private String name; public Student(String name) { this.name = name; } @Override public void giveMoney() { try { //假设数钱花了一秒时间 Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(name + "上交班费50元"); } }
再定义一个检测方法执行时间的工具类,在任何方法执行前先调用start方法,执行后调用finsh方法,就可以计算出该方法的运行时间,这也是一个最简单的方法执行时间检测工具。
public class MonitorUtil { private static ThreadLocal<Long> tl = new ThreadLocal<>(); public static void start() { tl.set(System.currentTimeMillis()); } //结束时打印耗时 public static void finish(String methodName) { long finishTime = System.currentTimeMillis(); System.out.println(methodName + "方法耗时" + (finishTime - tl.get()) + "ms"); } }
创建
StuInvocationHandler
类,实现InvocationHandler接口,这个类中持有一个被代理对象的实例target。InvocationHandler中有一个invoke方法,所有执行代理对象的方法都会被替换成执行invoke方法。再在invoke方法中执行被代理对象target的相应方法。当然,在代理过程中,我们在真正执行被代理对象的方法前加入自己其他处理。这也是Spring中的AOP实现的主要原理,这里还涉及到一个很重要的关于java反射方面的基础知识。
public class StuInvocationHandler<T> implements InvocationHandler { //invocationHandler持有的被代理对象 T target; public StuInvocationHandler(T target) { this.target = target; } /** * proxy:代表动态代理对象 * method:代表正在执行的方法 * args:代表调用目标方法时传入的实参 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("代理执行" +method.getName() + "方法"); */ //代理过程中插入监测方法,计算该方法耗时 MonitorUtil.start(); Object result = method.invoke(target, args); MonitorUtil.finish(method.getName()); return result; } }
做完上面的工作后,我们就可以具体来创建动态代理对象了,上面简单介绍了如何创建动态代理对象,我们使用简化的方式创建动态代理对象:
public class ProxyTest { public static void main(String[] args) { //创建一个实例对象,这个对象是被代理的对象 Person zhangsan = new Student("张三"); //创建一个与代理对象相关联的InvocationHandler InvocationHandler stuHandler = new StuInvocationHandler<Person>(zhangsan); //创建一个代理对象stuProxy来代理zhangsan,代理对象的每个执行方法都会替换执行Invocation中的invoke方法 Person stuProxy = (Person) Proxy.newProxyInstance(Person.class.getClassLoader(), new Class<?>[]{Person.class}, stuHandler); //代理执行上交班费的方法 stuProxy.giveMoney(); } }
我们执行这个ProxyTest类,先想一下,我们创建了一个需要被代理的学生张三,将zhangsan对象传给了stuHandler中,我们在创建代理对象stuProxy时,将stuHandler作为参数了的,上面也有说到所有执行代理对象的方法都会被替换成执行invoke方法,也就是说,最后执行的是StuInvocationHandler中的invoke方法。所以在看到下面的运行结果也就理所当然了。
上面说到,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。是因为所有被代理执行的方法,都是通过在InvocationHandler中的invoke方法调用的(该方法传入Proxy对象实例),所以我们只要在invoke方法中统一处理,就可以对所有被代理的方法进行相同的操作了。例如,这里的方法计时,所有的被代理对象执行的方法都会被计时,然而我只做了很少的代码量。
动态代理的过程,代理对象和被代理对象的关系不像静态代理那样一目了然,清晰明了。因为动态代理的过程中,我们并没有实际看到代理类(!!),也没有很清晰地的看到代理类的具体样子(可以通过设置JVM参数,对字节码反编译,查看生成的代理类,跳转至[反编译得到代理类](#② 反编译的代理类中认识注解本质 - 反射返回动态代理对象 & 注解实现类)),而且动态代理中被代理对象和代理对象是通过InvocationHandler来完成的代理过程的,其中具体是怎样操作的,为什么代理对象执行的方法都会通过InvocationHandler中的invoke方法来执行。带着这些问题,我们就需要对java动态代理的源码进行简要的分析,弄清楚其中缘由。
public final class $Proxy0 extends Proxy implements Person { ... public final void giveMoney() throws { try { this.h.invoke(this, m3, null); //这里调用代理对象的giveMoney方法,直接就调用了InvocationHandler中的invoke方法,并把m3传了进去。 *this.h.invoke(this, m3, null); return; } catch (Error|RuntimeException localError) { throw localError; } catch (Throwable localThrowable) { throw new UndeclaredThrowableException(localThrowable); } } }
生成的代理类:
$Proxy0 extends Proxy implements Person
,我们看到代理类继承了Proxy类,所以也就决定了java动态代理只能对接口进行代理,Java的继承机制注定了这些动态代理类们无法实现对class的动态代理。
上面的动态代理的例子,其实就是AOP的一个简单实现了,在目标对象的方法执行之前和执行之后进行了处理,对方法耗时统计。Spring的AOP实现其实也是用了Proxy和InvocationHandler这两个东西的。
d)小总结
注解本质是一个继承了Annotation的特殊接口,其具体实现类是Java运行时生成的动态代理类。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler的invoke方法。该方法会从memberValues这个Map中索引出对应的值。而memberValues的来源是Java常量池。
最后我们再总结一下整个反射注解的工作原理:
-
1)首先,我们通过键值对的形式可以为注解属性赋值,像这样:
@Hello(value = "hello")
。 -
2)接着,你用注解修饰某个元素,编译器将在编译期扫描每个类或者方法上的注解,会做一个基本的检查,你的这个注解是否允许作用在当前位置,最后会将注解信息写入元素的属性表。
-
3)然后,当你进行反射的时候,虚拟机将所有生命周期在
RUNTIME
的注解取出来放到一个 map 中,并创建一个 AnnotationInvocationHandler 实例,把这个 map 传递给它。 -
4)最后,虚拟机将采用 JDK 动态代理机制生成一个目标注解的代理类,并初始化好处理器。
那么这样,一个注解的实例就创建出来了,它本质上就是一个代理类,你应当去理解好 AnnotationInvocationHandler 中 invoke 方法的实现逻辑,这是核心。一句话概括就是,通过方法名返回注解属性值。
2、Spring常用注解
@Aspect注解 https://blog.csdn.net/fz13768884254/article/details/83538709
@Around(“@annotation(dataDictClass)”)表示在dataDictClass注解上环绕增强