动态代理

代理模式是设计模式的一种,不展开说了,比较基础。这里介绍一下Java的动态代理。

动态代理的两种方式

基于接口的JDK动态代理

JDK动态代理是通过Java的反射机制实现的。它要求目标对象实现一个InvocationHandler接口,然后通过java.lang.reflect.Proxy类创建代理对象。代理对象实现了目标接口,并将方法调用委托给一个InvocationHandler接口的实现类。通过在InvocationHandler的实现类中重写invoke()方法,可以在方法调用前后添加额外的逻辑。

基于类的CGLIB动态代理

CGLIB动态代理不要求目标对象实现接口,而是通过字节码技术生成目标对象的子类作为代理对象。CGLIB(Code Generation Library)使用ASM框架直接操作字节码,在运行时生成代理类。通过继承目标类并重写方法,CGLIB代理对象可以拦截方法调用,并在方法调用前后插入自定义逻辑。

以下是两种代理方式的实例代码:

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
public class LogProxy {
/**
* 参数:原始对象
* 返回值:被代理的对象
* JDK 动态代理
* 基于接口的动态代理
* 被代理类必须实现接口
* JDK提供的
*/
public static Object getObject(final Object obj){
/**
*
ClassLoader loader:类加载器,用于加载代理类。可以通过目标对象所在的类的类加载器进行传递,通常使用targetObject.getClass().getClassLoader()。
Class<?>[] interfaces:要实现的接口数组,指定代理类需要实现哪些接口。代理类将完全实现这些接口中的方法。可以使用 targetObject.getClass().getInterfaces() 获取目标对象实现的接口列表。
InvocationHandler h:调用处理程序,负责处理代理对象上的方法调用。需要传入一个实现了 InvocationHandler 接口的对象,该接口包含一个方法 invoke(),在方法调用期间会被调用。在 invoke() 方法中,可以通过反射调用目标对象的方法,并添加额外的逻辑。
*/
Object proxyInstance = Proxy.newProxyInstance(obj.getClass().getClassLoader()
, obj.getClass().getInterfaces(), new InvocationHandler() {
/**
通过自定义的 InvocationHandler 实现类,我们可以在不修改原始类的情况下对方法进行增强或添加额外的逻辑。
proxy:代理对象本身。如果需要在 invoke() 方法中调用代理对象的方法,可以使用该参数。
method:被调用的目标方法对象。通过该参数可以获取目标方法的信息,如方法名、参数类型等。
args:方法的参数。通过该参数可以获取方法的参数值。
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//方法执行前
long startTime = System.currentTimeMillis();

Object result = method.invoke(obj, args);//执行方法的调用

//方法执行后
long endTime = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat();
System.out.printf(String.format("%s方法执行结束时间:%%s ;方法执行耗时:%%d%%n"
, method.getName()), sdf.format(endTime), endTime - startTime);
return result;
}
});
return proxyInstance;
}
/**
* 使用CGLib创建动态代理对象
* 第三方提供的的创建代理对象的方式CGLib
* 被代理对象不能用final修饰
* 使用的是Enhancer类创建代理对象
*/
public static Object getObjectByCGLib(final Object obj){
/**
* 使用CGLib的Enhancer创建代理对象
* 参数一:对象的字节码文件
* 参数二:方法的拦截器
*/
Object proxyObj = Enhancer.create(obj.getClass(), new MethodInterceptor() {
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
//方法执行前
long startTime = System.currentTimeMillis();

Object invokeObject = method.invoke(obj, objects);//执行方法的调用

//方法执行后
long endTime = System.currentTimeMillis();
SimpleDateFormat sdf = new SimpleDateFormat();
System.out.printf(String.format("%s方法执行结束时间:%%s ;方法执行耗时:%%d%%n"
, method.getName()), sdf.format(endTime), endTime - startTime);
return invokeObject;
}
});
return proxyObj;
}
}

动态代理的底层原理

两种都是程序运行期间生成动态代理类的字节码,加载类并创建对象。只不过一个是实现了接口的类,一个是子类。

两者的不同

有以下几点:

  • JDK Proxy 是 Java 语言自带的功能,无需通过加载第三方类实现;
  • Java 对 JDK Proxy 提供了稳定的支持,并且会持续的升级和更新,Java 8 版本中的 JDK Proxy 性能相比于之前版本提升了很多;
  • JDK Proxy 是通过拦截器加反射的方式实现的;
  • JDK Proxy 只能代理实现接口的类;
  • JDK Proxy 实现和调用起来比较简单;
  • CGLib 是第三方提供的工具,基于 ASM 实现的,性能比较高;
  • CGLib 无需通过接口来实现,它是针对类实现代理,主要是对指定的类生成一个子类,它是通过实现子类的方式来完成调用的。

主要还是在设计方面和性能方面:

Proxy 是面向接口的,所有使用 Proxy 的对象都必须定义一个接口,而且用这些对象的代码也必须是对接口编程的,Proxy 生成的对象是接口一致的而不是对象一致的。这对于软件架构设计,尤其对于既有软件系统是有一定掣肘的。

Proxy 毕竟是通过反射实现的,必须在效率上付出代价:有实验数据表明,调用反射比一般的函数开销至少要大 10 倍。而且,从程序实现上可以看出,对 proxy class 的所有方法调用都要通过使用反射的 invoke 方法。因此,对于性能关键的应用,使用 proxy class 是需要精心考虑的,以避免反射成为整个应用的瓶颈。

ASM 能够通过改造既有类,直接生成需要的代码。增强的代码是硬编码在新生成的类文件内部的,没有反射带来性能上的付出。同时,ASM 与 Proxy 编程不同,不需要为增强代码而新定义一个接口,生成的代码可以覆盖原来的类,或者是原始类的子类。它是一个普通的 Java 类而不是 proxy 类,甚至可以在应用程序的类框架中拥有自己的位置,派生自己的子类。

相比于其他流行的 Java 字节码操纵工具,ASM 更小更快。ASM 具有类似于 BCEL 或者 SERP 的功能,而只有 33k 大小,而后者分别有 350k 和 150k。同时,同样类转换的负载,如果 ASM 是 60% 的话,BCEL 需要 700%,而 SERP 需要 1100% 或者更多。

ASM 已经被广泛应用于一系列 Java 项目,如Hibernate 和 Spring 也通过cglib,更高层一些的自动代码生成工具也使用了 ASM。

聊聊 Spring AOP原理

SpringBoot在很多地方都用到了AOP技术,如日志记录、全局异常处理、事务、缓存、性能监控、参数校验、权限校验等。

AOP 思想

基于动态代理思想,对原来目标对象创建代理对象,在不修改原对象代码情况下,通过代理对象调用增强功能的代码,从而对原有业务方法进行增强。

AOP 作用

在不修改源代码的情况下,可以增加额外的功能,实现在原有功能基础上的增强。

AOP 使用场景

  • 记录日志(调用方法后记录日志)
  • 监控性能(统计方法运行时间)
  • 权限控制(调用方法前校验是否有权限)
  • 事务管理(调用方法前开启事务,调用方法后提交关闭事务 )
  • 缓存优化(第一次调用查询数据库,将查询结果放入内存对象, 第二次调用,直接从内存对象返回,不需要查询数据库 )

AOP 实现原理

Spring AOP 的有两种实现方式:JDK proxy 和 CGLib 动态代理。

当 Bean 实现接口时,Spring 使用 JDK proxy实现。当 Bean 没有实现接口时,Spring 使用 CGlib 代理实现。

通过配置可以强制使用 CGlib 代理(在 spring 配置中加入 aop:aspectj-autoproxy proxy-target-class=“true”)。