SpringAOP中通知顺序颠倒的bug

今天在学习通过注解来实现AOP功能发现了一个bug,就是后置通知和最终通知的顺序与xml配置下实现AOP功能的顺序有些出入

先记录下这几个通知类型并解释其意义

1、前置通知

  • 在目标方法前执行
  • 无论目标方法是否抛出异常,都执行,因为在执行前置通知的时候,目标方法还没有执行,还没有遇到异常

2、后置通知

  • 在目标方法执行之后执行
  • 当目标方法遇到异常,后置通知将不再执行
  • 后置通知可以接受目标方法的返回值,但是必须注意:aop:after-returning标签,后置通知的参数的名称和配合文件中returning="var"的值是一致的

3、最终通知

  • 在目标方法执行之后执行
  • 无论目标方法是否抛出异常,都执行,因为相当于finally

4、异常通知

  • 接受目标方法抛出的异常信息

5、环绕通知

  • 如果不在环绕通知中调用ProceedingJoinPoint的proceed,目标方法不会执行
  • 环绕通知可以控制目标方法的执行

言归正传,说起BUG

学习过程中,写了一个小例子:计算器实现加减乘除,我想在这些功能的前后做些事情,于是就会用到spring的aop功能。

简单看下实现的例子目录:

在这里插入图片描述

切面类:

package com.zcl.util;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
 * Author:markusZhang
 * degree of proficiency:
 * Date:Create in 2020/2/25 17:29
 */
@Component
@Aspect
@Order(1)
public class LogUtils {

    /**
     * try{
     *     @Before
     *     method.invoke()
     *     @AfterReturning
     * }catch(){
     *     @AfterThrowing
     * }finally{
     *     @After
     * }
     * 5个通知注解
     * @Before: 在方法执行之前运行                       前置通知
     * @After:在方法执行之后运行                       最终通知
     * @AfterReturning:在目标方法正常返回之后运行     后置通知
     * @AfterThrowing:在目标方法抛出异常之后运行       异常通知
     * @Around:                                        环绕通知
     *
     * 抽取可重用切入点表达式:
     * 1 随便声明一个没有实现的返回void的空方法
     * 2 给方法标注上@PointCut注解
     *
     */
    @Pointcut("execution(public int com.zcl.impl.MyMathCalculator.*(int,int))")
    public void hhMyPoint(){}

    //想在执行目标方法之前运行,写切入点表达式
    //execution(访问权限符 返回值类型 方法签名)
    @Before("hhMyPoint()")
    public void logStart(JoinPoint joinPoint){
        //获取到目标方法运行时使用的参数
        Object[] args = joinPoint.getArgs();
        //获取到方法签名
        Signature signature = joinPoint.getSignature();
        System.out.println("【LogUtils】【前置通知:"+signature.getName()+"】将被调用,它的参数是【"+ Arrays.asList(args)+"】");
    }

    /**
     * 切入点表达式的写法:
     * 固定格式:execution(访问权限符 返回值类型 方法全类名(参数表))
     *
     * 通配符:
     *       *:
     *          1) 匹配一个或者多个字符:
     *          2) 匹配任意一个参数:
     *          3) 只能匹配一层路径
     *          4) 权限位置*不能表示  权限位置不写也行
     *       ..:
     *          1) 匹配任意多个参数,任意类型参数
     *          2) 匹配任意多层路径
     *
     *  记住两种:
     *  最精确的:execution(public int com.zcl.impl.MyMathCalculator.add(int,int))
     *  最模糊的:execution(* *.*(..)) 千万别写;
     *
     *  &&:同时满足
     *  ||:满足任意一个表达式即可
     *  !:只要不是这个表达式即可
     */

    @AfterReturning(value = "hhMyPoint()",returning = "result")
    public void logReturn(JoinPoint joinPoint,Object result){
        System.out.println("【LogUtils】【后置通知:"+joinPoint.getSignature().getName()+"】调用完成,他的结果是:"+result);
    }

    /**
     * 细节四:我们可以在通知方法运行的时候,拿到目标方法的详细信息
     *  1)只需要为通知方法的参数列表上写一个参数:
     *          JoinPoint joinPoint:封装了当前目标方法的详细信息
     *  2)告诉spring哪个参数是用来接受异常
     *          throwing = "exception"
     *  3)Exception exception:指定通知方法可以接收哪些异常
     *
     *
     *  ajax接受服务器数据
     *
     */

    @After("hhMyPoint()")
    public void logEnd(JoinPoint joinPoint){
        System.out.println("【Log】【最终通知:"+joinPoint.getSignature().getName()+"】最终结束");
    }

    /**
     *Spring对通知方法的要求不严格;
     * 唯一的要求就是方法的参数列表不能乱写
     *      通知方法是Spring利用反射调用,每次方法调用得确定这个方法的参数表的值;
     *      参数表上的每一个参数,spring都得知道是什么?
     *      JoinPoint:认识
     *      不知道的参数一定告诉Spring这是什么
     */

    @AfterThrowing(value = "hhMyPoint()",throwing = "exception")
    public void logException(JoinPoint joinPoint,Exception exception) {
        System.out.println("【Log】【异常通知:"+joinPoint.getSignature().getName()+"】方法计算出现异常,异常是:"+exception);
    }

    /**
     * @Around :环绕通知:是spring中最强大的通知;
     * @Around :环绕通知:动态代理
     *
     * try{
     *     @Before
     *     method.invoke()
     *     @AfterReturning
     * }catch(){
     *     @AfterThrowing
     * }finally{
     *     @After
     * }
     *
     * 四合一通知就是环绕通知:
     * 环绕通知中有一个参数 ProceedingJoinPoint pjp
     */
    @Around("hhMyPoint()")
    public Object myAround(ProceedingJoinPoint pjp) throws Throwable {
        Object[] args = pjp.getArgs();
        Object proceed=null;
        String methodName = pjp.getSignature().getName();
        try{
            //直接处理的意思  就是利用反射调用目标方法即可 就是method.invoke(args);
            System.out.println("【前置通知:】【"+methodName+"方法】即将计算,它的参数是:【"+Arrays.asList(args)+"】");
            proceed = pjp.proceed(args);
            System.out.println("【后置通知:】【"+methodName+"方法】正常计算返回!它的结果是:"+proceed);
        }catch (Exception exception){
            System.out.println("【异常通知:】【"+methodName+"方法】出现异常,它的异常是:"+exception);
            //为了让外界能知道这个异常,这个异常一定抛出去
            throw new RuntimeException(exception);
        }finally{
            System.out.println("【最终通知:】【"+methodName+"方法】结束计算");
        }
        //反射调用后的返回值也一定返回出去
        return proceed;
    }
}

功能类:

package com.zcl.impl;
import org.springframework.stereotype.Service;
/**
 * Author:markusZhang
 * degree of proficiency:
 * Date:Create in 2020/2/25 17:16
 */
@Service
public class MyMathCalculator /*implements Calculator*/ {

    public int add(int i, int j) {
        System.out.println("方法内部执行...");
        int result = i+j;
        return result;
    }

    public int sub(int i, int j) {
        System.out.println("方法内部执行...");
        int result = i-j;
        return result;
    }

    public int mul(int i, int j) {
        System.out.println("方法内部执行...");
        int result = i*j;
        return result;
    }

    public int div(int i, int j) {
        System.out.println("方法内部执行...");
        int result = i / j;
        return result;
    }
}

测试类:

package com.zcl.test;

import com.zcl.impl.MyMathCalculator;
import com.zcl.inter.Calculator;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Author:markusZhang
 * degree of proficiency:
 * Date:Create in 2020/2/25 17:24
 */
public class AOPTest {
    /**
     * 有了动态代理,日志记录可以做的非常强大,而且与业务逻辑解耦
     *
     * jdk默认的动态代理,如果目标对象没有实现任何接口,是无法为它创建代理对象
     */
    ApplicationContext ioc = new ClassPathXmlApplicationContext("applicationContext.xml");

    /**
     * 通知方法的执行顺序:
     *
     *      try{
     *           @Before
     *           method.invoke()
     *           @AfterReturning
     *       }catch(){
     *           @AfterThrowing
     *       }finally{
     *           @After
     *       }
     * 正常执行: @Before(前置通知)==>@AfterReturn(后置通知)==>@After(最终通知)
     * 异常执行: @Before(前置通知)==>@AfterThrowing(异常通知)==>@After(最终通知)
     *
     *
     * 环绕通知:是优先于普通通知执行,执行顺序:
     *
     * [前置通知]
     * {
     *  try{
     *      环绕前置
     *      环绕执行:目标方法执行
     *      环绕返回
     *  }catch(e){
     *     环绕出现异常
     *  }finally{
     *     环绕后置
     *  }
     * }
     * [后置/异常通知]
     * [最终通知]
     *
     *
     */
    @Test
    public void test01(){
        MyMathCalculator bean = ioc.getBean(MyMathCalculator.class);
        bean.add(2,3);
    }
}

结果:

按道理来说:
正常返回的执行顺序应该是:
环绕前置—>普通前置—>目标方法执行—>环绕后置—>环绕最终—>普通后置—>普通最终
但实际情况是这样的:
在这里插入图片描述
环绕通知的顺序是正常的,而普通的后置通知和最终通知顺序不对。

总结

我觉得,如果在省时省力且对顺序无要求的情况下使用注解,在要求顺序的情况下那就得使用xml配置了,这是最正确的。
目前我用的是spring4.0版本的,如果spring高级版本修复了这个bug,欢迎指正

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值