【重写SpringFramework】第二章aop模块:Spring Advice(chapter 2-2)

1. 前言

上一节介绍了 AOP 规范的基本情况,从理论上来说,AOP 是对方法、构造器和字段进行访问的同时,执行额外的增强逻辑。在 AOP 规范的具体实践中,只定义了处理方法和构造器的相关接口。由于 AOP 规范在顶层设计方面进行了高度抽象,Spring 框架在落实层面有着自身的考量。对于应用程序来说,业务逻辑是由方法提供的,因此 Spring AOP 主要关心方法拦截,忽略了构造器的处理。

2. 继承结构

AOP 规范关注方法和构造器的拦截,Spring AOP 只考虑方法的拦截,这是因为业务代码都出现在方法里。再者,MethodInterceptor 只是表明拦截的目标是一个方法,并未规定在方法执行前还是执行后进行拦截。因此,Spring AOP 需要进一步抽象,将 Advice 分为前置通知和后置通知,后置通知又分为返回通知和异常通知。

在这里插入图片描述

从类图中可以看到,继承体系分为两部分,BeforeAdviceAfterAdvice 的子接口定义了各种通知的行为,MethodInterceptor 的实现类则起到了类似控制器的作用,决定了通知出现在方法之前还是之后。XxxAdviceInterceptorXxxAdvice 两两一组,分别实现了三种通知。这样做的好处是,Spring 内部会使用 MethodInterceptor 的实现类确保前置、后置、异常等拦截时机,我们只需要关注增强逻辑即可。换句话说,拦截器提供了语义,通知表现出行为

3. 通知

3.1 前置通知

前置通知是指在目标方法执行前进行拦截,MethodBeforeAdvice 接口定义了前置通知的行为。

public interface MethodBeforeAdvice extends BeforeAdvice{
    void before(Method method, Object[] args, Object target) throws Throwable;
}

MethodBeforeAdviceInterceptor 类持有一个 MethodBeforeAdvice 实例,invoke 方法先执行前置通知,然后交由下个拦截器处理。

public class MethodBeforeAdviceInterceptor implements MethodInterceptor {
    private MethodBeforeAdvice advice;

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //执行前置通知
        this.advice.before(invocation.getMethod(), invocation.getArguments(), invocation.getThis());
        return invocation.proceed();
    }
}

3.2 返回通知

返回通知是指在目标方法执行成功后进行拦截,AfterReturningAdvice 接口定义了返回通知的行为。

public interface AfterReturningAdvice extends AfterAdvice{
    void afterReturning(Object retVal, Method method, Object[] args, Object target) throws Throwable;
}

AfterReturningAdviceInterceptor 类持有一个 AfterReturningAdvice 实例,invoke方法先执行剩余流程,拿到返回值后,再执行后置通知。

public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice {
    private final AfterReturningAdvice advice;

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        Object retVal = mi.proceed();
        //执行返回通知
        this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis());
        return retVal;
    }
}

3.3 异常通知

异常通知是指在目标方法的执行过程中,处理可能出现的异常。ThrowsAdvice 是一个空接口,仅表明这是一个异常通知。Spring 对于如何处理异常只是做了一个约定,ThrowsAdvice 的实现类想要处理异常,必须满足两个条件:一是方法名是固定的,二是方法参数必须是一个或四个,且最后一个参数为 Throwable 类型。方法的签名如下:

  • void afterThrowing(Method method, Object[] args, Object target, Exception ex):包括目标方法、参数、方法所属的对象、抛出的异常
  • void afterThrowing(Exception ex):只有抛出的异常

这里有个问题,异常通知为何要采取约定方法签名的方式?这是因为约定方法签名,用户可以针对不同类型的异常来指定处理方法,也就是说可以定义若干个 afterThrowing 方法。如果由 ThrowsAdvice 接口定义方法,考虑到通用性,只能处理 Throwable 类型的异常,限制了更为灵活和细粒度的异常处理方式。

public interface ThrowsAdvice extends Advice {
    //标记接口
}

ThrowsAdviceInterceptor 类负责处理异常,持有两个字段。throwsAdvice 字段表示异常通知,由于 ThrowsAdvice 只是标记接口,因此没有指定异常通知的具体类型。exceptionHandlerMap 字段负责解析异常通知,将异常类型与处理异常的方法关联起来。

invoke 方法先执行剩余流程,并使用 try…catch 块进行包裹。如果出现错误,尝试寻找处理异常的方法,调用一个或四个参数的 afterThrowing 方法。如果没有找到,说明异常拦截的处理失败了,则继续向外抛出异常。

public class ThrowsAdviceInterceptor implements MethodInterceptor, AfterAdvice {
    private final Object throwsAdvice;
    private final Map<Class<?>, Method> exceptionHandlerMap = new HashMap<>();

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        try {
            return mi.proceed();
        }catch (Throwable ex){
            //获取异常处理方法
            Method handlerMethod = getExceptionHandler(ex);
            if (handlerMethod != null) {
                //处理异常
                invokeHandlerMethod(mi, ex, handlerMethod);
            }
            throw ex;
        }
    }

    private void invokeHandlerMethod(MethodInvocation mi, Throwable ex, Method method) throws Throwable {
        Object[] handlerArgs;
        //单个参数方法
        if (method.getParameterTypes().length == 1) {
            handlerArgs = new Object[] {ex};
        }
        //四个参数方法
        else {
            handlerArgs = new Object[] {mi.getMethod(), mi.getArguments(), mi.getThis(), ex};
        }

        try {
            //执行异常处理的方法
            method.invoke(this.throwsAdvice, handlerArgs);
        } catch (InvocationTargetException targetEx) {
            throw targetEx.getTargetException();
        }
    }
}

3.4 环绕通知

上述类型的通知不仅可以单独使用,也可以互相配合。当前置通知、返回通知和异常通知同时作用于一个方法时,就形成了环绕通知。因此,环绕通知并不是一个具体的实现类,只具有逻辑上的意义。此外,我们还可以绕开 Spring 的扩展,通过 MethodInterceptor 接口的实现类完成环绕通知。从功能上来说,二者是等价的。示例代码如下:

//示例代码:通过拦截器实现环绕通知
public class XxxMethodInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        Object result;
        try {
            //前置通知
            result = mi.proceed();
        }catch (Exception e) {
            //异常通知
            throw e;
        }
        //返回通知
        return result;
    }
}

4. MethodInvocation

4.1 概述

MethodInterceptor 的三个实现类都只是定义了相关通知的拦截时机,那么什么时候调用目标方法呢?我们来看 Joinpoint 接口中有关 proceed 方法的解释:

Proceeds to the next interceptor in the chain. The implementation and the semantics of this method depends on the actual joinpoint type (see the children interfaces).

执行拦截器链中的下一个拦截器。该方法的实现和语义取决于连接点的实际类型。

连接点的类型不同,proceed 方法的实现也有些区别。以方法调用为例,MethodInvocation 的实现类持有一个拦截器集合,比如 List<MethodInterceptor>。拦截器链的工作方式类似「责任链模式」,参考 Servlet 规范中的过滤器。在 proceed 方法中,依次执行每个拦截器,最后调用目标方法,然后开始收束,反向执行各个拦截器的剩余逻辑。在整个流程中,前置通知在目标方法之前执行,而返回通知和异常通知在目标方法之后执行。

在这里插入图片描述

4.2 简单实现

为了方便测试,我们需要一个 MethodInvocation 接口的简单实现类。示例代码如下,method 字段表示需要执行的方法,target 字段表示目标对象,interceptors 字段表示拦截器集合,interceptorIndex 字段为当前拦截器的下标。在 proceed 方法中,依次执行每个拦截器,当所有的拦截器都得到执行,再调用目标方法。

//示例代码:MethodInvocation的简单实现类
public class SimpleMethodInvocation implements MethodInvocation {
    private final Method method;
    private final Object target;
    private final List<MethodInterceptor> interceptors;
    private int interceptorIndex = -1;

    @Override
    public Object proceed() throws Throwable {
        //所有拦截器都得到执行,调用目标方法
        if(interceptorIndex == this.interceptors.size() - 1){
            return method.invoke(this.target);
        }

        //依次执行每个拦截器
        MethodInterceptor interceptor = this.interceptors.get(++this.interceptorIndex);
        return interceptor.invoke(this);
    }
}

5. 测试

5.1 准备工作

AdviceSupport 是一个工具类,负责添加拦截器和调用目标方法。其中,target 字段表示目标对象,interceptors 是拦截器的集合。通过 addAvice 方法将通知包装成一个拦截器,并加入到拦截器集合中。invoke 方法负责创建 SimpleMethodInvocation 实例,并由后者完成实际的调用流程。

//测试类:辅助完成AOP功能
public class AdviceSupport {
    private List<MethodInterceptor> interceptors = new ArrayList<>();
    private Object target;

    //添加通知
    public void addAdvice(Advice advice){
        MethodInterceptor interceptor = null;
        //前置通知
        if(advice instanceof MethodBeforeAdvice){
            interceptor = new MethodBeforeAdviceInterceptor((MethodBeforeAdvice) advice);
        }
        //返回通知
        else if (advice instanceof AfterReturningAdvice){
            interceptor = new AfterReturningAdviceInterceptor((AfterReturningAdvice) advice);
        }
        //异常通知
        else if (advice instanceof ThrowsAdvice){
            interceptor = new ThrowsAdviceInterceptor(advice);
        }

        this.interceptors.add(interceptor);
    }


    //调用目标对象的方法
    public void invoke(String methodName) throws Throwable {
        Method targetMethod = null;
        //通过反射的方式寻找目标方法
        for (Method method : this.target.getClass().getDeclaredMethods()) {
            if(method.getName().equals(methodName)){
                targetMethod = method;
                break;
            }
        }

        //将Method包装成一个方法调用,执行增强逻辑和目标方法
        if(targetMethod != null){
            SimpleMethodInvocation mi = new SimpleMethodInvocation(targetMethod, this.target, this.interceptors);
            mi.proceed();
        }
    }
}

前置通知和返回通知的实现类比较简单,仅打印日志即可,我们来看一下异常通知的实现类。之前提到,异常通知比较特殊,对处理异常的方法签名有要求,一是方法名必须是 afterThrowing,二是参数必须是一个或四个,这里使用了四个参数的形式。

//测试类:异常通知简单实现
public class SimpleThrowsAdvice implements ThrowsAdvice {

    public void afterThrowing(Method method, Object[] args, Object target, Exception ex){
        System.out.println("异常通知: 目标对象[" + target.getClass().getSimpleName() + "]执行方法[" + method.getName() + "]");
    }
}

TargetObj 类表示目标对象,我们需要测试两种情况,一是正常流程,执行前置通知和返回通知,handle 方法仅打印日志。二是异常流程,执行前置通知和异常通知,handleWithError 方法将会抛出一个运行时异常。

//测试类:待增强的目标对象
public class TargetObj {

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

    public void handleWithError(){
        int i = 10 / 0;
    }
}

5.2 Advice 环绕通知

首先使用前置、返回和异常通知组成一个环绕通知,它们都被包装成对应的拦截器对象,然后添加目标对象。最后调用 handle 方法模拟正常流程,调用 handleWithError 方法模拟异常流程。

//测试方法
@Test
public void testAdvice() throws Throwable {
    AdviceSupport support = new AdviceSupport();
    support.addAdvice(new SimpleMethodBeforeAdvice());
    support.addAdvice(new SimpleAfterReturningAdvice());
    support.addAdvice(new SimpleThrowsAdvice());
    support.setTarget(new TargetObj());

    support.invoke("handle");
    support.invoke("handleWithError");
}

测试结果分为两组,首先是正常的 handle 方法,执行前置通知和返回通知。然后是抛出异常的 handleWithError,执行前置通知和异常通知。从测试结果可以得到一下结论:

  • 正常流程中起作用的是前置通知、返回通知

  • 异常流程中起作用的是前置通知、异常通知

注:测试方法中可以任意打乱 Advice 的添加顺序,但通知的执行顺序是不变的,固定为前置 -> 目标方法 -> 后置/异常

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

前置通知,方法名:handleWithError
执行目标方法,并抛出异常
异常通知: 目标对象[TargetObj]执行方法[handleWithError]

5.3 方法拦截器

前面提到,前置通知、返回通知和异常通知构成的环绕通知可以使用拦截器来代替,测试类 SimpleMethodInterceptor 实现了类似环绕通知的功能。

//测试类:通过拦截器实现环绕通知
public class SimpleMethodInterceptor implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        Object result;
        String methodName = mi.getMethod().getName();
        try {
            System.out.println("前置拦截,方法名:" + methodName);
            result = mi.proceed();
        }catch (Exception e) {
            System.out.println("异常拦截,方法名:" + methodName);
            throw e;
        }
        System.out.println("返回拦截,方法名:" + methodName);
        return result;
    }
}

在测试方法中,只添加了一个拦截器,然后分别执行 handlehandleWithError 方法。

//测试方法
@Test
public void testMethodInterceptor() throws Throwable {
    AdviceSupport support = new AdviceSupport();
    support.addInterceptor(new SimpleMethodInterceptor());
    support.setTarget(new TargetObj());
    support.invoke("handle");
    support.invoke("handleWithError");
}

测试结果与上个测试基本相同,这说明一个拦截器起到了三个通知的作用。

前置拦截,方法名:handle
执行目标方法
返回拦截,方法名:handle

前置拦截,方法名:handleWithError
执行目标方法,并抛出异常
异常拦截,方法名:handleWithError

6. 总结

本节介绍了 Spring 对 AOP 规范的初步实现。首先,Spring 从实际需求出发,化繁为简,只关注方法的拦截。其次,对于方法来说,拦截的时机是必须要考虑的。鉴于此,Spring AOP 对 Advice 进一步抽象。具体来说,Advice 的子接口分别负责前置通知、返回通知和异常通知,而 MethodInterceptor 接口的子类则负责对应通知的调用时机。以前置通知为例,MethodBeforeAdvice 的关注点是增强逻辑,而 MethodBeforeAdviceInterceptor 则注重增强逻辑切入的时机。这样一来,拦截逻辑和调用时机被分离开来,降低了系统的耦合度,用户只需要关心拦截逻辑的实现即可。

当前置通知、返回通知和异常通知同时作用于一个方法时,便构成了一个环绕通知,这种环绕通知仅具有逻辑上的意义。此外,我们还可以通过 MethodInterceptor 接口来实现环绕通知,二者在功能上等价的。在实际应用中,Spring 事务正是通过 MethodInterceptor 实现的环绕通知,我们将在第四章 tx 模块进行介绍。

在实现了增强逻辑之后,还有连接点需要考虑。对于方法来说,我们关心的是 MethodInvocation 接口。一般来说,MethodInvocation 持有一组拦截器,在执行 proceed 方法时,按照一定的顺序指定拦截器的逻辑,最终调用目标方法。由此可见,连接点决定了如何处理运行时事件,起到了类似监听器的作用。

在这里插入图片描述

7. 项目信息

新增修改一览,新增(17),修改(0)

aop
├─ src
│  ├─ main
│  │  └─ java
│  │     └─ cn.stimd.springwheel.aop
│  │        ├─ framework
│  │        │  └─ adapter
│  │        │     ├─ AfterReturningAdviceInterceptor.java (+)
│  │        │     ├─ MethodBeforeAdviceInterceptor.java (+)
│  │        │     └─ ThrowsAdviceInterceptor.java (+)
│  │        ├─ AfterAdvice.java (+)
│  │        ├─ AfterReturningAdvice.java (+)
│  │        ├─ BeforeAdvice.java (+)
│  │        ├─ MethodBeforeAdvice.java (+)
│  │        └─ ThrowsAdvice.java (+)
│  └─ test
│     └─ java
│        └─ cn.stimd.springwheel.aop.test
│           └─ advice
│              ├─ AdviceSupport.java (+)
│              ├─ AdviceTest.java (+)
│              ├─ SimpleAfterReturningAdvice.java (+)
│              ├─ SimpleMethodBeforeAdvice.java (+)
│              ├─ SimpleMethodInterceptor.java (+)
│              ├─ SimpleMethodInvocation.java (+)
│              ├─ SimpleThrowsAdvice.java (+)
│              └─ TargetObj.java (+)
└─ pom.xml (+)

注:+号表示新增、*表示修改
  • 项目地址:https://gitee.com/stimd/spring-wheel

  • 本节分支:https://gitee.com/stimd/spring-wheel/tree/chapter2-2

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


欢迎关注公众号【Java编程探微】,加群一起讨论。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值