全文介绍了JAVA动态代理的相关内容,如有错误敬请指正。
一.从常规编码方式说起
在介绍动态代理之前,让我们先回顾一下一种常见的编码方式:
1.首先定义一个接口,接口定义一个act方法
public interface ActService {
//表演
void act(String content);
}
2.编写实现类
public class ActServiceImpl implements ActService {
@Override
public void act(String content) {
System.out.println("表演的内容为:" + content);
}
3.创建该类的实现,并转型为接口。
public static void main(String[] args) {
ActService actService = new ActServiceImpl();
actService.act("霸王别姬");
}
二.什么是代理模式?
代理模式(Proxy Pattern)是23种设计模式之一,简单的说代理模式就是通过使用代理对象来替代对真实对象的访问,这样就可以在不修改原真实对象的前提下,提供额外的功能操作,扩展真实对象的功能。
代理模式分为三种角色:
- Real Subject:顾名思义真实类,也就是被代理的类,或者说是委托类。用来完成真正的业务操作
- Proxy :代理类,将自身的请求用Real Subject 的功能来实现,代理类并不真正去实现业务功能,而是对其进行增强。
- Subject:定义Real Subject 和Proxy都应该实现的接口。
代理模式的主要作用是扩展真实对象的功能,比如在真实对象执行前加上日志打印记录,并且不需要修改真实对象的代码
以文章标题一下的代码为例,比如演员都有一个技能表演,但是演员一般都比较忙,所以会委派一个经纪人来负责对接一些商务活动。
在这个例子中:公共接口Subject中就包括一个 方法 act()表演,Real Subject就是演员,Proxy 就是经纪人.
假设一个影视公司想找演员拍戏,公司无法直接访问到演员,只能访问经纪人这个代理对象,代理对象和公司进行商务谈判并且签好合同后,才会将真正的业务内容(表演)交给演员去完成。 在这个过程中,经纪人并不会真正去实现 act()表演,而是 实现了谈判,签合同等行为来对演员进行了功能增强。
那么为什么委托类和代理类都需要实现相同的接口?
那是为了保持行为的一致性,在访问者看来两者之间就没有区别。这样,通过代理类这个中间层,很好地隐藏和保护了委托类对象,能有效屏蔽外界对委托类对象的直接访问。同时,也可以在代理类上加上额外的操作,比如经纪人会进行商务谈判并签订合同,这就实现了委托类的功能增强。
三.java字节码生成原理
在探讨java的代理实现前,有必要先说一下java的 .class 字节码生成原理。
这种图片概述了字节码的生成,加载,解析的过程。
一个 Class 类对应一个 .class 字节码文件,也就是说字节码文件中存储了一个类的全部信息。字节码其实是二进制文件,内容是只有 JVM 能够识别的机器码。
解析过程这样的:JVM 读取 .class 字节码文件,取出二进制数据,加载到内存中,解析字节码文件内的信息,生成对应的 Class 类对象,显然,上述这个过程是在编译期就发生的。
那么,由于JVM 是通过 .class 字节码文件(也就是二进制信息)加载类的,如果我们在运行期通过某种方式,遵循 Java 编译系统组织 .class 字节码文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类。这样,我们不就完成了在运行时动态的创建一个类。这个思想其实也就是动态代理的思想。
在运行时期按照 JVM 规范对 .class 字节码文件的组织规则,生成对应的二进制数据。当前有很多开源框架可以完成这个功能,如:
- CGLIB
- Javassist
总的来说CGLIB 比 Javassist 快得多,并且提供了更好的性能,但是 Javassist 相对来说更容易使用,两者各有千秋。
四.静态代理
什么是静态代理?
从 JVM 层面来说, 静态代理(Static Proxy)在编译时就将接口、委托类、代理类这些都变成了一个个实际的 .class 文件。
静态代理实现步骤
1.定义一个接口(Subject)
2.创建一个委托类(Real Subject)实现这个接口
3.创建一个代理类(Proxy) 实现这个接口
4.在代理类(Proxy) 中注入委托类(Real Subject),在代理类中访问委托类的具体实现。这样我们就可以屏蔽外部对委托类的直接访问,并且可以在委托类的方法调用前后做一些事情来实现对代理类的增强。
新增一个静态代理类:
public class StaticProxyActServiceImpl implements ActService {
//注入委托类
private final ActService actService;
public StaticProxyActServiceImpl(ActService actService) {
this.actService = actService;
}
@Override
public void act(String content) {
//调用委托类方法前
System.out.println("签订演出合同");
actService.act(content);
//调用委托类方法后
System.out.println("收取演出费用");
}
}
打印结果
签订演出合同
表演的内容为:霸王别姬
收取演出费用
从上面的代码可以看出静态代理是一个委托类对应一个代理类,如果新增若干个委托类就需要新增若干个代理类,而且一旦接口的方法或者代理类的增强行为发生变更。所有的代理类都需要同步进行修改。
所有,能不能将代理类做成一个通用的呢?
动态代理应运而生。
五.动态代理
上面介绍了Java 字节码生成原理,现在来介绍动态代理(Dynamic Proxy)。
由静态代理可以看出,代理类无非是在委托类方法执行的前后做了一些自己的操作,委托类的不同导致代理类的不同。
那么为了做一个通用性的代理类出来,我们把调用委托类方法的这个动作抽取出来,把它封装成一个通用性的处理类,于是就有了动态代理中的 InvocationHandler 角色(处理类)。
于是,在代理类和委托类之间就多了一个处理类的角色,这个角色主要是对代理类调用委托类方法的这个动作进行统一的调用,也就是由 InvocationHandler 来统一处理代理类调用委托类方法这个操作。看下图:
从 JVM 角度来说,动态代理是在运行时动态生成 .class 字节码文件 ,并加载到 JVM 中的。这个我们在 Java 字节码生成框架中已经提到过。
虽然动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助,Spring AOP、RPC、MyBatis 等框架的实现都依赖了动态代理。
就 Java 来说,动态代理的实现方式有很多种,比如:
JDK 动态代理
CGLIB 动态代理
Javassit 动态代理
下面详细讲解这三种动态代理机制。
六.JDK动态代理
JDK动态代理实现步骤
1.定义一个接口(Subject)
2.创建一个委托类(Real Subject)实现这个接口
3.创建一个处理类,实现InvocationHandler接口,重写其invoke方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些处理逻辑),并将委托类注入处理类。
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
该方法有下面三个参数:
proxy:代理类对象(见下一步)
method:我们可以通过它来调用委托类的方法(反射)
args:传给委托类方法的参数列表
4.创建一个代理类(Proxy),通过 Proxy.newProxyInstance() 创建委托类对象的代理对象
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h) {
Objects.requireNonNull(h);
final Class<?> caller = System.getSecurityManager() == null
? null
: Reflection.getCallerClass();
/*
* Look up or generate the designated proxy class and its constructor.
*/
Constructor<?> cons = getProxyConstructor(caller, loader, interfaces);
return newProxyInstance(caller, cons, h);
}
这个方法需要 3 个参数:
类加载器 ClassLoader
委托类实现的接口数组,至少需要传入一个接口进去
调用的 InvocationHandler 接口的实例(也就是第 3 步我们创建的类的实例)
也就是说:
我们在通过 Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现了 InvocationHandler 接口的处理类的 invoke()方法,可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。
代码测试:
步骤3.创建一个处理类并注入委托类
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class JDKDynamicInvocationHandler implements InvocationHandler {
//委托类注入到处理类中(用Object代替,方便扩展)
private final Object target;
public JDKDynamicInvocationHandler(Object target) {
this.target = target;
}
//重写invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//调用委托类方法之前,添加操作
System.out.println(" do something before method invoke:" + method.getName());
Object result = method.invoke(target, args);
//调用委托类方法之后,添加操作
System.out.println(" do something after method invoke:" + method.getName());
return result;
}
}
步骤4.创建一个代理工厂类,生产代理对象
import java.lang.reflect.Proxy;
public class JDKDynamicProxyFactory {
public static Object getProxy(Object target) {
Object proxyInstance = Proxy.newProxyInstance
(target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new JDKDynamicInvocationHandler(target));
return proxyInstance;
}
}
测试:
ActService actService= (ActService) JDKDynamicProxyFactory.getProxy(new ActServiceImpl());
actService.act("霸王别姬");
打印日志:
do something before method invoke:act
表演的内容为:霸王别姬
do something after method invoke:act
七.CGLIB动态代理
回顾一下JDK动态代理的内容:
通过JDK动态代理生成的代理类必然是以下的结构,继承了Proxy 且实现了传入的接口。
public class $Proxy1 extends Proxy implements 传入的接口{
}
JDK动态代理的原理是根据定义好的规则,用传入的接口创建一个新类,这就是为什么采用JDK动态代理时为什么只能用接口引用指向代理,而不能用传入的类引用执行动态类。
JDK动态代理有一个非常致命的缺陷是它只能代理实现了某个接口(Subject)的实现类(Real Subject),并且代理类只能代理接口中定义的方法,如果实现类中有自己私有的方法,则代理类无法调用该方法。
为了解决上述的这个问题,CGLIB动态代理应运而生。
CGLIB(Code Generation Library)是一个基于 ASM 的 Java 字节码生成框架,它允许我们在运行时对字节码进行修改和动态生成。原理就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,纺入额外的业务逻辑。
CGLIB引入了一个新的角色,方法拦截器(MethodInterceptor)。和JDK动态代理中的InvocationHandler类似,也是用来实现方法的统一调用的。如下图所示:
另外由于 CGLIB 采用继承的方式,所以被代理的类不能被 ==final ==修饰。
很多知名的开源框架都使用到了 CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。
CGLIB动态代理的使用步骤:
1)创建一个委托类(Real Subject)
2)创建一个方法拦截器实现接口 MethodInterceptor,并重写interceptor方法。intercept 用于拦截并增强委托类的方法(和 JDK 动态代理 InvocationHandler 中的 invoke 方法类似)
public interface MethodInterceptor
extends Callback
{
public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args,
MethodProxy proxy) throws Throwable;
}
该方法有4个参数:
Object obj:委托类对象
Method method:被拦截的方法(委托类中需要增强的方法)
Object[] args:方法入参
MethodProxy proxy:用于调用委托类的原始方法(底层也是通过反射机制,不过不是 Method.invoke 了,而是使用 MethodProxy.invokeSuper 方法)
3)创建代理对象(Proxy):通过 Enhancer.create() 创建委托类对象的代理对象
public Object create() {
classOnly = false;
argumentTypes = null;
return createHelper();
}
public Object create(Class[] argumentTypes, Object[] arguments) {
classOnly = false;
if (argumentTypes == null || arguments == null || argumentTypes.length != arguments.length) {
throw new IllegalArgumentException("Arguments must be non-null and of equal length");
}
this.argumentTypes = argumentTypes;
this.arguments = arguments;
return createHelper();
}
也就是说:我们在通过Enhancer.create() 创建的代理对象在调用方法时,实际会调用到MethodInterceptor接口的处理类 intercept()方法,可以在 intercept()方法中自定义处理逻辑,比如在委托类方法执行前后做什么事情。
可以发现,CGLIB 动态代理机制和 JDK 动态代理机制的步骤差不多,CGLIB 动态代理的核心是方法拦截器 MethodInterceptor 和 Enhancer,而 JDK 动态代理的核心是处理类 InvocationHandler 和 Proxy。
代码测试:
首先添加CGLIB的pom文件配置
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
继续以ActServiceImpl 为例
1)创建一个方法拦截器实现接口 MethodInterceptor,并重写 intercept 方法
public class CGLIBDynamicMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
//调用委托类方法之前,添加操作
System.out.println(" do something before method invoke:" + method.getName());
Object result = proxy.invokeSuper(obj, args);
//调用委托类方法之后,添加操作
System.out.println(" do something after method invoke:" + method.getName());
return result;
}
}
2)创建代理对象(Proxy):通过 Enhancer.create() 创建委托类对象的代理对象
public class CGLIBProxyFactory {
public static Object getProxy(Class<?> clazz) {
//创建动态代理增强类
Enhancer enhancer = new Enhancer();
//设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
//设置委托类(父类)
enhancer.setSuperclass(clazz);
//设置方法拦截器
enhancer.setCallback(new CGLIBDynamicMethodInterceptor());
//创建代理类
Object o = enhancer.create();
return o;
}
}
从 setSuperclass 我们就能看出,为什么说 CGLIB 是基于继承的。
3)代码测试:
//CGLIB动态代理
ActServiceImpl actService = (ActServiceImpl) CGLIBProxyFactory.getProxy(ActServiceImpl.class);
actService.act("霸王别姬");
do something before method invoke:act
表演的内容为:霸王别姬
do something after method invoke:act
八、动态代理比对
JDK动态代理和CGLIB动态代理比对
1)JDK 动态代理是基于实现了接口的委托类,通过接口实现代理;而 CGLIB 动态代理是基于继承了委托类的子类,通过子类实现代理。
2)JDK 动态代理只能代理实现了接口的类,且只能增强接口中现有的方法;而 CGLIB 可以代理未实现任何接口的类。
3)就二者的效率来说,大部分情况都是 JDK 动态代理的效率更高,随着 JDK 版本的升级,这个优势更加明显。
4)提一嘴,常见的还有 Javassist 动态代理机制。和 CGLIB 一样,作为一个 Java 字节码生成框架,Javassist 天生就拥有在运行时动态创建一个类的能力,实现动态代理自然不在话下。 Dubbo 就是默认使用 Javassit 来进行动态代理的。
九、什么情况下使用动态代理
1)设计模式中有一个开闭原则,即对修改关闭,对扩展开放。我们在工作中有时会接手很多前人的代码,里面代码逻辑让人摸不着头脑,就很难去下手修改代码,那么这时我们就可以通过代理对类进行增强。
2)我们在使用 RPC 框架的时候,框架本身并不能提前知道各个业务方要调用哪些接口的哪些方法 。那么这个时候,就可以通过动态代理的方式来建立一个中间人给客户端使用,也方便框架进行搭建逻辑,某种程度上也是客户端代码和框架松耦合的一种表现。
3)Spring 的 AOP 机制同样也是采用了动态代理,此处不做详细讨论。
十.总结
全部捋一遍下来还是收获蛮多的,我感觉只要理解了字节码在编译期生成还是在运行期生成,就差不多能够把握住静态代理和动态代理了。总结一下静态代理和动态代理中的角色:
静态代理:
Subject:公共接口
Real Subject:委托类
Proxy:代理类
JDK 动态代理:
Subject:公共接口
Real Subject:委托类
Proxy:代理类
InvocationHandler:处理类,统一调用方法
CGLIB 动态代理:
Subject:公共接口
Real Subject:委托类
Proxy:代理类
MethodInterceptor:方法拦截器,统一调用方法