代理是什么?动态代理又是什么?动态代理中是如何使用反射的?全文脉络思维导图如下:
20210512165153.png

1.常规编码方式

在了解代理之前,先回顾下我们常规的编码方式:所有 interface 接口总是通过向上转型并指向某个实例的。
1)首先定义一个接口:

  1. public interface SmsService {
  2. boolean send (String msg);
  3. }

2)然后编写其实现类:

  1. public class SmsServiceImpl implements SmsService{
  2. @Override
  3. public boolean send(String msg) {
  4. System.out.println("send msg:"+ msg);
  5. return true;
  6. }
  7. }

3)最后创建该实现类实例,转型为接口并调用:

  1. SmsService smsService = new SmsServiceImpl();
  2. smsService.send("消息");

上述方式是我们通常的编码方式。而代理模式和这种方式有很大的区别,请看下文。

2.代理模式概述

简单来说,代理模式就是 使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能
代理模式大致有三种角色:

  • Real Subject:真实类,也就是被代理类、委托类。用来真正完成业务服务功能;
  • Proxy:代理类,将自身的请求用Real Subject 对应的功能来实现,代理类对象并不真正的去实现其业务功能;
  • Subject:定义Real Subject 和 Proxy 角色都应该实现的接口。

20210512172858.png
代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些额外的操作,并且不用修改这个方法的原有代码。spring AOP就是可以很好的体现这句话。
举个例子:我找小红帮我向小绿问句话,小红可以看做是代理我的代理类Proxy,而我就是Real Subject,因为小红要传达的话就是我说的,那么我和小红都需要实现的接口(Subject)就是说话,在外界看来我俩都是一样的。
20210512173938.jpg
这就是为什么委托类和代理类都需要实现相同的接口,为了保持行为的一致性,在访问者看来两者没有区别。通过代理类这个中间层,很好的隐藏和保护了委托类对象,能有效屏蔽外界对委托类对象的直接访问。同时,也可以在代理类上加上一些额外的操作,比如小红在说话前会唱首歌,外界会以为你在说过前会唱首歌,所以,这就实现了委托类的功能增强。
代理模式有静态代理和动态代理两种实现方式。

3.静态代理

先看下静态代理的实现步骤:
1)定义一个接口(Subject)
2)创建一个委托类(Real Subject)实现这个接口
2)创建一个代理类(Proxy)同样实现这个接口
4)将委托类 Real Subject 注入进代理类Proxy,在代理类的方法中调用 Real Subject 中对应的方法。这样就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些其他操作。

从实现和应用的角度来说,静态代理中,我们对目标对象的每个方法的增强都是手动完成的,非常不灵活(一旦接口新增方法,目标对象和代理对象都要进行修改)而且麻烦(每一个目标类都要写一个与之对应的代理类)。实际应用场景非常少,日常开发几乎不使用静态代理。

代码示例
1)定义发送短信接口

  1. public interface SmsService {
  2. boolean send (String msg);
  3. }

2)创建一个委托类(Real Subject)来实现这个接口

  1. public class SmsServiceImpl implements SmsService{
  2. @Override
  3. public boolean send(String msg) {
  4. System.out.println("send msg:"+ msg);
  5. return true;
  6. }
  7. }

3)创建一个代理类(Proxy)同样实现这个接口
4)将委托类 Real Subject 注入进代理类 Proxy,在代理类的方法中调用 Real Subject 中的对应方法。这样的话,可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些其他的事情。

  1. public class SmsProxy implements SmsService {
  2. private final SmsService smsService;
  3. public SmsProxy(SmsService smsService){
  4. this.smsService = smsService;
  5. }
  6. @Override
  7. public boolean send(String msg) {
  8. System.out.println("before method send()");
  9. smsService.send(msg);
  10. System.out.println("after method send()");
  11. return false;
  12. }
  13. public static void main(String[] args) {
  14. SmsService smsService = new SmsServiceImpl();
  15. SmsProxy smsProxy = new SmsProxy(smsService);
  16. smsProxy.send("消息");
  17. }
  18. }

控制台输出:

  1. before method send()
  2. send msg:消息
  3. after method send()

通过输出结果可以看出,已经增强了委托类 SmsServiceImpl 的 send() 。
从上述代码可以看出,静态代理存在一定的弊端,假如我们现在新增一个委托类实现 SmsService 接口,如果我们想要对这个委托类进行增强,就需要重新写一个代理类,然后注入这个新的委托类,非常不灵活。静态代理是一个委托了对应一个代理类,能不能将代理类做成一个通用的呢?为此,动态代理出现了。

4.java字节码生成框架

讲解动态代理之前,先说下 .class 字节码文件,动态代理机制和 java字节码生成框架息息相关。
一个 Class 类对应一个 .class 字节码文件,也就是说字节码文件中存储了一个类的全部信息。字节码其实是二进制文件,内容是只有 JVM 能够识别的机器码。
解析过程是这样的:JVM 读取 .class 字节码文件,取出二进制数据,加载到内存中,解析字节码文件中的信息,生成对应的 Class 类对象:
20210513112013.png上述过程是在编译期就发生的。
那么,由于 JVM 是通过 .class 字节码文件加载类的,如果我们在运行期遵守 java 编译系统组织 .class 字节码文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类,这样,我们不就完成了在运行是动态的创建一个类。这个思想其实就是动态代理的思想。
20210513112549.jpg
在运行期按照 JVM 规范对.class 字节码文件的组织规则,生成对应的二进制数据,当前有很多开源框架可以完成这个功能,比如

  • ASM
  • CGLIB
  • Javassist
  • . . . . . .

需要注意的是,CGLIB是基于 ASM的。这里简单对比下 ASM 和 Javassist:

  • Javassist 源代码级API 比 ASM 中实际的字节码操作更容易使用。
  • Javassist 在复杂的字节码级操作上提供了更高级别的抽象层。

    1. Javassist 源代码级API 只需要很少的字节码知识,所以实现起来更容易,更快。
  • Javassist 使用反射机制,这使得它比 ASM 慢。

总的来说 ASM 比 Javassist 快得多,并且提供了更好的性能,但是 Javassist 相对来说更容易使用。
Javassist代码示例
正常来说,我们创建一个类的代码是这样的:

  1. public class Programmer {
  2. public void code(){
  3. System.out.println("I'm a Programmer,Just Coding.....");
  4. }
  5. }

下面通过Javassist创建一个JavassistProgrammer 类的字节码文件

  1. import org.apache.ibatis.javassist.ClassPool;
  2. import org.apache.ibatis.javassist.CtClass;
  3. import org.apache.ibatis.javassist.CtMethod;
  4. import org.apache.ibatis.javassist.CtNewMethod;
  5. public class Generator {
  6. public static void main(String[] args) throws Exception {
  7. ClassPool pool = ClassPool.getDefault();
  8. //创建Programmer 类,
  9. CtClass ctClass = pool.makeClass("com.lee.JavassistProgrammer");
  10. //定义方法
  11. CtMethod method = CtNewMethod.make("public void code(){}" ,ctClass);
  12. //插入方法代码
  13. method.insertBefore("System.out.println(\"coding ...\" );");
  14. ctClass.addMethod(method);
  15. ctClass.writeFile("d://");
  16. }
  17. }

通过反编译查看生成的文件
image.png

5.什么是动态代理

了解了Java 字节码生成框架,现在学习下动态代理(Dynamic Proxy)
先回顾下静态代理,我们把静态代理的执行过程抽象为下图:
20210513135301.png
可以看出,代理类实际是在调用委托类方法前后增加了一些操作,委托类不同,代理类也会随之不同。

那么为了做一个通用的代理类出来,我们把调用委托类方法的这个动作抽取出来,把它封装成一个通用性的处理类,于是就有了动态代理类中的 InvocationHandler 角色(处理类)。这个角色主要是 对代理类调用委托类方法的这个动作进行统一的调用,也就是由 InvocationHandler 来统一处理代理类调用委托类方法这个操作。
20210513140330.png
从 JVM 的角度来说,动态代理是在运行时动态生成 .class 字节码文件,并加载到 JVM 中的。

虽然动态代理在我们日常开发中使用相对较少,但是在框架中几乎是必用的一门技术。 spring AOP、RPC的框架的实现都使用了动态代理

就 java而言,动态代理的实现方式有很多种,比如:

  • JDK 动态代理
  • CGLIB 动态代理
  • Javassist 动态代理
  • . . . . . .

6.JDK动态代理机制

先来看下 JDK 动态代理的使用步骤:
1)定义一个接口(Subject)
2)创建一个委托类(Real Subject)实现这个接口
3)创建一个处理类并实现 InvocationHandler 接口,重写其 invoke 方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些其他处理逻辑),并将委托类注入处理类中
image.png
该方法的参数:

  • proxy:代理类对象
  • method:用来调用委托类的方法
  • args:传给委托类方法的参数

4)创建代理对象(Proxy):通过 Proxy.newProxyInstance() 创建委托类对象的代理对象
image.png
这个方法的三个参数:

  • 委托类的类加载器 ClassLoader
  • 委托类实现的接口数组,至少需要传入一个接口进去
  • InvocationHandler 处理类实例

代码示例
1)定义一个接口(Subject)

  1. public interface SmsService {
  2. boolean send (String msg);
  3. }

2)创建一个委托类(Real Subject)实现这个接口

  1. public class SmsServiceImpl implements SmsService{
  2. @Override
  3. public boolean send(String msg) {
  4. System.out.println("send msg:"+ msg);
  5. return true;
  6. }
  7. }

3)创建一个处理类并实现 InvocationHandler 接口,重写其 invoke 方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些其他处理逻辑),并将委托类注入处理类中

  1. import java.lang.reflect.InvocationHandler;
  2. import java.lang.reflect.Method;
  3. public class JdkInvocationHandler implements InvocationHandler{
  4. // 将委托类注入处理类(使用Object 方便扩展)
  5. private final Object target;
  6. public JdkInvocationHandler (Object target){
  7. this.target = target;
  8. }
  9. @Override
  10. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  11. // 方法调用前处理逻辑
  12. System.out.println("before method " + method.getName());
  13. // 方法调用
  14. Object result = method.invoke(target,args);
  15. // 方法调用后处理逻辑
  16. System.out.println("after method " + method.getName());
  17. return result;
  18. }
  19. }

4)创建代理对象(Proxy):通过 Proxy.newProxyInstance() 创建委托类对象的代理对象

  1. import com.lee.proxy.JdkInvocationHandler;
  2. import java.lang.reflect.Proxy;
  3. public class JdkProxyFactory {
  4. public static Object getProxy (Object tatget){
  5. return Proxy.newProxyInstance(tatget.getClass().getClassLoader(),
  6. tatget.getClass().getInterfaces(),new JdkInvocationHandler(tatget));
  7. }
  8. }

5)使用和输出

  1. public static void main(String[] args) {
  2. SmsService smsService = (SmsService)JdkProxyFactory.getProxy(new SmsServiceImpl());
  3. smsService.send("消息");
  4. }

image.png

7.CGLIB 动态代理机制

JDK 动态代理有一个很严重的问题是它只能代理实现了某个接口的实现类,并且代理类也只能代理接口中实现的方法,要是实现类中有自己私有的方法,而接口中没有的话,该方法不能进行代理调用。

为了解决这个问题,我们可以使用 CGLIB 动态代理机制。
上文提到 CGLIB(Code Generation Library)是一种基于 ASM 的 Java 字节码生成框架,它允许我们在运行时对字节码进行修改和动态生成。原理是 通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,织入额外的业务逻辑。 关键词:拦截。CGLIB 引入一个新的角色就是 方法拦截器 __MethodInterceptor 。和 JDK 中的处理类 __InvocationHandler 差不多,也是用来实现方法的统一调用的。
20210513151241.png
由于 CGLIB 采用 继承 的方式,所以被代理的类不能被 final 修改。
很多知名的开源框架都使用到了 CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

来看看 CGLIB 动态代理使用步骤
1)首先创建一个委托类(Real Subject)
2)创建一个方法拦截器实现接口 MethodInterceptor 并重写 intercept 方法。 用于拦截并增强委托类的方法(和 JDK 动态代理中的 InvocationHandler 中的 invoke 方法类似)。
image.png方法的四个参数:

  • Object var1:委托类对象
  • Method var2:被拦截的方法(委托类中需要增强的方法)
  • Object[] var3:方法入参
  • MethodProxy var4:方法拦截器,用于调用委托类的原始方法(底层也是通过反射,不过不是 Method.invoke ,而是 MethodProxy.invokeSuper 方法)。

3)创建代理对象 ( Proxy ):通过 Enhancer.create() 创建委托类的代理对象
代码示例:
JDK 动态代理不需要额外的依赖,CGLIB 是一个开源项目,如果使用的话,需要手动添加依赖。
1)收现创建一个委托类(Real Subject)

  1. public class AliSmsService {
  2. public void send (String msg) {
  3. System.out.println("send msg :" + msg );
  4. }
  5. }

2)创建一个方法拦截器实现接口 MethodInterceptor 并重写 intercept 方法

  1. public class CglibMethodInterceptor implements MethodInterceptor{
  2. @Override
  3. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
  4. // 方法调用前处理逻辑
  5. System.out.println("before method " + method.getName());
  6. // 方法调用
  7. Object result = methodProxy.invokeSuper(o , objects);
  8. // 方法调用后处理逻辑
  9. System.out.println("after method " + method.getName());
  10. return result;
  11. }
  12. }

3)创建代理对象 ( Proxy ):通过 Enhancer.create() 创建委托类的代理对象

  1. public class CglibProxyFactory {
  2. public static Object getProxy (Class<?> clazz){
  3. // 创建动态代理增强类
  4. Enhancer enhancer = new Enhancer();
  5. // 设置类加载器
  6. enhancer.setClassLoader(clazz.getClassLoader());
  7. // 设置委托类(设置父类)
  8. enhancer.setSuperclass(clazz);
  9. // 设置方法拦截器
  10. enhancer.setCallback(new CglibMethodInterceptor());
  11. // 创建代理类
  12. return enhancer.create();
  13. }
  14. }

setSuperclass 我们就能看出,为什么说 CGLIB 是基于继承的。
4)使用和打印

  1. AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
  2. aliSmsService.send("cglib");

image.png

JDK 动态代理和 CGLIB 动态代理对比
1)JDK 动态代理是基于实现了接口的委托类,通过接口实现代理;而 CGLIB 动态代理是基于集成了委托类的子类,通过子类实现代理。
2)JDK 动态代理只能代理实现了接口的类,且只能增强接口中现有的方法;而 CGLIB 可以代理未实现任何接口的类。
3)就二者的效率来说,大部分都是 JDK 动态代理的效率更高。

8.什么情况下使用动态代理

1)设计模式中有一个设计原则是 开闭原则,即对修改关闭,对扩展开放,我们在工作中有时会接手很多前人的代码,里面代码逻辑让人摸不着头脑,就很难去下手修改代码,那么这时我们就可以通过代理对类进行增强。

2)我们在使用 RPC 框架 的时候,框架本身并不能提前知道各个业务方要调用哪些接口的哪些方法 。那么这个时候,就可用通过动态代理的方式来建立一个中间人给客户端使用,也方便框架进行搭建逻辑,某种程度上也是客户端代码和框架松耦合的一种表现。

9.静态代理和动态代理的对比

1)灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增方法,目标对象和代理对象都要修改,比较麻烦。
2)JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成一个个实际的 .class 字节码文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM中的。

10.总结

只要理解了字节码在编译期生成还是在运行期生成,就差不多能够把握住静态代理和动态代理了。总结一下静态代理和动态代理中的角色:
静态代理:

  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类

JDK 动态代理:

  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类
  • InvocationHandler :处理类,统一调用方法

CGLIB 动态代理:

  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类
  • MethodInterceptor :方法拦截器,统一调用方法