动态代理在Java中有着广泛的应用,比如 Spring AOP、Hibernate 数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。在了解动态代理之前,需要先来学习一下静态代理。。
静态代理
假设现在项目经理有一个需求:在项目现有所有类的方法前后打印日志。
你如何在不修改已有代码的前提下,完成这个需求?
我首先想到的是静态代理。具体做法是:
- 为现有的每一个类都编写一个对应的代理类,并且让它实现和目标类相同的接口(假设都有的话)
- 代理类里面会有 真实类 AImpl 的对象引用,该对象引用在new AProxy 时进行初始化。
有点类似与包装模式的感觉,就是利用代理类在目标对象外面加一层
静态代理的缺点
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。
-
当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
-
- 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
- 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
-
当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
类加载的复习
Java虚拟机类加载过程主要分为五个阶段:==加载、验证、准备、解析、初始化。==其中加载阶段需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构 (即方法区会存储一个这个类的模板数据结构)
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,==获取类的二进制字节流(class字节码)==就有很多途径:
- 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
- 从网络中获取,典型的应用是 Applet
- 运行时计算生成, 这种场景使用最多的是动态代理技术,在
java.lang.reflect.Proxy
类中,就是用了 - ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流 - 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
- 从数据库中获取等等
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。(即在不写代理类的情况下,直接生成代理字节码,进而得到代理对象,然后根据它创建代理实例(反射))
常见的字节码操作类库
这里有一些介绍:https://java-source.net/open-source/bytecode-libraries
Apache BCEL (Byte Code Engineering Library):
是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。ObjectWeb ASM
:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。CGLIB(Code Generation Library)
:是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。Javassist
:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。- …
由于使用ASM对使用者要求比较高,使用Javassist会比较麻烦。目前常见的两种动态代理方法为:
- 通过实现接口的方式 -> JDK动态代理
- 通过继承类的方式 -> CGLIB动态代理
这两种方式可以很好的保证生成的代理类与目标类的结构相同。
JDK 动态代理
动态代理核心思路
JDK提供了 java.lang.reflect.InvocationHandler接口
和 java.lang.reflect.Proxy类
,这两个类相互配合就能实现动态代理了。
1.利用 Proxy 获得代理类的class对象
Proxy有个静态方法:getProxyClass(ClassLoader, interfaces)
,只要你给它传入类加载器和一组接口,它就给你返回返回指定接口的代理类对象。
- 所以,一旦我们明确接口,完全可以通过接口的Class对象,创建一个代理Class,通过代理Class即可创建代理对象。这也是为什么在静态类中我们创建了一个代理类与目标类的共同接口的原因:保证代理类和目标类的信息与方法相同
- 该静态方法主要是将为 interface 构建了一个构造器,进而让它能够创建出相应的 class 对象。
根据《java 反射》那章节的知识,创建一个实例主要有两种,直接getInstance或者利用构造器。且这两种本质上都是利用构造器来创建实例的。
2. 获取class 对象中的构造实例的构造器
getConstructor
用于获取构造器,根据代理 Class的构造器创建对象时,需要传入InvocationHandler
。
-
每次调用代理对象的方法,最终都会调用InvocationHandler的invoke()方法(可以将其理解为需要被代理的代理类)
3. 利用 Class.newInstance
反射创建实例
即通过构造器创建实例
- 根据代理Class的构造器创建对象时,需要传入InvocationHandler。通过构造器传入一个引用,那么必然有个成员变量去接收。没错,代理对象的内部确实有个成员变量invocationHandler,
- 而且代理对象的每个方法内部都会调用handler.invoke()!
InvocationHandler对象成了代理对象和目标对象的桥梁,不像静态代理这么直接。
4.然后直接调用返回的实例的方法,就可以发现该方法已经被代理了。
例子
硬编码的动态代理
首先创建一个接口和一个目标类:
public interface IPrinter {
void print();
}
public class Printer implements IPrinter{
@Override
public void print() {
System.out.println("打印!");
}
}
然后利用上面的步骤进行动态代理,如下:
public class JdkproxyTest {
// Proxy.getProxyClass()
public static void main(String[] args) throws Exception {
//两个参数,第一个为类加载器,可以为任意的类加载器
//第二个需要创建对象的类接口
Class iprinterProxyClazz = Proxy.getProxyClass(Printer.class.getClassLoader(),Printer.class.getInterfaces());
//获取构造器
Constructor iprinterConstructer = iprinterProxyClazz.getConstructor(InvocationHandler.class);
//利用反射创建实例,即通过构造器创建实例
IPrinter iPrinterimpl = (IPrinter) iprinterConstructer.newInstance(new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法调用前准备的日志");
//手动new一个 打印机的实现案例
Printer printerImpl = new Printer();
method.invoke(printerImpl,args);
System.out.println("方法调用前准备的日志");
return 0;
}
});
iPrinterimpl.print();
}
}
输出结果如下:
方法调用前准备的日志
打印!
方法调用前准备的日志
虽然我们完成了目标类的动态代理,但是我们还是把 Printer类写死在了 invoke中,任然无法达到一个代码多个目标类使用的目的,因此这种方式属于硬编码。
改进一下,让调用者把目标对象作为参数传进来:
优化代码
- 假设现在打印机存在两个方法需要代理,如下:
public interface IPrinter { void print(); void close(); } public class Printer implements IPrinter{ @Override public void print() { System.out.println("=====打印!"); } @Override public void close() { System.out.println("=====打印机关闭了"); } }
-
将Printer的具体实现抽象出来,让invoke 方法里面全部都是 Object类:
public class JdkProxyTest2 { //传入任意目标类,返回它的代理类 private static Object getProxy(final Object target) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { Class iprinterProxyClazz = Proxy.getProxyClass(target.getClass().getClassLoader(),target.getClass().getInterfaces()); //获取构造器 Constructor iprinterConstructer = iprinterProxyClazz.getConstructor(InvocationHandler.class); //利用反射创建实例,即通过构造器创建实例 Object proxyImpl = (Object) iprinterConstructer.newInstance(new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("方法调用前准备的日志"); //手动new一个 打印机的实现案例 method.invoke(target,args); System.out.println("方法调用前准备的日志"); return 0; } }); return proxyImpl; } public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { Printer printerimpl = new Printer(); IPrinter iPrinterProxy = (IPrinter) getProxy(printerimpl); iPrinterProxy.print(); System.out.println("-----------------------------"); iPrinterProxy.close(); } }
结果显示:
方法调用前准备的日志
=====打印!
方法调用前准备的日志
-----------------------------
方法调用前准备的日志
=====打印机关闭了
方法调用前准备的日志
我们可以在主函数中加上System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
开启代理字节码的保存。我们将代理字节码反编译的结果下:
可以看到,这就是我们的静态代理的写法,只不过静态代理的字节码需要手动写,而动态代理可以有JVM自行生成
优化代码2
在实际编程中,一般不用getProxyClass()
,而是使用Proxy类的另一个静态方法:Proxy.newProxyInstance(),直接返回代理实例, 连中间得到代理Class对象的过程都帮你隐藏(本事和上面代码类似):
//传入任意目标类,返回它的代理类
private static Object getProxy(final Object target) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Object proxy = Proxy.newProxyInstance(target.getClass().getClassLoader()
, target.getClass().getInterfaces()
, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("方法调用前准备的日志");
//手动new一个 打印机的实现案例
method.invoke(target,args);
System.out.println("方法调用前准备的日志");
return 0;
}
});
return proxy;
}
输出结果:
方法调用前准备的日志
=====打印!
方法调用前准备的日志
-----------------------------
方法调用前准备的日志
=====打印机关闭了
方法调用前准备的日志
优化代码3
代码2中,代理类的调用虽然足够灵活,可以动态生成一个具体的代理类,而不用自己显示的创建一个实现具体接口的代理类。不过调用这个代理类的过程还是有些略显复杂,与我们减少包装代码的目标不符,所以可以考虑做些小重构来简化调用过程
-
将invoke和getProxy解耦,这样 getProxy就可以配置各种invoke了
class DynamicProxy implements InvocationHandler { Object target = null; public DynamicProxy(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("方法调用前准备的日志"); //手动new一个 打印机的实现案例 method.invoke(target,args); System.out.println("方法调用前准备的日志"); return 0; } //将invoke和getProxy解耦 public <T> T getProxy() { return (T) Proxy.newProxyInstance(this.target.getClass().getClassLoader() , this.target.getClass().getInterfaces(),this); } }
-
那么此时的主函数无需强制转换了,如下:
public static void main(String[] args) throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException { //设置保存代理类 DynamicProxy dynamicProxyTool = new DynamicProxy(new Printer()); IPrinter iPrinterProxy = dynamicProxyTool.getProxy(); iPrinterProxy.print(); System.out.println("-----------------------------"); iPrinterProxy.close(); }
-
结果如下:
方法调用前准备的日志 =====打印! 方法调用前准备的日志 ----------------------------- 方法调用前准备的日志 =====打印机关闭了 方法调用前准备的日志
InvocationHandler 和 Proxy 的主要方法:
java.lang.reflect.InvocationHandler:
Object invoke(Object proxy, Method method, Object[] args)
定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用
java.lang.reflect.Proxy
static InvocationHandler getInvocationHandler(Object proxy)
用于获取指定代理对象所关联的调用处理器static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
返回指定接口的代理类static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
构造实现指定接口的代理类的一个新实例,所有方法会调用给定处理器对象的 invoke 方法static boolean isProxyClass(Class<?> cl)
返回 cl 是否为一个代理类
总结
动态代理具体步骤:
- 通过实现
InvocationHandler
接口创建自己的调用处理器; - 通过为 Proxy 类指定 ClassLoader 对象和一组 interface 来创建动态代理类;
- 通过反射机制获得动态代理类的构造函数,其唯一参数类型是调用处理器接口类型;
- 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
动态代理可以使用下面的图像来表是:
JDK动态代理执行方法调用的过程简图如下:
源码解析:
可以查看这篇文章:https://blog.csdn.net/yu_kang/article/details/88423600
我们可以看到无论是JDK动态加载还是传统的静态加载我们都需要代理类与目标类实现共同的接口,也就是说他们必须通过接口来保证相同的方法函数。
CGLIB是一种不需要共同实现接口的代理方法
CGLIB 动态代理
核心思路
CGLIB 创建动态代理类的模式是:
- 查找目标类上的所有非final 的public类型的方法定义;
- 将这些方法的定义转换成字节码;
- 将组成的字节码转换成相应的代理的class对象;
- 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求
总结: 简单的说,cglib类库并没有使用接口去创建代理类,而是直接复制目标类中的所有方法构建出来了一个新的代理类字节码。
例子
我们可以通过 cglib.proxy.Enhancer.create
来创建代理类,该函数接受两个参数:
- Class type :即目标类
- 回调函数处理器(该处理器需要 继承
MethodInterceptor
并且实现里面的intercept
方法)。
具体代码如下:
public class CGlibProxy implements MethodInterceptor {
public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("方法调用前准备的日志");
//手动new一个 打印机的实现案例
method.invoke(target,args);
System.out.println("方法调用前准备的日志");
return null;
}
public <T> T getProxy(Class<T> cls){
return (T) Enhancer.create(cls,this);
}
}
测试代码如下:
public class CGlibProxy implements MethodInterceptor {
public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("方法调用前准备的日志");
//手动new一个 打印机的实现案例
method.invoke(target,args);
System.out.println("方法调用后准备的日志");
return null;
}
public <T> T getProxy(Class<T> cls){
return (T) Enhancer.create(cls,this);
}
}
测试代码如下:
public class cgProxyTest {
public static void main(String[] args) {
DynamicProxy dynamicProxy = new DynamicProxy(new Printer());
IPrinter printer = dynamicProxy.getProxy();
printer.print();
System.out.println("===========");
printer.close();
}
}
动态代理与 CGLIB 的对比
JDK动态代理: 基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。
cglib动态代理: 基于ASM机制实现,通过生成业务类的子类作为代理类。
JDK Proxy 的优势:
- 最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
- 平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
代码实现简单。
基于类似 cglib 框架的优势:
- 无需实现接口,达到代理类无侵入
- 只操作我们关心的类,而不必为其他相关类增加工作量。
- 高性能
使用JDK动态代理还是cglib代理得看场景:
JDK动态代理和CGlib最大的不同就是接口信息。JDK动态代理实例化接口信息只拦截接口中有的目标类的方法。但是CGlib是直接利用目标类进行实例化,也就是说将拦截目标类中的所有方法。
面试问题
描述动态代理的几种实现方式?分别说出相应的优缺点
代理可以分为 “静态代理” 和 “动态代理”,动态代理又分为 “JDK动态代理” 和 “CGLIB动态代理” 实现。
静态代理: 代理对象和实际对象都继承了同一个接口,在代理对象中指向的是实际对象的实例,这样对外暴露的是代理对象而真正调用的是 Real Object
- 优点:可以很好的保护实际对象的业务逻辑对外暴露,从而提高安全性。
- 缺点:不同的接口要有不同的代理类实现,会很冗余
JDK 动态代理:
- 为了解决静态代理中,生成大量的代理类造成的冗余;
- JDK 动态代理只需要实现 InvocationHandler 接口,重写 invoke 方法便可以完成代理的实现,
- JDK的代理是利用反射生成代理类 Proxyxx.class 代理类字节码,并生成对象
jdk动态代理之所以只能代理接口是因为代理类本身已经extends了Proxy,而java是不允许多重继承的,但是允许实现多个接口
- 优点:解决了静态代理中冗余的代理实现类问题。
- 缺点:JDK 动态代理是基于接口设计实现的,如果没有接口,会抛异常。
CGLIB 代理:
由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;
- CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。
实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。
但是CGLib在创建代理对象时所花费的时间却比JDK多得多
- 所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。
同时,由于CGLib由于是采用动态创建子类的方法 ,对于final方法,无法进行代理。
- 优点:没有接口也能实现动态代理,而且采用字节码增强技术,性能也不错。
- 缺点:技术实现相对难理解些
CGlib 对接口实现代理?
如果要对接口进行代理的话,可以使用enhancer.setSuperclass(UserService.class);
配置代码块。代码如下:
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import proxy.UserService;
import java.lang.reflect.Method;
/**
* 创建代理类的工厂 该类要实现 MethodInterceptor 接口。
* 该类中完成三样工作:
* (1)声明目标类的成员变量,并创建以目标类对象为参数的构造器。用于接收目标对象
* (2)定义代理的生成方法,用于创建代理对象。方法名是任意的。代理对象即目标类的子类
* (3)定义回调接口方法。对目标类的增强这在这里完成
*/
public class CGLibFactory implements MethodInterceptor {
// 声明目标类的成员变量
private UserService target;
public CGLibFactory(UserService target) {
this.target = target;
}
// 定义代理的生成方法,用于创建代理对象
public UserService myCGLibCreator() {
Enhancer enhancer = new Enhancer();
// 为代理对象设置父类,即指定目标类
enhancer.setSuperclass(UserService.class);
/**
* 设置回调接口对象 注意,只所以在setCallback()方法中可以写上this,
* 是因为MethodIntecepter接口继承自Callback,是其子接口
*/
enhancer.setCallback(this);
return (UserService) enhancer.create();// create用以生成CGLib代理对象
}
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("start invoke " + method.getName());
Object result = method.invoke(target, args);
System.out.println("end invoke " + method.getName());
return result;
}
}