likes
comments
collection
share

【重写SpringFramework】第二章aop模块:AOP代理下(chapter 2-5)

作者站长头像
站长
· 阅读数 16

1. 前言

上一节介绍了 AOP 代理的基本情况,并实现了通过 JDK 动态代理的方式来创建代理对象。这种方式的特点是必须定义接口,就代码层面来说有些繁琐。Spring 还提供了另一种创建代理的方式,Cglib 框架是通过自动生成子类的方式完成代理的,省去了定义接口的环节。

但是,AOP 代理并不是十全十美的,在某些情况下会导致 AOP 失效的情况。为了搞清楚这一问题,我们有必要探讨一下 AOP 代理的本质。只有从原理上有所认识,才能解释各种现象是如何发生的。

2. Cglib 代理

Spring 核心包集成了 CGLIB 相关的 API,其中 Enhancer 类负责创建代理对象,MethodInterceptor 接口(这是 CGLIB 相关的接口,不是 aopalliance 包的同名接口)负责处理拦截的逻辑。Enhancer 作为门面类至少需要完成三个步骤,如下所示:

  1. 设置父类的 Class 属性,实际上就是目标类,因为生成的代理是其子类。
  2. 设置 Callback,通过回调的方式执行增强逻辑。
  3. 调用 create 方法创建代理对象,可以强转为目标对象的类型。与之对比,JDK 动态代理必须强转为接口类型。
Target target = new Target();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(Target.class);			//设置目标类
enhancer.setCallback(new MethodInterceptor() {	//设置拦截器
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("cglib拦截");
        return methodProxy.invoke(method, args);
    }
});
Target proxy = (Target) enhancer.create();		//创建代理对象
proxy.doSomething();

3. CglibAopProxy

3.1 创建代理对象

CglibAopProxy 实现了 AopProxy 接口,作用是通过 Cglib 的方式创建代理对象。同样分为两个阶段,首先是创建代理对象。getProxy 方法可以分为三步:

  1. 获取目标类的接口,并添加到 AdvisedSupport 实例中。如果类名中包含 $$,说明对象是通过 Cglib 方式创建的,需要获取父类,也就是原始对象。
  2. 创建 Enhancer 实例,设置相关属性。
  3. 调用 createProxyClassAndInstance 方法创建代理。
public class CglibAopProxy implements AopProxy {
    protected final AdvisedSupport advised;

    public CglibAopProxy(AdvisedSupport advised) {
        this.advised = advised;
    }


    @Override
    public Object getProxy() {
        Class<?> rootClass = this.advised.getTargetClass();
        Class<?> proxySuperClass = rootClass;

        //1. 获取目标类的接口,并添加到AdvisedSupport实例中
        //如果目标类是通过Cglib的方式创建的,类名中包含 $$
        if (ClassUtils.isCglibProxyClass(rootClass)) {
            proxySuperClass = rootClass.getSuperclass();
            Class<?>[] additionalInterfaces = rootClass.getInterfaces();
            for (Class<?> additionalInterface : additionalInterfaces) {
                this.advised.addInterface(additionalInterface);
            }
        }

        //2. 创建增强器,设置相关属性
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(proxySuperClass);
        enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
        enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
        enhancer.setStrategy(new DefaultGeneratorStrategy());

        Callback[] callbacks = getCallbacks();
        Class<?>[] types = new Class<?>[callbacks.length];
        for (int x = 0; x < types.length; x++) {
            types[x] = callbacks[x].getClass();
        }
        enhancer.setCallbackTypes(types);

        //3. 创建代理
        return createProxyClassAndInstance(enhancer, callbacks);
    }
}

第二步,Enhancer 最重要的属性有两个,一是 superclass,Cglib 是通过生成子类的方式创建代理,因此设置的是目标对象的 Class 信息。二是 Callback 数组,由 getCallbacks 方法创建,具体来说就是将 AdvisedSupport 包装成一个 DynamicAdvisedInterceptor 实例,在之后的回调流程中使用。

//所属类[cn.stimd.spring.aop.framework.CglibAopProxy]
private Callback[] getCallbacks() {
    Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised);
    return new Callback[] {aopInterceptor};
}

第三步,CglibAopProxy 提供的实现是常规的创建代理的方式,即通过调用构造器的方式。

//所属类[cn.stimd.spring.aop.framework.CglibAopProxy]
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    enhancer.setCallbacks(callbacks);
    enhancer.setInterceptDuringConstruction(false);
    return enhancer.create();
}

子类 ObjenesisCglibAopProxy 重写了 createProxyClassAndInstance 方法,SpringObjenesis 组件可以绕过构造函数来创建实例。

public class ObjenesisCglibAopProxy extends CglibAopProxy {
    private static final SpringObjenesis objenesis = new SpringObjenesis();

    public ObjenesisCglibAopProxy(AdvisedSupport advised) {
        super(advised);
    }

    @Override
    protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
        Class<?> proxyClass = enhancer.createClass();
        Object proxyInstance = null;

        if (objenesis.isWorthTrying()) {
            proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
        }

        if (proxyInstance == null) {
            try {
                proxyInstance = proxyClass.newInstance();
            } catch (Throwable ex) {
                throw new BeansException("无法使用Objenesis来创建代理对象");
            }
        }

        ((Factory) proxyInstance).setCallbacks(callbacks);
        return proxyInstance;
    }
}

3.2 调用过程

由于设置了 DynamicAdvisedInterceptor 作为回调,在调用目标方法时,自动执行 DynamicAdvisedInterceptorintercept 方法。该方法与 JdkDynamicAopProxyinvoke 方法的调用逻辑几乎完全一致,简单回顾一下执行过程,同样分为三步:

  1. 准备工作。如果 exposeProxy 属性为 true,则将代理对象绑定到当前线程上
  2. 获取能够应用于目标方法的拦截器链
  3. 如果拦截器链不为空,说明是增强方法,创建 CglibMethodInvocation 对象执行拦截逻辑
//CglibAopProxy的内部类
private static class DynamicAdvisedInterceptor implements MethodInterceptor, Serializable {
    private final AdvisedSupport advised;

    public DynamicAdvisedInterceptor(AdvisedSupport advised) {
        this.advised = advised;
    }

    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object oldProxy = null;
        boolean setProxyContext = false;
        Class<?> targetClass = null;
        Object target;

        try {
            //1. 如果exposeProxy属性为true,将代理对象绑定到ThreadLocal上
            if (this.advised.exposeProxy) {
                oldProxy = AopContext.setCurrentProxy(proxy);
                setProxyContext = true;
            }

            //获取目标对象和对应的类型
            target = this.advised.getTargetSource().getTarget();
            if (target != null) {
                targetClass = target.getClass();
            }

            //2. 获取拦截器链
            List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
            Object retVal;

            //3. 执行拦截操作,最终调用目标方法
            //如果拦截器链为空,当作普通方法调用
            if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
                retVal = methodProxy.invoke(target, args);
            }else{
                //拦截器不为空,执行增强逻辑并调用目标方法
                retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
            }
            return retVal;
        }finally {
            //解除代理对象与当前线程的绑定
            if (setProxyContext) {
                AopContext.setCurrentProxy(oldProxy);
            }
        }
    }
}

CglibMethodInvocation 作为 CglibAopProxy 的内部类,同时又继承了 ReflectiveMethodInvocation 的子类,proceed 方法实际上是由父类实现的。CglibMethodInvocation 重写了调用目标方法的逻辑,可以看到,CglibMethodInvocation 是通过 MethodProxy 来调用方法的,而 JdkDynamicAopProxy 则是通过反射的方式调用方法。

//CglibAopProxy的内部类
private static class CglibMethodInvocation extends ReflectiveMethodInvocation{
    private final MethodProxy methodProxy;
    private final boolean publicMethod;


    @Override
    protected Object invokeJoinpoint() throws Throwable {
        if (this.publicMethod) {
            return this.methodProxy.invoke(this.target, this.arguments);
        }
        return super.invokeJoinpoint();
    }
}

4. AOP 代理的本质

4.1 基本原理

AOP 代理实际上是个包装类,持有一个目标对象,以及一个 Advisor 集合。一个类中有 N 个方法,所有方法一共用到了 M 种增强,因此 Advisor 集合代表的是一个类所有的可用增强。当代理对象在使用过程中,尤其是第一次调用某个方法,需要从 Advisor 集合中寻找适用于该方法的拦截器链,然后缓存起来以便下次使用。因此对于目标对象的方法来说,拦截器链不为空的方法就是增强方法,否则只是普通的方法

【重写SpringFramework】第二章aop模块:AOP代理下(chapter 2-5)

从创建代理到调用方法,整个流程中发生了两次过滤,首先过滤出适用于整个类的 Advisor 集合,然后过滤出适用于某个方法的拦截器链。这中间经过了由 AdvisorMethodInterceptor 的转变,这说明过滤的过程就是切面特性的体现,即哪些方法应该被增强,哪些不应该被增强。因此,我们需要对 Advisor 集合和拦截器链的概念有明确的认识,主要有两点区别:

  • Advisor 集合是面向类的,拦截器链是面向某个方法的,拦截器链是 Advisor 集合的子集。
  • Advisor 集合的元素类型是 Advisor,具有切面的语义。拦截器链的元素类型为 Object,实际上是 MethodInterceptor,只负责增强的逻辑。

4.2 示例代码

假如存在两个增强组件,TransactionAdvisor 的作用是开启数据库事务,支持解析 @Transactional 注解。LoggerAdvisor 的作用是打印日志,支持解析 @Logger 注解。然后定义一个类 Target,其中 foo 方法声明了两个注解,bar 方法只声明了一个注解。在创建代理时,代理对象持有 TransactionAdvisorLoggerAdvisor 两个增强组件。

//示例代码
public class Target {

    @Transactional
    @Logger
    public void foo() {}

    @Logger
    public void bar() {}
}

代理对象创建之后,调用 foobar 方法时会执行相应的增强逻辑。结合下图,分别描述两个方法的调用情况:

  • 调用 foo 方法时,拦截器链由 TransactionInterceptorLoggerInterceptor 组成,将会执行数据库事务和打印日志两个增强逻辑。
  • 调用 bar 方法时,拦截器链只有 LoggerInterceptor,意味着执行打印日志这一个增强逻辑。

【重写SpringFramework】第二章aop模块:AOP代理下(chapter 2-5)

5. AOP 失效的问题

5.1 对外暴露代理

当我们在一个增强方法中调用同一个类中的另一个增强方法,第二个方法的增强逻辑会失效,这就是 AOP 失效的问题。这是因为调用第一个方法时,引用指向的是代理对象。首先执行的是拦截器链,也就是增强逻辑。当进入第一个方法内部,this 引用指向的是目标对象,此时不存在拦截器链,第二个方法只是一个普通方法。

示例代码如下,调用 foo 方法时,实际是通过代理对象的引用调用的,因此会触发增强逻辑。但是在 foo 方法内部,this 引用指向的是目标对象,此时 bar 方法只是一个普通方法,不会触发增强逻辑。AOP 实质上是通过代理对象的回调来触发拦截器链,从而执行增强逻辑。因此,解决思路就是在调用 bar 方法时,确保使用的是代理对象的引用。

//示例代码:模拟AOP失效
public class Target {

    public void foo() {
        this.bar();
    }

    public void bar() {}
}

ProxyConfig 类的 exposeProxy 属性的作用是暴露代理对象,具体来说是在 AOP 代理回调的过程中,将代理对象绑定到当前线程上。我们已经在 JdkDynamicAopProxyCglibAopProxy 的回调方法中详细说明过了,通过 AopContextcurrentProxy 方法可以拿到当前代理对象。因此我们只需要在增强方法中拿到代理对象,然后通过代理对象来调用其他增强方法即可。调整后的代码如下:

//示例代码:暴露代理对象
public class Target {

    public void foo() {
        //获取代理对象的引用
        Test proxy = (Test) AopContext.currentProxy();
        //通过代理对象调用bar方法,再次触发AopProxy的回调
        proxy.bar();
    }

    public void bar() {}
}

5.2 ThreadLocal 的不足

代理对象保存在 AopContextThreadLocal 类型字段中,也就是说代理对象是和当前线程绑定的。如下代码所示,执行 foo 方法的线程绑定了代理对象,但新线程并没有绑定任何对象,因此获取代理对象的操作会导致抛出异常。这一缺陷与 AOP 代理的底层逻辑有关,应当尽量避免这一情况的出现。

//示例代码:无法获取代理对象
public class Target {

    public void foo() {
        new Thread(() -> {
            //在新线程中无法得到代理对象
            Target proxy = (Target) AopContext.currentProxy();
            Target.bar();
        }).start();
    }

    public void bar() {}
}

6. 测试

6.1 CGLIB 代理

测试类 SimpleCglibInterceptor 实现了 MethodInterceptor 接口,重写 intercept 方法模拟拦截的功能。

//测试类
public class SimpleCglibInterceptor implements MethodInterceptor {
    private Object target;

    public SimpleCglibInterceptor(Object target) {
        this.target = target;
    }

    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        System.out.println("cglib 拦截...");
        return methodProxy.invoke(target, args);
    }
}

测试类 CglibTarget 没有实现任何接口,这是因为 Cglib 代理是生成子类的方式,以此和 JDK 动态代理区别开来。

//测试类
public class CglibTarget {

    public void handle() {
        System.out.println("执行handle方法");
    }
}

在测试方法中,首先创建了 Enhancer 实例,指定代理对象的父类型(目标对象的子类),添加 SimpleCglibInterceptor 对象作为回调,然后调用 Enhancercreate 方法创建代理对象。注意,这里强转的是实现类的类型,而非接口类型。

@Test
public void testCglibProxy() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(CglibTarget.class);
    enhancer.setCallback(new SimpleCglibInterceptor(new CglibTarget()));
    CglibTarget proxy = (CglibTarget) enhancer.create();
    proxy.handle();
}

从测试结果可以看到,代理对象创建成功,并在调用目标方法前进行拦截并打印日志。

Cglib 拦截...
执行handle方法

6.2 CGLIB AOP 代理

在测试方法中,需要将代理工厂的 proxyTargetClass 属性设置为 true,开启 CGLIB 代理模式。调用 getProxy 方法返回的是普通对象的类型,不要使用接口类型来接收。

//测试类
@Test
public void testCglibAopProxy(){
    ProxyFactory factory = new ProxyFactory();
    factory.setTarget(new CglibTarget());
    factory.setProxyTargetClass(true);      //设置为Cglib代理模式
    factory.addAdvice(new SimpleAfterReturningAdvice());
    factory.addAdvice(new SimpleMethodBeforeAdvice());

    CglibTarget proxy = (CglibTarget) factory.getProxy();
    proxy.handle();
}

从测试结果可以看到,前置通知和返回通知的日志被打印出来,说明代理对象创建成功。

前置通知,方法名:handle
执行handle方法
返回通知,方法名:handle

6.3 对外暴露代理

在测试类 CglibTarget 定义两个方法,step1 方法调用了两次 step2 方法,第一次通过 this 引用目标对象来调用,第二次获取代理对象之后再调用。

//测试类
public class CglibTarget {

    public void step1(){
        System.out.println("执行step1...");

        //直接调用,不会触发拦截
        this.step2();

        //通过对外暴露的代理对象调用会触发拦截
        CglibTarget proxy = (CglibTarget) AopContext.currentProxy();
        proxy.step2();
    }

    public void step2(){
        System.out.println("执行step2...");
    }
}

在测试方法中,首先创建 ProxyFactory 对象,设置 exposeProxy 属性为 true,允许向外暴露对象。然后调用 step1 方法,模拟 AOP 失效的情形。

//测试方法
@Test
public void testExposeAopProxy(){
    ProxyFactory factory = new ProxyFactory();
    factory.setTarget(new CglibTarget());
    factory.setProxyTargetClass(true);      //设置为Cglib代理模式
    factory.setExposeProxy(true);           //对外暴露代理
    factory.addAdvice(new SimpleMethodBeforeAdvice());

    CglibTarget proxy = (CglibTarget) factory.getProxy();
    proxy.step1();
}

测试结果分为三组,首先是外部调用 step1 方法,由于 CglibTarget 是一个代理对象,因此 step1 方法得到了增强。其次,通过 this 关键字调用 step2 方法,由于此时处于目标方法内部,step2 方法被当作普通方法调用,不会触发拦截。第三,获取线程绑定的代理对象,然后调用 handle 方法就会再次增强。

前置通知,方法名:step1
执行step1...
-------------------------
执行step2...
-------------------------
前置通知,方法名:step2
执行step2...

7. 总结

本节介绍了 AOP 代理的第二种方式,即通过 Cglib 方式创建代理对象。这种方式的特点是无需实现接口,而是以生成子类的方式来实现。总的来说,AOP 代理是一个包装对象,持有一个目标对象,以及一组 Advisor 实例。这些 Advisor 组件应用于整个对象,对于具体方法来说还需要进一步过滤,得到一个拦截器链,实际上由 MethodInterceptor 组成。因此,AOP 代理的本质就是通过回调的方式执行拦截器链的增强逻辑,最终执行目标方法。

在实际应用中,经常会遇到 AOP 失效的情况,比如 @Transactional@Async 注解失效。这是由 AOP 代理的特性决定的,主要包括以下两个方面:

  • 如果在同一个对象的增强方法中调用另一个增强方法,第二个方法的增强效果会消失。这是因为增强逻辑必须由代理的回调来触发,而方法内部 this 指向的是目标对象。解决方法是对外暴露代理,通过 ThreadLocal 拿到代理对象。
  • 对外暴露代理也有不足之处,比如在新线程中无法拿到代理对象,这是因为代理对象是和调用方法的线程绑定的。这属于底层逻辑的限制,应避免这种情况的出现。

8. 项目信息

新增修改一览,新增(4),修改(2)

aop
└─ src
   ├─ main
   │  └─ java
   │     └─ cn.stimd.springwheel.aop
   │        └─ framework
   │           ├─ CglibAopProxy.java (+)
   │           ├─ DefaultAopProxyFactory.java (*)
   │           └─ ObjenesisCglibAopProxy.java (+)
   └─ test
      └─ java
         └─ cn.stimd.springwheel.aop.test
            └─ proxy
               ├─ CglibProxy.java (+)
               ├─ ProxyTest.java (*)
               └─ SimpleCglibInterceptor.java (+)

注:+号表示新增、*表示修改

注:项目的 master 分支会跟随教程的进度不断更新,如果想查看某一节的代码,请选择对应小节的分支代码。


欢迎关注公众号【Java编程探微】,加群一起讨论。

原创不易,觉得内容不错请关注、点赞、收藏。

转载自:https://juejin.cn/post/7390341683617841186
评论
请登录