AOP五大通知注解

一、五大通知注解的概念

1.前置通知@Before
在目标方法执行之前执行执行的通知。无论何时都第一个执行

前置通知方法,可以没有参数,也可以额外接收一个JoinPoint,Spring会自动将该对象传入,代表当前的连接点,通过该对象可以获取目标对象 和 目标方法相关的信息。
注意,如果接收JoinPoint,必须保证其为方法的第一个参数,否则报错。

2.后置通知 @AfterReturning
在目标方法执行之后执行的通知。正常执行时第三个执行

在后置通知中也可以选择性的接收一个JoinPoint来获取连接点的额外信息,但是这个参数必须处在参数列表的第一个。

在后置通知中,还可以通过配置获取返回值

3.异常通知 @AfterThrowing
在目标方法抛出异常时执行的通知。出现异常时第三个执行

可以配置传入JoinPoint获取目标对象和目标方法相关信息,但必须处在参数列表第一位

另外,还可以配置参数,让异常通知可以接收到目标方法抛出的异常对象。

4.最终通知 @After
是在目标方法执行之后执行的通知。无论何时都第二个执行

和后置通知不同之处在于,后置通知是在方法正常返回后执行的通知,如果方法没有正常返-例如抛出异常,则后置通知不会执行。

而最终通知无论如何都会在目标方法调用过后执行,即使目标方法没有正常的执行完成。

另外,后置通知可以通过配置得到返回值,而最终通知无法得到。

最终通知也可以额外接收一个JoinPoint参数,来获取目标对象和目标方法相关信息,但一定要保证必须是第一个参数。

5 环绕通知@Around
一个@Around可以干掉上面的四个通知注解,可同时作用于了四个横切关注点

在环绕通知中必须显式的调用目标方法,目标方法才会执行,这个显式调用时通过ProceedingJoinPoint来实现的,可以在环绕通知中接收一个此类型的形参,spring容器会自动将该对象传入,注意这个参数必须处在环绕通知的第一个形参位置。

要注意,只有环绕通知可以接收ProceedingJoinPoint,而其他通知只能接收JoinPoint。

环绕通知需要返回返回值,否则真正调用者将拿不到返回值,只能得到一个null。

环绕通知有控制目标方法是否执行、有控制是否返回值、有改变返回值的能力。

环绕通知虽然有这样的能力,但一定要慎用,不是技术上不可行,而是要小心不要破坏了软件分层的“高内聚 低耦合”的目标。

案例步骤
1 导包或引入pom依赖
2 ①写配置 ,将目标类和切面类加入到ioc容器中(通过那四类注解@Component等)②告诉spring到底是哪个切面类(通过注解@Aspect + @Component,相当于告诉了spring,只要有切面存在那么就给根据目标类的接口给目标类创建代理类对象) ③告诉spring切面类中的每一个方法到底何时何地运行 ,即通知注解
3 Spring配置文件头部

<!--    自动包扫描,扫描相关的组件-->
    <context:component-scan base-package="caculator.Interface"></context:component-scan>
<!--    开启基于注解的aop功能-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

项目层级
在这里插入图片描述

4 ① 切面类的实现方法一

@Aspect
@Component
public class logUtils {
    
    
      @Pointcut("execution(public int caculator.Interface.MyCaculator.*(int,int))")
      public void test(){
    
    }


    @Before("test()")
    public static void logStart(JoinPoint joinPoint){
    
    
        System.out.println("logUtil前置方法【"+joinPoint.getSignature().getName()+"】开始执行" + "调用的参数是:" + Arrays.asList(joinPoint.getArgs()));
    }


    @AfterReturning(value = "test()",returning = "result")
    public void logReturn(JoinPoint joinPoint,Object result){
    
    
        System.out.println("logUtil返回方法【"+joinPoint.getSignature().getName()+"】执行完毕" + "返回的结果是:" + result);
    }

    @AfterThrowing(value= "test()",throwing = "exception")
    public void logException(JoinPoint joinPoint,Exception exception){
    
    
        System.out.println("logUtil异常方法【"+joinPoint.getSignature().getName()+"】出现异常" + "异常的原因是:" + exception);
    }

    @After("test()")
    public static void logAfter(JoinPoint joinPoint){
    
    
          System.out.println("logUtil结束方法【"+joinPoint.getSignature().getName()+"】结束执行");
      }
}      

②切面类的实现方法二

@Aspect
@Component
public class LogUtils {
    
    
      @Pointcut("execution(public int caculator.noInterface.MyCaculator.*(int ,int))")
      public void test(){
    
    }


//--------------------@Around方法测试---------------------------
    @Around("test()")
    public Object myAround(ProceedingJoinPoint pjp) throws Exception {
    
    
        Object result = null;
        String name = pjp.getSignature().getName();
        try {
    
    
            System.out.println("环绕前置通知【"+name+"】开始执行" + "调用的参数是:" + Arrays.asList(pjp.getArgs()));
            result = pjp.proceed(pjp.getArgs());//类似于Objet result = method.invoke(customer,args);
            System.out.println("环绕返回通知【"+name+"】执行完毕" + "返回的结果是:" + result);
        } catch (Throwable t) {
    
    
            System.out.println("环绕异常通知【"+name+"】出现异常" + "异常的原因是:" + t.getCause());
        } finally {
    
    
            System.out.println("环绕结束通知【"+name+"】结束执行");
        }
        return result;
    }
}

被代理类

@Component
public class MyCaculator {
    
    

    public int add(int op1, int op2) {
    
    
        return op1 + op2;
    }

    public int sub(int op1, int op2) {
    
    
        return op1 - op2;
    }

    public int mul(int op1, int op2) {
    
    
        return op1 * op2;
    }

    public int div(int op1, int op2) {
    
    
        return op1 / op2;
    }
}

测试

 @Test
    public void test1(){
    
    
        //基于无接口注解的aop,前提是导入cglib包或依赖
        ApplicationContext ioc = new ClassPathXmlApplicationContext("config/annotation.xml");
        //经测试可知即使没有实现任何接口的目标类也可以被创建相应的代理对象,并且可以通过execute指定切面类的切面方法执行的时机
        MyCaculator bean = ioc.getBean(MyCaculator.class);
        //输出结果是class caculator.MyTCaculator$$EnhancerBySpringCGLIB$$dce8466b,这是典型的内部类
        //也就是相当于在MyCaculator运行期间创建了MyCaculator的动态内部类作为代理类
        System.out.println(bean.getClass());
        bean.add(1,1);
    }

第四步方法①运行结果
在这里插入图片描述
第四部方法②运行结果
在这里插入图片描述
二、五大通知注解的运行顺序
实现步骤的第四步改为以下代码

@Aspect
@Component
public class LogUtils {
    
    
      @Pointcut("execution(public int caculator.noInterface.MyCaculator.*(int ,int))")
      public void test(){
    
    }


    @Before("test()")
    public static void logStart(JoinPoint joinPoint){
    
    
        System.out.println("@Before【"+joinPoint.getSignature().getName()+"】开始执行" + "调用的参数是:" + Arrays.asList(joinPoint.getArgs()));
    }


    @AfterReturning(value = "test()",returning = "result")
    public void logReturn(JoinPoint joinPoint,Object result){
    
    
        System.out.println("@AfterReturing【"+joinPoint.getSignature().getName()+"】执行完毕" + "返回的结果是:" + result);
    }

    @AfterThrowing(value= "test()",throwing = "e")
    public void logException(JoinPoint joinPoint,Exception e){
    
    
        System.out.println("@AfterThrowing【"+joinPoint.getSignature().getName()+"】出现异常" + "异常的原因是:" + e.getCause());
    }

        @After("test()")
        public static void logAfter(JoinPoint joinPoint){
    
    
            System.out.println("@After【"+joinPoint.getSignature().getName()+"】结束执行");
        }

//--------------------@Around方法测试---------------------------
    @Around("test()")
    public Object myAround(ProceedingJoinPoint pjp) throws Exception {
    
    
        Object result = null;
        String name = pjp.getSignature().getName();
        try {
    
    
            System.out.println("环绕前置通知【"+name+"】开始执行" + "调用的参数是:" + Arrays.asList(pjp.getArgs()));
            //类似于Objet result = method.invoke(customer,args);
            result = pjp.proceed(pjp.getArgs());
            System.out.println("环绕返回通知【"+name+"】执行完毕" + "返回的结果是:" + result);
        } catch (Throwable t) {
    
    
            System.out.println("环绕异常通知【"+name+"】出现异常" + "异常的原因是:" + t.getCause());
        } finally {
    
    
            System.out.println("环绕结束通知【"+name+"】结束执行");
        }
        return result;
    }
}

测试正常执行和异常执行步骤如下,重点分析结果
1 正常情况下

【环绕前置通知】【add】开始执行调用的参数是:[1, 1]
 @Before【add】开始执行调用的参数是:[1, 1]
 目标方法执行
【环绕返回通知】【add】执行完毕返回的结果是:2
【环绕结束通知】【add】结束执行
 @After【add】结束执行
 @AfterReturning【add】执行完毕返回的结果是:2

说明:环绕前置先执行,然后是普通前置执行,因为环绕前置后面就是方法执行了,普通前置无论如何也是要在方法执行前执行的,所有普通前置介于环绕前置和方法执行中间,方法无论执行

2 异常情况下

【环绕前置通知】【div】开始执行调用的参数是:[1, 0]
 @Before【div】开始执行调用的参数是:[1, 0]
 目标异常方法执行
【环绕异常通知】【div】出现异常异常的原因是:java.lang.ArithmeticException: / by zero
【环绕结束通知】【div】结束执行
 @AfterThrowing【div】结束执行
 @AfterReturning【div】执行完毕返回的结果是:null

说明:最后之所以还返回了结果null,是因为在方法返回前即执行到pjp.proceed(pjp.getArgs())时被捕获异常,相当于被环绕异常通知方法捕获并处理,直接执行到了finally执行了环绕结束通知方法并将最后结果result返回,此时的result的值还是被定义时的初始值null,至此环绕方法结束,此时的通知方法已经捕获不到任何异常了,因为异常已经被环绕方法用tdf代码块吃掉并吐出了一个值为null的结果,故普通通知方法按顺序执行普通结束和普通返回通知方法

@Around与四种注解同时存在时的执行顺序为
---正常执行 环绕前置通知 => 普通前置通知 => 目标方法执行 => 环绕返回通知 => 环绕结束通知 => 普通结束通知 =>普通返回通知;
---出现异常 环绕前置通知 => 普通前置通知 => => 目标方法执行 =>环绕异常通知 => 环绕结束通知 => 普通结束通知 =>普通返回通知(返回null)

补充:在环绕方法catch块内通常只是进行异常处理(输出异常信息),我们在此处可以在抛出一个运行时异常,这样可以对外面的普通方法达到通知效果,如此两种方法都能进行异常处理了,catch内抛出运行时运行结果如下

【环绕前置通知】【div】开始执行调用的参数是:[1, 0]
 @Before【div】开始执行调用的参数是:[1, 0]
 目标异常方法执行
【环绕异常通知】【div】出现异常异常的原因是:java.lang.ArithmeticException: / by zero
【环绕结束通知】【div】结束执行
 @AfterThrowing【div】结束执行
 @AfterReturning【div】出现异常异常的原因是:java.lang.RuntimeException: java.lang.ArithmeticException: / by zero

三、多切面的优先级顺序
判断依据是根据切面类首字母优先级排序,开字母顺序靠后的再最外层,顺序考前的在内层
,此时我们又引入一个类VaAspectTest,代码和图解如下

@Aspect
@Component
public class VaAspectTest {
    
    
    @Pointcut("execution(public int caculator.noInterface.MyCaculator.*(int,int))")
    public void test(){
    
    }


    @Before("test()")
    public static void logStart(JoinPoint joinPoint){
    
    
        System.out.println("Multi前置方法【"+joinPoint.getSignature().getName()+"】开始执行" + "调用的参数是:" + Arrays.asList(joinPoint.getArgs()));
    }


    @AfterReturning(value = "test()",returning = "result")
    public void logReturn(JoinPoint joinPoint,Object result){
    
    
        System.out.println("Multi返回方法【"+joinPoint.getSignature().getName()+"】执行完毕" + "返回的结果是:" + result);
    }

    @AfterThrowing(value= "test()",throwing = "exception")
    public void logException(JoinPoint joinPoint,Exception exception){
    
    
        System.out.println("Multi异常方法【"+joinPoint.getSignature().getName()+"】出现异常" + "异常的原因是:" + exception);
    }

    @After("test()")
    public static void logAfter(JoinPoint joinPoint){
    
    
        System.out.println("Multi结束方法【"+joinPoint.getSignature().getName()+"】结束执行");
    }
}

在这里插入图片描述

环绕前置通知【add】开始执行调用的参数是:[1, 1]
LogUtils@Before【add】开始执行调用的参数是:[1, 1]
VaAspect@Before前置方法【add】开始执行调用的参数是:[1, 1]
目标方法执行(内部执行)
VaAspect@After结束方法【add】结束执行
VaAspect@AfterReturning返回方法【add】执行完毕返回的结果是:2
环绕返回通知【add】执行完毕返回的结果是:2
环绕结束通知【add】结束执行
LogUtils@After【add】结束执行
LogUtils@AfterReturing【add】执行完毕返回的结果是:2

这个结果可能有些出乎意料。原因是@Around是作用于切面LogUtils的,和VaAspect切面无关

猜你喜欢

转载自blog.csdn.net/wwwwwww31311/article/details/113615856