简介:Java动态代理是一种在运行时创建代理类的机制,动态代理可以在不修改源代码的情况下,在运行时为某个接口动态生成实现类,并且可以拦截接口中的方法调用,从而实现一些特殊的功能。
动态代理在Java中有着广泛的应用,比如Spring AOP、Hibernate数据查询、测试框架的后端mock、RPC远程调用、Java注解对象获取、日志、用户鉴权、全局性异常处理、性能监控,甚至事务处理等。
分类:java中有两种常见的动态代理方式 JDK原生动态代理和CGLIB动态代理
在了解动态代理之前,我们一定要了解的一种设计模式,代理模式,什么是代理模式呢?
代理模式:给某一个对象提供一个代理,并由代理对象来控制对真实对象的访问。代理模式是一种结构型设计模式。
代理模式角色分为 3 种:
Subject(抽象主题角色):定义代理类和真实主题的公共对外方法,也是代理类代理真实主题的方法;RealSubject(真实主题角色):真正实现业务逻辑的类;Proxy(代理主题角色):用来代理和封装真实主题;
代理模式的结构比较简单,其核心是代理类,为了让客户端能够一致性地对待真实对象和代理对象,在代理模式中引入了抽象层。
代理模式按照职责(使用场景)来分类,至少可以分为以下几类:1、远程代理。 2、虚拟代理。 3、Copy-on-Write 代理。 4、保护(Protect or Access)代理。 5、Cache代理。 6、防火墙(Firewall)代理。 7、同步化(Synchronization)代理。 8、智能引用(Smart Reference)代理等等。
如果根据字节码的创建时机来分类,可以分为静态代理和动态代理:
所谓静态也就是在程序运行前就已经存在代理类的字节码文件,代理类和真实主题角色的关系在运行前就确定了。
而动态代理的源码是在程序运行期间由JVM根据反射等机制动态的生成,所以在运行前并不存在代理类的字节码文件
我们先通过实例来学习静态代理,然后理解静态代理的缺点,再来学习本文的主角:动态代理
编写一个接口 UserService ,以及该接口的一个实现类 UserServiceImpl
publicinterfaceUserService {
publicvoidselect();
publicvoidupdate();
}
publicclassUserServiceImplimplementsUserService {
publicvoidselect() {
System.out.println("查询 selectById");
}
publicvoidupdate() {
System.out.println("更新 update");
}
}
我们将通过静态代理对 UserServiceImpl 进行功能增强,在调用select和update之前记录一些日志。写一个代理类 UserServiceProxy,代理类需要实现 UserService
publicclassUserServiceProxyimplementsUserService {
privateUserServicetarget; // 被代理的对象
publicUserServiceProxy(UserServicetarget) {
this.target=target;
}
publicvoidselect() {
before();
target.select(); // 这里才实际调用真实主题角色的方法
after();
}
publicvoidupdate() {
before();
target.update(); // 这里才实际调用真实主题角色的方法
after();
}
privatevoidbefore() { // 在执行方法之前执行
System.out.println(String.format("log start time [%s] ", newDate()));
}
privatevoidafter() { // 在执行方法之后执行
System.out.println(String.format("log end time [%s] ", newDate()));
}
}
客户端测试
publicclassClient1 {
publicstaticvoidmain(String[] args) {
UserServiceuserServiceImpl=newUserServiceImpl();
UserServiceproxy=newUserServiceProxy(userServiceImpl);
proxy.select();
proxy.update();
}
}
结果:
log start time [Thu Dec 20 14:13:25 CST 2018]
查询 selectById
log end time [Thu Dec 20 14:13:25 CST 2018]
log start time [Thu Dec 20 14:13:25 CST 2018]
更新 update
log end time [Thu Dec 20 14:13:25 CST 2018]
通过静态代理,我们达到了功能增强的目的,而且没有侵入原代码,这是静态代理的一个优点。
静态代理的缺点虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。
1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
那么要如何改进这个问题呢
如果能让代理类自动生成,那么这个问题岂不是迎刃而解,所以,动态代理应景而生
JDK原生动态代理是通过实现java.lang.reflect.InvocationHandler接口和使用java.lang.reflect.Proxy类来创建代理对象,但是只能为接口创建代理
一下是一个简单的JDK原生代理的使用示例:
//定义一个接口
publicinterfaceHelloService {
voidsayHello(Stringname);
}
//定义一个实现类
publicclassHelloServiceImplimplementsHelloService {
@Override
publicvoidsayHello(Stringname) {
System.out.println("Hello "+name);
}
}
//定义一个调用处理器
publicclassMyInvocationHandlerimplementsInvocationHandler {
privateObjecttarget; //目标对象
publicMyInvocationHandler(Objecttarget) {
this.target=target;
}
@Override
publicObjectinvoke(Objectproxy, Methodmethod, Object[] args) throwsThrowable {
System.out.println("Before invoke"); //在调用前执行一些操作
Objectresult=method.invoke(target, args); //调用目标对象的方法
System.out.println("After invoke"); //在调用后执行一些操作
returnresult;
}
}
//测试代码
publicclassTest {
publicstaticvoidmain(String[] args) {
HelloServicehelloService=newHelloServiceImpl(); //创建目标对象
MyInvocationHandlerhandler=newMyInvocationHandler(helloService); //创建调用处理器
HelloServiceproxy= (HelloService) Proxy.newProxyInstance( //创建代理对象
helloService.getClass().getClassLoader(),//类的类加载器
helloService.getClass().getInterfaces(), //该方法返回该类实现的接口数组
handler);
proxy.sayHello("world"); //通过代理对象调用方法
}
}
输出结果:
Before invoke
Hello world
After invoke
这样就实现了在不修改原始代码的情况下,在sayHello方法前后添加了一些额外的操作。
上面提到,静态代理的缺点,最大的原因其实就是因为他在程序运行前就已经存在了,所以会导致类爆炸或者单个类内容爆炸的情况出现,那么如果可以在程序运行中也就是编译之后,动态生成代理类,那么事情岂不是就得到了解决,首先我们需要了解
这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:
通过一个类的全限定名来获取定义此类的二进制字节流
将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
从ZIP包获取,这是JAR、EAR、WAR等格式的基础
从网络中获取,典型的应用是 Applet
运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流
由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
从数据库中获取等等
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。
常见的字节码操作类库
Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。
其实看了上面的内容以后,相比你也明白,其实动态代理的底层原理,就是通过实时计算生成对应的二进制字节流从而达到一个动态生成代理类的效果,后面的我不说你也明白,和静态代理的操作是一样的了
JDK原生动态代理是通过java.lang.reflect.Proxy和java.lang.reflect.InvocationHandler来实现的
Proxy是所有动态代理的父类,它提供了一个静态方法newProxyInstance来创建代理对象
InvocationHandler是一个接口,它定义了一个invoke方法,用于处理代理对象的方法调用
JDK原生动态代理的实现原理是这样的:
拿到被代理对象的引用,然后获取他的接口
JDK代理重新生成一个类,同时实现我们给的代理对象所实现的接口
把被代理对象的引用拿到了
重新动态生成一个class字节码
然后编译
CGLIB动态代理是一种基于字节码增强的动态代理技术,它可以对任意类进行代理,不需要被代理的类实现接口。CGLIB动态代理的原理是通过ASM字节码框架生成目标类的子类,并覆盖其中方法实现增强。
CGLIB动态代理使用net.sf.cglib.proxy.MethodInterceptor接口作为拦截器,用于拦截目标类方法调用。
一下是一个简单的示例:
// 定义一个业务类,没有实现接口
publicclassUserService {
publicvoidsave() {
System.out.println("保存用户");
}
}
// 定义一个代理类,实现MethodInterceptor接口
publicclassUserProxyimplementsMethodInterceptor {
// 定义一个被代理的对象
privateObjecttarget;
// 创建代理对象的方法
publicObjectcreateProxy(Objecttarget) {
this.target=target;
// 创建Enhancer对象,用于创建代理类
Enhancerenhancer=newEnhancer();
// 设置被代理的类
enhancer.setSuperclass(this.target.getClass());
// 设置拦截器
enhancer.setCallback(this);
// 返回代理对象
returnenhancer.create();
}
// 拦截目标方法调用,并在前后添加增强逻辑
@Override
publicObjectintercept(Objectproxy, Methodmethod, Object[] args,
MethodProxymethodProxy) throwsThrowable {
System.out.println("开始事务");
// 调用目标方法
Objectresult=method.invoke(target, args);
System.out.println("提交事务");
returnresult;
}
}
// 测试类,创建代理对象并调用业务方法
publicclassTest {
publicstaticvoidmain(String[] args) {
UserServiceuserService=newUserService();
UserProxyuserProxy=newUserProxy();
UserServiceproxy= (UserService) userProxy.createProxy(userService);
proxy.save();
}
}
这段代码使用了net.sf.cglib.proxy.Enhancer类来创建代理对象,并设置了被代理的类和拦截器
当调用proxy.save()方法时,会触发拦截器中的intercept()方法,并在原始方法前后打印出"开始事务"和"提交事务"
上面的代码是不是看起来和静态代理十分类似,实则完全不同,这里的代理类可以说是针对的不是代理目标类,而是针对的是你也业务需求,可以新建多个代理类来针对不同业务,根据业务的不同使用不同的代理类,而这个代理类可以写入任何service
FastClass机制是一种优化CGLIB动态代理调用效率的技术,它的原理是为代理类和被代理类各生成一个FastClass类,这个类会为代理类或被代理类的方法分配一个索引,然后通过索引来直接调用相应的方法,避免了反射调用
下面是一个简单的例子来叫你如何使用FastClass机制:
publicclassFastClassTest {
publicstaticvoidmain(String[] args) {
// 创建委托类实例
HelloServicehelloService=newHelloServiceImpl();
// 创建委托类的FastClass对象
FastClassfastClass=FastClass.create(HelloServiceImpl.class);
// 获取委托类sayHello方法的索引
intindex=fastClass.getIndex("sayHello", newClass[]{String.class});
try {
// 通过索引直接调用委托类方法
fastClass.invoke(index, helloService, newObject[]{"cglib"});
} catch (InvocationTargetExceptione) {
e.printStackTrace();
}
}
}
由于 JDK 动态代理限制了只能基于接口设计,而对于没有接口的情况,JDK方式解决不了;CGLib 采用了非常底层的字节码技术,其原理是通过字节码技术为一个类创建子类,并在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑,来完成动态代理的实现。实现方式实现 MethodInterceptor 接口,重写 intercept 方法,通过 Enhancer 类的回调方法来实现。但是CGLib在创建代理对象时所花费的时间却比JDK多得多,所以对于单例的对象,因为无需频繁创建对象,用CGLib合适,反之,使用JDK方式要更为合适一些。 同时,由于CGLib由于是采用动态创建子类的方法,对于final方法,无法进行代理。
提一嘴,有没有发现这个技术和spring很搭
CGLIB 创建动态代理类的模式是:
查找目标类上的所有非final 的public类型的方法定义;
将这些方法的定义转换成字节码;
将组成的字节码转换成相应的代理的class对象;
实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求
JDK原生动态代理是面向接口的,也就是说被代理的类必须实现一个或多个接口,CGLIB动态代理是通过字节码底层继承要代理的类来实现,因此如果被代理的类被final关键字所修饰,会失败
JDK原生动态代理是通过反射机制生成一个实现了被代理对象接口的新类。CGLIB动态代理是通过ASM字节码框架生成目标类的子类
JDK原生动态代理使用java.lang.reflect.InvocationHandler接口作为拦截器,用于处理代理对象的方法调用。CGLIB动态代理使用net.sf.cglib.proxy.MethodInterceptor接口作为拦截器,用于拦截目标类方法调用
JDK动态代理:基于Java反射机制实现,必须要实现了接口的业务类才能用这种办法生成代理对象。
cglib动态代理:基于ASM机制实现,通过生成业务类的子类作为代理类。
JDK Proxy 的优势:
最小化依赖关系,减少依赖意味着简化开发和维护,JDK 本身的支持,可能比 cglib 更加可靠。
平滑进行 JDK 版本升级,而字节码类库通常需要进行更新以保证在新版 Java 上能够使用。
代码实现简单。
基于类似 cglib 框架的优势:
无需实现接口,达到代理类无侵入
只操作我们关心的类,而不必为其他相关类增加工作量。
高性能