动态代理技术在Java中有着非常广泛的应用,我再阅读Spring源码的时候多次碰到对这种技术的使用,在这篇文章做个总结。
代理模式
首先介绍下什么是代理模式:代理模式给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
在代理模式中,我们把角色分为 3 种:
Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;
RealSubject(真实主题角色):真正实现业务逻辑的类;
Proxy(代理主题角色):用来代理和封装真实主题;
后面在编写Java动态代理的代码时,我们也会看到代码与这三个角色的对应关系。
如果根据字节码的创建时机来分类,可以将代理模式分为静态代理和动态代理:
- 所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
- 而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件
静态代理
我们先通过实例来了解下静态代理,理解静态代理的缺点,再来了解动态代理
编写一个接口 Run,以及该接口的一个实现类 Runner。
public interface Run {
public void run();
}
public class Runner implements Run {
@Override
public void run() {
System.out.println("I am running");
}
}
然后我们使用静态代理对 Runner 进行功能增强,在调用run
之前记录一些日志。写一个代理类 RunnerProxy ,代理类需要实现 Run
public class RunnerProxy implements Run {
private Run run;
public RunnerProxy(Run run) {
this.run = run;
}
@Override
public void run() {
System.out.println("before run");
run.run();
System.out.println("after run");
}
}
具体使用方式如下:
public class ProxyTest {
public static void main(String[] args) {
Run run = new Runner();
RunnerProxy runnerProxy = new RunnerProxy(run);
runnerProxy.run();
}
}
打印结果如下:
before run
I am running
after run
通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。
静态代理的缺点
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。
1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
- 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
- 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
动态代理
动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用,在这里我们借助JDK自带的动态代理和CGLIB动态代理两种技术来实现。
JDK动态代理
JDK动态代理主要涉及两个类:java.lang.reflect.Proxy
和 java.lang.reflect.InvocationHandler
我们依照上面代理的三个角色分别来写编码。
Subject(抽象主题角色),对应Run接口:
public interface Run {
public void run();
}
RealSubject(真实主题角色),对应Runner类:
public class Runner implements Run {
@Override
public void run() {
System.out.println("I am running");
}
}
Proxy(代理主题角色),对应ProxyHandler:
public class ProxyHandler implements InvocationHandler {
private Object object;
public ProxyHandler(Object object) {
this.object = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before " + method.getName());
Object object = method.invoke(this.object, args);
System.out.println("after " + method.getName());
System.out.println("--------------------------------");
return object;
}
}
具体使用方式如下:
import java.lang.reflect.Proxy;
public class ProxyTest {
public static void main(String[] args) {
Run run = new Runner();
Jump jump = new Jumper();
ProxyHandler handler = new ProxyHandler(run);
Run proxyRunner = (Run) Proxy.newProxyInstance(run.getClass().getClassLoader(), run.getClass().getInterfaces(), handler);
proxyRunner.run();
}
}
运行结果:
before run
I am running
after run
CGLIB动态代理
首先maven引入CGLIB包。
正常编写类Jumper,不需要实现接口
public class Jumper {
public void jump() {
System.out.println("I am jumping");
}
}
编写一个JumperLogger,继承MethodInterceptor,用于方法的拦截回调:
public class JumperLogger implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
before();
Object result = methodProxy.invokeSuper(object, objects);
after();
return result;
}
private void before() {
System.out.println(String.format("log start time [%s] ", new Date()));
}
private void after() {
System.out.println(String.format("log end time [%s] ", new Date()));
}
}
具体使用方式如下:
public class ProxyTest {
public static void main(String[] args) {
JumperLogger jumperLogger = new JumperLogger();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Jumper.class);
enhancer.setCallbacks(new Callback[]{jumperLogger});
Jumper proxy = (Jumper) enhancer.create();
proxy.jump();
}
}
运行结果如下:
log start time [Fri Apr 14 17:55:33 CST 2023]
I am jumping
log end time [Fri Apr 14 17:55:33 CST 2023]
CGLIB 创建动态代理类的模式是:
- 查找目标类上的所有非final 的public类型的方法定义;
- 将这些方法的定义转换成字节码;
- 将组成的字节码转换成相应的代理的class对象;
- 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求
JDK动态代理与CGLIB动态代理对比
JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。
cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
JDK Proxy 的优势:
- 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
- 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
- 代码实现简单。
基于类似 cglib 框架的优势:
- 无需实现接口,达到代理类无侵入
- 只操作我们关心的类,而不必为其他相关类增加工作量。
- 高性能