写点什么

精彩推荐 |【Java 技术专题】「重塑技术功底」攻破 Java 技术盲点之剖析动态代理的实现原理和开发指南(中)

作者:洛神灬殇
  • 2024-01-15
    江苏
  • 本文字数:4841 字

    阅读完需:约 16 分钟

精彩推荐 |【Java技术专题】「重塑技术功底」攻破Java技术盲点之剖析动态代理的实现原理和开发指南(中)

前提介绍

经历了上一篇文章内容:《精彩推荐 |【Java 技术专题】「重塑技术功底」攻破 Java 技术盲点之剖析动态代理的实现原理和开发指南(上)》,相信您对于 Java 原生的动态代理技术应该有了一定的认识和了解了,那么我们先来回顾一下对应的技术要点,看看您是否真正的认识了对应的技术原理了?



技术回顾

要回顾 Java 动态代理,需要考虑以下几个关键点:



  • 动态代理的工作原理:动态代理通过在运行时动态创建代理对象来实现对目标对象的增强或修改。代理对象会拦截目标对象的所有方法调用,并在调用前后执行特定的逻辑。

  • 接口的重要性:动态代理要求目标对象和代理对象都实现一个或多个相同的接口。这些接口定义了代理对象和目标对象可以调用的方法。通过这种方式,客户端代码可以像调用普通接口一样调用代理对象,而无需关心其实现细节。

  • 实现动态代理的步骤:创建目标对象实现的所有接口的类,作为代理类的基础;在代理类中,使用Proxy.newProxyInstance()方法动态创建代理对象;在代理类中,重写目标对象实现的所有接口的方法,以便在调用这些方法时执行自定义逻辑。

  • 代理模式的应用场景:动态代理适用于需要对目标对象进行增强或修改的场景。例如,在不修改目标对象的源代码的情况下,为对象添加日志记录、性能监控、事务处理等功能。


注意:使用动态代理时,需要确保目标对象和代理对象都实现了相同的接口,否则客户端代码将无法正确调用代理对象。另外,动态代理不适用于非接口方法,因为 Java 不支持多重继承,代理对象只能继承自一个类

回顾问题分析

代理对象实现了什么接口

目标对象实现的接口即为所实现的接口,这与静态代理模式中代理对象实现的接口是相同的。此外,代理对象和目标对象都共有一个共同的接口,即它们都继承自同一个接口。因此,Proxy.newProxyInstance()方法返回的类型就是该接口类型。

代理对象的方法体是什么

代理对象的方法体是拦截器中 invoke 方法的实现,用于拦截并处理代理对象的所有逻辑,控制是否执行目标对象的目标方法。这个方法通常包括对请求的处理、对目标方法的调用以及对结果的返回等操作。


通过代理对象的方法体,可以实现方法级别的拦截和过滤,以及对目标方法的调用进行扩展和增强。同时,代理对象的方法体还可以用于实现事务管理、日志记录、安全控制等高级功能。




Java 动态代理为我们提供了非常灵活的代理机制,但 Java 动态代理是基于接口的,如果对象没有实现接口我们该如何代理呢

CGLIB 动态代理

CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,它具备强大的运行时字节码修改和动态生成能力。

CGLIB 的原理

CGLIB 需要指定父类和回调方法。当然 cglib 也可以与 Java 动态代理一样面向接口,因为本质是继承


CGLIB 通过继承方式实现代理,能够动态地生成子类,覆盖并重写目标对象的方法,这种能力使得 CGLIB 成为 Spring 框架中实现 AOP(面向切面编程)的关键组件。

继承方式

这种动态生成和修改字节码的能力使得 CGLIB 在许多高级应用场景中,如 AOP(面向切面编程)编程、动态代理、方法拦截等,都展现出了卓越的性能和灵活性。


通过使用 CGLIB,开发人员可以在运行时动态地定义横切关注点,例如日志记录、事务管理、安全控制等,而无需修改原有的业务逻辑代码。

为什么要用 CGLIB

选择 CGLIB 来实现动态代理的主要原因之一是 Spring 框架的使用。Spring 框架广泛应用于企业级应用程序的开发,而 CGLIB 作为基于 ASM 的字节码生成库,与 Spring 框架紧密集成,提供了强大的运行时字节码修改和动态生成能力,以下是使用 CGLIB 实现动态代理的简单示例代码:

建立被代理的类

首先,创建一个简单的代理对象,并不需要实现复杂的接口,那么可以直接创建一个实现类对象作为目标类,然后通过 CGLIB 或其他字节码库动态地修改这个类的字节码,生成代理类。


/**  * 被代理的类  * 目标对象类  */  public class TargetObject {      /**      * 目标方法(即目标操作)      */      public void exec() {          System.out.println("exec");      }  }  
复制代码

cglib 拦截器类

接下来,我们将实现一个 MethodInterceptor。通过该实现,所有的方法调用将被转发到 intercept()方法进行处理。



这种方法拦截器提供了一种机制,可以在方法调用之前和之后执行自定义逻辑,从而实现更高级的功能,如日志记录、事务管理、安全控制等。


/**  * 动态代理-拦截器  */  public class MyInterceptor implements MethodInterceptor {          private Object target;//目标类        public MyInterceptor(Object target) {          this.target = target;      }        /**      * 返回代理对象      * 具体实现,暂时先不追究。      */      public Object createProxy() {          Enhancer enhancer = new Enhancer();          enhancer.setCallback(this);//回调方法 拦截器          //设置代理对象的父类,可以看到代理对象是目标对象的子类。所以这个接口类就可以省略了。          enhancer.setSuperclass(this.target.getClass());          return enhancer.create();      }   }  
复制代码


上述代码中,我们通过 CGLIB 的 Enhancer 来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用 create()方法得到代理对象,对这个对象所有非 final 方法的调用都会转发给 MethodInterceptor.intercept()方法,在 intercept()方法里我们可以加入任何逻辑。


  /**      * args 目标方法的参数      * method 目标方法      */      @Override      public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)       throws Throwable {          System.out.println("Before method invocation");          //调用目标类的目标方法          method.invokeSuper(this.target, objects);        System.out.println("After method invocation");          return null;     }  
复制代码


通过调用 MethodProxy.invokeSuper()方法,我们将调用转发给原始对象,具体到本例,就是 TargetObject 的具体方法。CGLIG 中 MethodInterceptor 的作用跟 JDK 代理中的 InvocationHandler 很类似,都是方法调用的中转站。

测试类

在需要使用 TargetObject 时,我们可以通过 CGLIB 的动态代理机制来获取一个代理对象。通过这种方式,我们可以在运行时动态地创建代理对象,而无需修改 TargetObject 的源代码。



这个代理对象将继承自 TargetObject,并且所有的方法调用都会被拦截并转发到指定的处理逻辑中。这种机制提供了极大的灵活性和可扩展性,使得我们可以在不改变原有业务逻辑的情况下,实现各种附加功能。


public class MainTest {      public static void main(String[] args) {          //目标对象          TargetObject target = new TargetObject();          //拦截器          MyInterceptor myInterceptor = new MyInterceptor(target);          //代理对象,调用cglib系统方法自动生成          //注意:代理类是目标类的子类。          TargetObject proxyObj = (TargetObject) myInterceptor.createProxy();          proxyObj.exec();      }  }  
复制代码


我们分析了一下,从文件数上来说,cglib 比 jdk 实现的少了个接口类。因为 cglib 返回的代理对象是目标对象的子类。而 jdk 产生的代理对象和目标对象都实现了一个公共接口。

易错点:CGLIB 的 invoke 和 invokeSuper 的区分

在 CGLIB 中,MethodProxy 是一个非常重要的接口,它提供了对被代理方法的低级别访问。MethodProxy 的 invoke 和 invokeSuper 方法都是用于调用目标方法的。

invoke 方法

当你调用代理对象的某个方法时,这个方法会被转发到 MethodProxy 的 invoke 方法,invoke 方法接受四个参数:被代理对象、被代理方法、被代理方法的参数以及一个 MethodProxy 对象。


在 invoke 方法中,你可以使用 MethodProxy 的 getMethod 方法来获取被代理方法的详细信息(如方法名、参数类型等),然后你可以决定是否转发该方法调用或者执行其他逻辑。

invokeSuper 方法:

invokeSuper 方法是 MethodProxy 接口中的一个重要方法,用于执行被代理方法的调用,这个方法的调用会触发对目标对象的实际方法调用,从而使得我们可以对目标方法进行拦截和增强。


使用 invokeSuper,我们可以调用目标对象上的原始方法,并且可以在这个调用前后添加额外的逻辑。在 CGLIB 的动态代理中,通常在拦截器的 intercept 方法中使用 MethodProxy.invokeSuper 来调用目标方法,从而实现方法的拦截和增强。


注意:对于从 Object 中继承的方法,CGLIB 代理也会进行代理,如 hashCode()、equals()、toString()等,但是 getClass()、wait()等方法不会,因为它是 final 方法,CGLIB 无法代理

TargetObject 代理对象的类型信息

如果对 CGLIB 代理之后的对象类型进行深挖,可以看到如下信息:


class=class cglib.HelloConcrete$$EnhancerByCGLIB$$e3734e52superClass=class lh.HelloConcrete
interfaces: interface net.sf.cglib.proxy.Factory invocationHandler=not java proxy class
复制代码


  • 看到使用 CGLIB 代理之后的对象类型是 cglib.TargetObject$$e3734e52,这是 CGLIB 动态生成的类型;父类是 TargetObject,印证了 CGLIB 是通过继承实现代理;

  • 同时实现了 net.sf.cglib.proxy.Factory 接口,这个接口是 CGLIB 自己加入的,包含一些工具方法。

final 控制以及限制

既然是继承就不得不考虑 final 的问题。我们知道 final 类型不能有子类,所以 CGLIB 不能代理 final 类型,遇到这种情况会抛出类似如下异常:


java.lang.IllegalArgumentException: Cannot subclass final class cglib.TargetObject
复制代码


同样的,final 方法是不能重载的,所以也不能通过 CGLIB 代理,遇到这种情况不会抛异常,而是会跳过 final 方法只代理其他方法。如果你还对代理类 cglib.TargetObject



e3734e52 具体实现感兴趣,它大致长这个样子:


CGLIB 代理类具体实现


public class TargetObject$$EnhancerByCGLIB$$e3734e52  extends TargetObject  implements Factory{  ...  private MethodInterceptor CGLIB$CALLBACK_0; // ~~  ...     public final String exec()  {    ...    MethodInterceptor tmp17_14 = CGLIB$CALLBACK_0;    if (tmp17_14 != null) {      // 将请求转发给MethodInterceptor.intercept()方法。      return (String)tmp17_14.intercept(this,               CGLIB$exec$0$Method,               new Object[] { },               CGLIB$exec$0$Proxy);    }    return super.exec();  }  ...}
复制代码


上述代码我们看到,当调用代理对象的 exec()方法时,首先会尝试转发给 MethodInterceptor.intercept()方法,如果没有 MethodInterceptor 就执行父类的 exec()。这些逻辑没什么复杂之处,但是他们是在运行时动态产生的,无需我们手动编写。

未完待续

由于篇幅过程,小编,会在下一章深入分一下 cglib 的底层实现以及生产详细的 class 类的内容,以及底层的细节原理,请《精彩推荐 |【Java 技术专题】「重塑技术功底」攻破 Java 技术盲点之剖析动态代理的实现原理和开发指南(下)》



归纳总结

动态代理是一种技术,它可以在运行时动态地创建代理对象,拦截对目标对象的调用并执行一些额外的逻辑。动态代理主要分为两种实现方式:JDK 的动态代理和 CGLIB 的动态代理。


JDK 的动态代理

  • 代理对象和目标对象需要实现共同的接口。

  • 拦截器(或处理器)必须实现 InvocationHandler 接口。

  • JDK 的动态代理主要适用于目标对象实现了某个接口的情况。

CGLIB 的动态代理

  • 代理对象是目标对象的子类。

  • 拦截器必须实现 MethodInterceptor 接口。

  • CGLIB 不仅支持基于接口的代理,还支持基于类的代理。这意味着即使目标对象没有实现接口,仍然可以使用 CGLIB 创建代理对象。

如何选择 JDK 原生还是 CGLIB 动态代理

在实际应用中,选择 JDK 的动态代理还是 CGLIB 的动态代理取决于具体的需求和上下文。例如,如果目标对象已经有一个明确的接口定义,那么使用 JDK 的动态代理可能更加合适。而如果需要更灵活的字节码操作,或者目标对象没有明确的接口,那么 CGLIB 可能是一个更好的选择。

发布于: 刚刚阅读数: 7
用户头像

洛神灬殇

关注

🏆 InfoQ写作平台-签约作者 🏆 2020-03-25 加入

👑 后端技术架构师,前优酷资深工程师 📕 个人著作《深入浅出Java虚拟机—JVM原理与实战》 💻 10年开发经验,参与过多个大型互联网项目,定期分享技术干货和项目经验

评论

发布
暂无评论
精彩推荐 |【Java技术专题】「重塑技术功底」攻破Java技术盲点之剖析动态代理的实现原理和开发指南(中)_Java_洛神灬殇_InfoQ写作社区