Spring 动态代理基础知识

Spring AOP 使用动态代理技术在运行期织入增强的代码,为了揭示 Spring AOP 底层的工作机理,有必要学习涉及的 Java 知识。Spring AOP 使用了两种代理机制:一种是基于 JDK 的动态代理;另一种是基于 CGLib 的动态代理。之所以需要两种代理机制,很大程度上是因为 JDK 本身只提供接口的代理,而不支持类的代理。

1.带有横切逻辑的实例

下面通过具体化代码实现一个性能监视横切逻辑,并通过动态代理技术对此进行改造。在调用每一个目标类方法时启动方法的性能监视,在目标类方法调用完成时记录方法的花费时间,如下代码所示。

public class ForumServiceImpl implements ForumService {

    public void removeTopic(int topicId) {
        //①-1开始对该方法进行性能监视
        PerformanceMonitor.begin("com.smart.proxy.ForumServiceImpl.removeTopic");
        System.out.println("模拟删除Topic记录:"+topicId);
        try {
            Thread.currentThread().sleep(20);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        //①-2结束对该方法进行性能监视
        PerformanceMonitor.end();
    }

    public void removeForum(int forumId) {
        //②-1开始对该方法进行性能监视
        PerformanceMonitor.begin("com.smart.proxy.ForumServiceImpl.removeForum");
        System.out.println("模拟删除Forum记录:"+forumId);
        try {
            Thread.currentThread().sleep(40);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        //②-2结束对该方法进行性能监视    
        PerformanceMonitor.end();
    }
}

在上面代码中,粗体表示的代码就是具有横切逻辑特征的代码,每个 Service 类和每个业务方法体的前后都执行相同的代码逻辑:方法调用前启动 PerformanceMomtor;方法调用后通知 PerformanceMonitor 结束性能监视并记录性能监视结果。

PerformanceMonitor 是性能监视的实现类,下面给出一个非常简单的实现版本,如下代码所示。

public class PerformanceMonitor {
  //①通过一个ThreadLocal保存与调用线程相关的性能监视信息
  private static ThreadLocal<MethodPerformace> performaceRecord = new ThreadLocal<MethodPerformace>();
  //②启动对某一目标方法的性能监视    
  public static void begin(String method) {
        System.out.println("begin monitor...");
        MethodPerformace mp = performaceRecord.get();
        if(mp == null){
            mp = new MethodPerformace(method);
            performaceRecord.set(mp);
        }else{
            mp.reset(method);    
        }
    }
    public static void end() {
        System.out.println("end monitor...");
        MethodPerformace mp = performaceRecord.get();
     //③打印出方法性能监视的结果信息        
     mp.printPerformace();
    }
}

ThreadLocal 是将非线程安全类改造为线程安全类的“法宝”。PerformanceMonitor 提供了两个方法:通过调用begin(String method)方法开始对某个目标类方法的监视,其中 method 为目标类方法的全限定名;而通过调用 end() 方法结束对目标类方法的监视,并给出性能监视信息。这两个方法必须配套使用。

用于记录性能监视信息的 MethodPerformance 类的代码如下所示。

public class MethodPerformace {
    private long begin;
    private long end;
    private String serviceMethod;
    public MethodPerformace(String serviceMethod){
        reset(serviceMethod);
    }
    public void printPerformace(){
        end = System.currentTimeMillis();
        long elapse = end - begin;
        System.out.println(serviceMethod+"花费"+elapse+"毫秒。");
    }
    public void reset(String serviceMethod){
        this.serviceMethod = serviceMethod;
        this.begin = System.currentTimeMillis();
    }
}

正如上面所示,当某个方法需要进行性能监视时,必须调整方法代码,在方法体前后分别添加开启性能监视和结束性能监视的代码。这些非业务逻辑的性能监视代码破坏了 ForumServiceImpl 业务逻辑的纯粹性。我们希望通过代理的方式将业务类方法中开启和结束性能监视的横切代码从业务类中完全移除,并通过 JDK 或 CGLib 动态代理技术将横切代码动态织入目标方法的相应位置。

2.JDK动态代理

自 Java1.3 以后,Java 提供了动态代理技术,允许开发者在运行期创建接口的代理实例。在 Sun 刚推出动态代理时,还很难想象它有多大的实际用途,现在终于发现动态代理是实现 AOP 的绝好底层技术。

JDK 的动态代理主要涉及 java.lang.reflect 包中的两个类:Proxy 和 InvocationHandler。其中,InvocationHandler 是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑编织在一起。

而 Proxy 利用 InvocationHandler 动态创建一个符合某一接口的实例,生成目标类的代理对象。这样描述一定很抽象,我们马上着手使用 Proxy 和 InvocationHandler 这两个“魔法戒”对上面的性能监视代码进行革新。

首先从业务类 ForumServiceImpl 中移除性能监视的横切代码,使 ForumServiceImpl 只负责具体的业务逻辑,如下代码所示。

public class ForumServiceImpl implements ForumService {
    public void removeTopic(int topicId) {
//        PerformanceMonitor.begin("com.smart.proxy.ForumServiceImpl.removeTopic");
        System.out.println("模拟删除Topic记录:"+topicId);
        try {
            Thread.currentThread().sleep(20);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }        
//        PerformanceMonitor.end();
    }

    public void removeForum(int forumId) {
//        PerformanceMonitor.begin("com.smart.proxy.ForumServiceImpl.removeForum");
        System.out.println("模拟删除Forum记录:"+forumId);
        try {
            Thread.currentThread().sleep(40);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }        
//        PerformanceMonitor.end();
    }
}

从业务类中移除性能监视横切代码后,必须为它找到一个安身之所,InvocationHandIer 就是横切代码的“安家乐园”。将性能监视横切代码安置在 PerformanceHandler 中,如下代码所示。

public class PerformaceHandler implements InvocationHandler {//①实现InvocationHandler 
    private Object target;
    public PerformaceHandler(Object target){//②target为目标业务类
        this.target = target;
    }
    public Object invoke(Object proxy, Method method, Object[] args)//
            throws Throwable {
        PerformanceMonitor.begin(target.getClass().getName()+"."+ method.getName());//③-1
        Object obj = method.invoke(target, args);//③-2通过反射方法调用业务类的目标方法
        PerformanceMonitor.end();//③-3
        return obj;
    }
}

③处 invoke() 方法中粗体所示部分的代码为性能监视的横切代码,我们发现,横切代码只出现一次,而不是像原来那样散落各处。③-2处的 method.invoke() 语句通过 Java 反射机制间接调用目标对象的方法,这样 InvocationHandler 的 invoke() 方法就将横切逻辑代码(③-1)和业务类方法的业务逻辑代码(③-2)编织到一起,所以,可以将 InvocationHandler 看成一个编织器。下面对这段代码作进一步的说明。

首先实现 InvocationHandler 接口,该接口定义了一个 invoke(Object proxy,Method method,Object[] args)方法,其中,proxy 是最终生成的代理实例,一般不会用到;method 是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;args 是被代理实例某个方法的入参,在方法反射调用时使用。

其次,在构造函数里通过 target 传入希望被代理的目标对象,如②处所示;在 InvocationHandler 接口方法 invoke(Object proxy,Method method,Object[] args)里,将目标实例传递给 method.invoke() 方法,并调用目标实例的方法,如③处所示。

下面通过 Proxy 结合 PerformanceHandler 创建 ForumService 接口的代理实例,如下代码所示。

public class ForumServiceTest {
    public void proxy() {
        // 使用JDK动态代理
        ForumService target = new ForumServiceImpl();//
        PerformaceHandler handler = new PerformaceHandler(target);//②
        ForumService proxy = (ForumService) Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(), handler);//
        proxy.removeForum(10);//
        proxy.removeTopic(1012);     
    }
}

上面的代码完成了业务类代码和横切代码的编织工作并生成了代理实例。在②处,让 PerformanceHandler 将性能监视横切逻辑编织到 Forumservice 实例中,然后在③处,通过 Proxy 的 newProxylnstance() 静态方法为编织了业务类逻辑和性能监视逻辑的 handler 创建一个符合 ForumService 接口的代理实例。该方法的第一个入参为类加载器;第二个入参为创建代理实例所需实现的一组接口;第三个入参是整合了业务逻辑和横切逻辑的编织器对象。

按照③处的设置方式,这个代理实例实现了目标业务类的所有接口,即 ForumServicelmpl 的 ForumService 接口。这样就可以按照调用ForumService 接口实例相同的方式调用代理实例,如④处所示。运行以上代码,输出以下信息:

begin monitor.
模拟删除Forum记录:10
end monitor.
com.smart.proxy·ForumServiceImpl.removeForum花费47毫秒。
begin monitor.
模拟删除Topic记录:1012
end monitor·
com.smart.proxy·ForumServiceImpI·removeTopic花费26毫秒。

我们发现,程序的运行效果和直接在业务类中编写性能监视逻辑的效果一致,但在这里,原来分散的横切逻辑代码己经被抽取到 PerformanceHandler 中。当其他业务类(如UserService、SystemService等)的业务方法也需要使用性能监视时,只要按照与上面代码相似的方式分别为它们创建代理对象即可。下面通过时序图描述通过创建代理对象进行业务方法调用的整体逻辑,以进一步认识代理对象的本质,如下图所示。

上图中使用虚线的方式对通过 Proxy 创建的 ForumService 代理实例加以突显,ForumService 代理实例内部利用 PerformaceHandIer 整合横切逻辑和业务逻辑。调用者调用代理对象的 removeForum() 和 removeTopic() 方法时,上图所示的内部调用时序清晰地告诉我们实际上后台所发生的一切。

3.CGLib动态代理

使用 JDK 创建代理有一个限制,即它只能为接口创建代理实例,这一点可以从 Proxy 的接口方法 newProxyInstance(ClassLoader loader,Class[] interfaces,InvocationHandler h)中看得很清楚:第二个入参 interfaces 就是需要代理实例实现的接口列表。虽然面向接口编程的思想被很多大师级人物(包括RodJohnson)所推崇,但在实际开发中,许多开发者也对此深感困惑:难道对一个简单业务表的操作也需要老老实实地创建5个类(领域对象类、DAO接口、DAO实现类、service接口和Service实现类)吗?难道不能直接通过实现类构建程序吗?对于这个问题,很难给出一个孰优孰劣的准确判断,但仍有很多不使用接口的项目也取得了非常好的效果。

对于没有通过接口定义业务方法的类,如何动态创建代理实例呢?JDK 动态代理技术显然己经黔驴技穷,CGLib 作为一个替代者,填补了这项空缺。CGLib 采用底层的字节码技术,可以为一个类创建子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。下面采用 CGLib 技术编写一个可以为任何类创建织入性能监视横切逻辑代理对象的代理创建器,如下面代码所示。

public class CglibProxy implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();

    public Object getProxy(Class clazz) {
        enhancer.setSuperclass(clazz);//①设置需要创建子类的类
        enhancer.setCallback(this);
        return enhancer.create();//②通过字节码技术动态创建子类
    }

    public Object intercept(Object obj, Method method, Object[] args,//③拦截父类所有方法调用
            MethodProxy proxy) throws Throwable {
        PerformanceMonitor.begin(obj.getClass().getName()+"."+method.getName());//③-1
        Object result=proxy.invokeSuper(obj, args);//③-2 通过代理类调用父类中的方法
        PerformanceMonitor.end();//③-1
        return result;
    }
}

在上面的代码中,用户可以通过 getProxy(Class clazz) 方法为一个类创建动态代理对象,该代理对象通过扩展 clazz 实现代理。在这个代理对象中,织入性能监视的横切逻辑(粗体部分)。intercept(Object obj, Method method,Object[] args,MethodProxy proxy) 是 CGLib 定义的 Interceptor 接口方法,它拦截所有目标类方法的调用。其中,obj 表示目标类的实例:method 为目标类方法的反射对象;args 为方法的动态入参;proxy 为代理类实例。

下面通过 CglibProxy 为 ForumServicelmpl 类创建代理对象,并测试代理对象的方法,

public class ForumServiceTest {
    public void proxy() {
        //使用CGLib动态代理
        CglibProxy cglibProxy = new CglibProxy();
        ForumService forumService = (ForumService)cglibProxy.getProxy(ForumServiceImpl.class);//
        forumService.removeForum(10);
        forumService.removeTopic(1023);  
    }
}

在①处通过 CglibProxy 为 ForumServicelmpl 动态创建了一个织入性能监视逻辑的代理对象,并调用代理类的业务方法。运行上面的代码,输出以下信息:

begin monitor...
模拟删除Forum记录:10
end monitor...
com.smart.proxy·ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0.removeForum花费47毫秒。
begin monitor...
模拟删除Topic记录:1023
end monitor...
com.smart.proxy·ForumServiceImpI$$EnhancerByCGLIB$$2a9V99c0·removeTopic花费16毫秒。

观察以上输出,除了发现两个业务方法中都织入了性能监控的逻辑外,还发现代理类的名字变成 com.smart.proxy·ForumServiceImpl$$EnhancerByCGLIB$$2a9199c0,这个特殊的类就是 CGLib 为 ForumServiceImpl 动态创建的子类。

值得一提的是,由于 CGLib 采用动态创建子类的方式生成代理对象,所以不能对目标类中的 final 或 private 方法进行代理。

4.AOP联盟

AOP 联盟(http://aopalliance.sourceforge.net)是众多开源 AOP 项目的联合组织,该组织的目的是为了制定一套规范描述 AOP 的标准,定义标准的 AOP 接口,以便各种遵守标准的具体实现可以相互调用。

这种标准的制定本应当由 sun 来做,但是因为 sun 运作迟缓,AOP 联盟便捷足先登,而且它的影响力越来越大。现在大部分的 AOP 实现都采用 AOP 联盟的标准,所以 AOP 联盟制定的规范己经成为事实上的标准。

5.代理知识小结
Spring AOP 的底层就是通过使用 JDK 或 CGLib 动态代理技术为目标 Bean 织入横切逻辑的。这里对动态创建代理对象作一个小结。

虽然通过 PerformanceHandler 或 CglibProxy 实现了性能监视横切逻辑的动态织入,但这种实现方式存在3个明显需要改进的地方。

(1)目标类的所有方法都添加了性能监视横切逻辑,而有时这并不是我们所期望的,我们可能只希望对业务类中的某些特定方法添加横切逻辑。

(2)通过硬编码的方式指定了织入横切逻辑的织入点,即在目标类业务方法的开始和结束前织入代码。

(3)手工编写代理实例的创建过程,在为不同类创建代理时,需要分别编写相应的创建代码,无法做到通用。

以上3个问题在 AOP 中占用重要的地位,因为 Spring AOP 的主要工作就是围绕以上3点展开的:Spring AOP 通过 Pointcut(切点)指定在哪些类的哪些方法上织入横切逻辑,通过 Advice(增强)描述横切逻辑和方法的具体织入点(方法前、方法后、方法的两端等)。此外,Spring 通过 Advisor(切面)将 Pointcut 和 Advice 组装起来。有了 Advisor 的信息,Spring 就可以利用 JDK 或 CGLib 动态代理技术采用统一的方式为目标 Bean创建织入切面的代理对象了。

JDK 动态代理所创建的代理对象,在Java1.3下,性能差强人意。虽然在高版本的 JDK 中动态代理对象的性能得到了很大的提高,但有研究表明, CGLib 所创建的动态代理对象的性能依旧比 JDK 所创建的动态代理对象的性能高不少(大概10倍)。但 CGLib 在创建代理对象时所花费的时间却比 JDK 动态代理多(大概8倍)。对于 singleton 的代理对象或者具有实例池的代理,因为无须频繁地创建代理对象,所以比较适合采用 CGLib 动态代理技术;反之则适合采用 JDK 动态代理技术。

原文地址:https://www.cnblogs.com/jwen1994/p/11080095.html