Spring系列之@Aspect中5中通知详解

2023-11-18

@Aspect中有5种通知

  1. @Before:前置通知, 在方法执行之前执行

  2. @Aroud:环绕通知, 围绕着方法执行

  3. @After:后置通知, 在方法执行之后执行

  4. @AfterReturning:返回通知, 在方法返回结果之后执行

  5. @AfterThrowing:异常通知, 在方法抛出异常之后

这几种通知用起来都比较简单,都是通过注解的方式,将这些注解标注在@Aspect类的方法上,这些方法就会对目标方法进行拦截,下面我们一个个来看一下。

@Before:前置通知

介绍

定义一个前置通知

@Aspect
public class BeforeAspect {

    @Before("execution(* com.javacode2018.aop.demo10.test1.Service1.*(..))")
    public void before(JoinPoint joinPoint) {
        System.out.println("我是前置通知!");
    }
}
  1. 类上需要使用@Aspect标注

  2. 任意方法上使用@Before标注,将这个方法作为前置通知,目标方法被调用之前,会自动回调这个方法

  3. @Before标注的方法参数可以为空,或者为JoinPoint类型,当为JoinPoint类型时,必须为第一个参数

  4. @Before标注的方法名称可以随意命名,符合java规范就可以,其他通知也类似

@Before中value的值为切入点表达式,也可以采用引用的方式指定切入点,如:

package com.javacode2018.aop.demo10.test1;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class BeforeAspect {

    @Pointcut("execution(* com.javacode2018.aop.demo10.test1.Service1.*(..))")
    public void pc() {
    }

    @Before("com.javacode2018.aop.demo10.test1.BeforeAspect.pc()")
    public void before(JoinPoint joinPoint) {
        System.out.println("我是前置通知!");
    }
}

此时,before方法上面的切入引用了pc方法上面的@Pointcut的值

案例

来个普通的service

package com.javacode2018.aop.demo10.test1;

public class Service1 {
    public String say(String name) {
        return "你好:" + name;
    }

    public String work(String name) {
        return "开始工作了:" + name;
    }
}

给上面的类定义一个前置通知,Service1中的所有方法执行执行,输出一段文字我是前置通知!

package com.javacode2018.aop.demo10.test1;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class BeforeAspect1 {

    @Pointcut("execution(* com.javacode2018.aop.demo10.test1.Service1.*(..))")
    public void pc() {
    }

    @Before("com.javacode2018.aop.demo10.test1.BeforeAspect1.pc()")
    public void before(JoinPoint joinPoint) {
        System.out.println("我是前置通知!");
    }
}

测试代码

package com.javacode2018.aop.demo10;

import com.javacode2018.aop.demo10.test1.BeforeAspect1;
import com.javacode2018.aop.demo10.test1.Service1;
import org.junit.Test;
import org.springframework.aop.aspectj.annotation.AspectJProxyFactory;

public class AopTest10 {

    @Test
    public void test1() {
        Service1 target = new Service1();
        Class<BeforeAspect1> aspectClass = BeforeAspect1.class;
        AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
        proxyFactory.setTarget(target);
        proxyFactory.addAspect(aspectClass);
        Service1 proxy = proxyFactory.getProxy();
        System.out.println(proxy.say("路人"));
        System.out.println(proxy.work("路人"));
    }
}

运行输出

我是前置通知!
你好:路人
我是前置通知!
开始工作了:路人

对应的通知类

@Before通知最后会被解析为下面这个通知类

org.springframework.aop.aspectj.AspectJMethodBeforeAdvice

通知中获取被调方法信息

通知中如果想获取被调用方法的信息,分2种情况

  1. 非环绕通知,可以将org.aspectj.lang.JoinPoint作为通知方法的第1个参数,通过这个参数获取被调用方法的信息

  2. 如果是环绕通知,可以将org.aspectj.lang.ProceedingJoinPoint作为方法的第1个参数,通过这个参数获取被调用方法的信息

JoinPoint:连接点信息

org.aspectj.lang.JoinPoint

提供访问当前被通知方法的目标对象、代理对象、方法参数等数据:

package org.aspectj.lang;  
import org.aspectj.lang.reflect.SourceLocation;

public interface JoinPoint {  
    String toString();         //连接点所在位置的相关信息  
    String toShortString();     //连接点所在位置的简短相关信息  
    String toLongString();     //连接点所在位置的全部相关信息  
    Object getThis();         //返回AOP代理对象
    Object getTarget();       //返回目标对象  
    Object[] getArgs();       //返回被通知方法参数列表,也就是目前调用目标方法传入的参数  
    Signature getSignature();  //返回当前连接点签名,这个可以用来获取目标方法的详细信息,如方法Method对象等
    SourceLocation getSourceLocation();//返回连接点方法所在类文件中的位置  
    String getKind();        //连接点类型  
    StaticPart getStaticPart(); //返回连接点静态部分  
} 

ProceedingJoinPoint:环绕通知连接点信息

用于环绕通知,内部主要关注2个方法,一个有参的,一个无参的,用来继续执行拦截器链上的下一个通知。

package org.aspectj.lang;
import org.aspectj.runtime.internal.AroundClosure;

public interface ProceedingJoinPoint extends JoinPoint {

    /**
     * 继续执行下一个通知或者目标方法的调用
     */
    public Object proceed() throws Throwable;

    /**
     * 继续执行下一个通知或者目标方法的调用
     */
    public Object proceed(Object[] args) throws Throwable;

}

Signature:连接点签名信息

注意JoinPoint#getSignature()这个方法,用来获取连接点的签名信息,这个比较重要

Signature getSignature();

通常情况,spring中的aop都是用来对方法进行拦截,所以通常情况下连接点都是一个具体的方法,Signature有个子接口

org.aspectj.lang.reflect.MethodSignature

JoinPoint#getSignature()都可以转换转换为MethodSignature类型,然后可以通过这个接口提供的一些方法来获取被调用的方法的详细信息。

下面对上面的前置通知的案例改造一下,获取被调用方法的详细信息,新建一个Aspect类:BeforeAspect2

package com.javacode2018.aop.demo10.test2;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;

@Aspect
public class BeforeAspect2 {

    @Pointcut("execution(* com.javacode2018.aop.demo10.test1.Service1.*(..))")
    public void pc() {
    }

    @Before("com.javacode2018.aop.demo10.test2.BeforeAspect2.pc()")
    public void before(JoinPoint joinPoint) {
        //获取连接点签名
        Signature signature = joinPoint.getSignature();
        //将其转换为方法签名
        MethodSignature methodSignature = (MethodSignature) signature;
        //通过方法签名获取被调用的目标方法
        Method method = methodSignature.getMethod();
        //输出方法信息
        System.out.println(method);
    }
}

测试用例

@Test
public void test2() {
    Service1 target = new Service1();
    Class<BeforeAspect2> aspectClass = BeforeAspect2.class;
    AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
    proxyFactory.setTarget(target);
    proxyFactory.addAspect(aspectClass);
    Service1 proxy = proxyFactory.getProxy();
    System.out.println(proxy.say("路人"));
    System.out.println(proxy.work("路人"));
}

运行输出

public java.lang.String com.javacode2018.aop.demo10.test1.Service1.say(java.lang.String)
你好:路人
public java.lang.String com.javacode2018.aop.demo10.test1.Service1.work(java.lang.String)
开始工作了:路人

@Around:环绕通知

介绍

环绕通知会包裹目标目标方法的执行,可以在通知内部调用ProceedingJoinPoint.process方法继续执行下一个拦截器。

用起来和@Before类似,但是有2点不一样

  1. 若需要获取目标方法的信息,需要将ProceedingJoinPoint作为第一个参数

  2. 通常使用Object类型作为方法的返回值,返回值也可以为void

特点

环绕通知比较特殊,其他4种类型的通知都可以用环绕通知来实现。

案例

通过环绕通知来统计方法的耗时。

package com.javacode2018.aop.demo10.test3;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

import java.lang.reflect.Method;

@Aspect
public class AroundAspect3 {

    @Pointcut("execution(* com.javacode2018.aop.demo10.test1.Service1.*(..))")
    public void pc() {
    }

    @Around("com.javacode2018.aop.demo10.test3.AroundAspect3.pc()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        //获取连接点签名
        Signature signature = joinPoint.getSignature();
        //将其转换为方法签名
        MethodSignature methodSignature = (MethodSignature) signature;
        //通过方法签名获取被调用的目标方法
        Method method = methodSignature.getMethod();

        long startTime = System.nanoTime();
        //调用proceed方法,继续调用下一个通知
        Object returnVal = joinPoint.proceed();
        long endTime = System.nanoTime();
        long costTime = endTime - startTime;
        //输出方法信息
        System.out.println(String.format("%s,耗时(纳秒):%s", method.toString(), costTime));
        //返回方法的返回值
        return returnVal;
    }
}

测试用例

@Test
public void test3() {
    Service1 target = new Service1();
    Class<AroundAspect3> aspectClass = AroundAspect3.class;
    AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
    proxyFactory.setTarget(target);
    proxyFactory.addAspect(aspectClass);
    Service1 proxy = proxyFactory.getProxy();
    System.out.println(proxy.say("路人"));
    System.out.println(proxy.work("路人"));
}

运行输出

public java.lang.String com.javacode2018.aop.demo10.test1.Service1.say(java.lang.String),耗时(纳秒):19000500
你好:路人
public java.lang.String com.javacode2018.aop.demo10.test1.Service1.work(java.lang.String),耗时(纳秒):59600
开始工作了:路人

对应的通知类

@Around通知最后会被解析为下面这个通知类

org.springframework.aop.aspectj.AspectJAroundAdvice

@After:后置通知

介绍

后置通知,在方法执行之后执行,用法和前置通知类似。

特点

  • 不管目标方法是否有异常,后置通知都会执行

  • 这种通知无法获取方法返回值

  • 可以使用JoinPoint作为方法的第一个参数,用来获取连接点的信息

案例

Service1中任意方法执行完毕之后,输出一行日志。

package com.javacode2018.aop.demo10.test4;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

@Aspect
public class AfterAspect4 {

    @Pointcut("execution(* com.javacode2018.aop.demo10.test1.Service1.*(..))")
    public void pc() {
    }

    @After("com.javacode2018.aop.demo10.test4.AfterAspect4.pc()")
    public void after(JoinPoint joinPoint) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        System.out.println(String.format("%s,执行完毕!", methodSignature.getMethod()));
    }
}

测试案例

@Test
public void test4() {
    Service1 target = new Service1();
    Class<AfterAspect4> aspectClass = AfterAspect4.class;
    AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
    proxyFactory.setTarget(target);
    proxyFactory.addAspect(aspectClass);
    Service1 proxy = proxyFactory.getProxy();
    System.out.println(proxy.say("路人"));
    System.out.println(proxy.work("路人"));
}

运行输出

public java.lang.String com.javacode2018.aop.demo10.test1.Service1.say(java.lang.String),执行完毕!
你好:路人
public java.lang.String com.javacode2018.aop.demo10.test1.Service1.work(java.lang.String),执行完毕!
开始工作了:路人

对应的通知类

@After通知最后会被解析为下面这个通知类

org.springframework.aop.aspectj.AspectJAfterAdvice

这个类中有invoke方法,这个方法内部会调用被通知的方法,其内部采用try..finally的方式实现的,所以不管目标方法是否有异常,通知一定会被执行。

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    try {
        //继续执行下一个拦截器
        return mi.proceed();
    }
    finally {
        //内部通过反射调用被@After标注的方法
        invokeAdviceMethod(getJoinPointMatch(), null, null);
    }
}

@AfterReturning:返回通知

用法

返回通知,在方法返回结果之后执行。

特点

  • 可以获取到方法的返回值

  • 当目标方法返回异常的时候,这个通知不会被调用,这点和@After通知是有区别的

案例

后置通知中打印出方法及返回值信息。

package com.javacode2018.aop.demo10.test5;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

@Aspect
public class AfterReturningAspect5 {
    @Pointcut("execution(* com.javacode2018.aop.demo10.test1.Service1.*(..))")
    public void pc() {
    }

    @AfterReturning(value = "com.javacode2018.aop.demo10.test5.AfterReturningAspect5.pc()", returning = "retVal")
    public void afterReturning(JoinPoint joinPoint, Object retVal) throws Throwable {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        System.out.println(String.format("%s返回值:%s", methodSignature.getMethod(), retVal));
    }

}

注意@AfterReturning注解,用到了2个参数

  • value:用来指定切入点

  • returning:用来指定返回值对应方法的参数名称,返回值对应方法的第二个参数,名称为retVal

对应的通知类

@AfterReturning通知最后会被解析为下面这个通知类

org.springframework.aop.aspectj.AspectJAfterReturningAdvice

@AfterThrowing:异常通知

用法

在方法抛出异常之后会回调@AfterThrowing标注的方法。

@AfterThrowing标注的方法可以指定异常的类型,当被调用的方法触发该异常及其子类型的异常之后,会触发异常方法的回调。也可以不指定异常类型,此时会匹配所有异常。

未指定异常类型

未指定异常类型,可以匹配所有异常类型,如下

@AfterThrowing(value = "切入点")
public void afterThrowing()

指定异常类型

通过@AfterThrowingthrowing指定参数异常参数名称,我们用方法的第二个参数用来接收异常,第二个参数名称为e,下面的代码,当目标方法发生IllegalArgumentException异常及其子类型异常时,下面的方法会被回调。

@AfterThrowing(value = "com.javacode2018.aop.demo10.test6.AfterThrowingAspect6.pc()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, IllegalArgumentException e)

特点

  • 不论异常是否被异常通知捕获,异常还会继续向外抛出。

案例

Service1中加了login方法,用户名不是路人甲java时抛出异常。

package com.javacode2018.aop.demo10.test1;

public class Service1 {
    public String say(String name) {
        return "你好:" + name;
    }

    public String work(String name) {
        return "开始工作了:" + name;
    }

    public boolean login(String name) {
        if (!"路人甲java".equals(name)) {
            throw new IllegalArgumentException("非法访问!");
        }
        return true;
    }
}

来个异常通知

package com.javacode2018.aop.demo10.test6;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;

@Aspect
public class AfterThrowingAspect6 {
    @Pointcut("execution(* com.javacode2018.aop.demo10.test1.Service1.*(..))")
    public void pc() {
    }

    @AfterThrowing(value = "com.javacode2018.aop.demo10.test6.AfterThrowingAspect6.pc()", throwing = "e")
    public void afterThrowing(JoinPoint joinPoint, IllegalArgumentException e) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        System.out.println(String.format("%s发生异常,异常信息:%s", methodSignature.getMethod(), e.getMessage()));
    }

}

测试用例

@Test
public void test6() {
    Service1 target = new Service1();
    Class<AfterThrowingAspect6> aspectClass = AfterThrowingAspect6.class;
    AspectJProxyFactory proxyFactory = new AspectJProxyFactory();
    proxyFactory.setTarget(target);
    proxyFactory.addAspect(aspectClass);
    Service1 proxy = proxyFactory.getProxy();
    proxy.login("路人");
}

运行输出

public boolean com.javacode2018.aop.demo10.test1.Service1.login(java.lang.String)发生异常,异常信息:非法访问!

java.lang.IllegalArgumentException: 非法访问!

 at com.javacode2018.aop.demo10.test1.Service1.login(Service1.java:14)
 at com.javacode2018.aop.demo10.test1.Service1$$FastClassBySpringCGLIB$$ea03ccbe.invoke(<generated>)
 at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
 at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:769)

对应的通知类

@AfterThrowing通知最后会被解析为下面这个通知类

org.springframework.aop.aspectj.AspectJAfterThrowingAdvice

来看一下这个类的invoke方法,这个方法是关键

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
    try {
        //继续调用下一个拦截器链
        return mi.proceed();
    }
    catch (Throwable ex) {
        //判断ex和需要不糊的异常是否匹配
        if (shouldInvokeOnThrowing(ex)) {
            //通过反射调用@AfterThrowing标注的方法
            invokeAdviceMethod(getJoinPointMatch(), null, ex);
        }
        //继续向外抛出异常
        throw ex;
    }
}

几种通知对比

通知类型 执行时间点 可获取返回值 目标方法异常时是否会执行
@Before 方法执行之前
@Around 环绕方法执行 自己控制
@After 方法执行后
@AfterReturning 方法执行后
@AfterThrowing 方法发生异常后

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring系列之@Aspect中5中通知详解 的相关文章

随机推荐

  • Linux搭建gitlab以及汉化

    注 请使用管理员权限的用户 执行如下操作 文章目录 GitLab搭建 一 安装并配置必要的依赖关系 1 安装ssh 3 启动SSH服务 4 安装防火墙 如果已经安装了防火墙并且已经在运行状态 则可直接进行第6步 5 开启防火墙 6 添加ht
  • 【OpenGL学习】Shader和Shader类的抽象

    Shader 本节学习OpenGL中Shader的使用并将其抽象为类 简要介绍OpenGL所使用的着色器语言GLSL 一 什么是Shader 参考维基百科中对Shader的定义 着色器 维基百科 自由的百科全书 wikipedia org
  • 【终极版】java连接oracle数据库三种方式(上篇)

    很多小伙伴都对java连接oracle数据库的操作弄的稀里糊涂 今天我就贴出终极版 一举解决所有的连接方式 上篇主要是解决利用驱动连接的方式 我是小順 请大家关注我 我会给大家发更多的工具 JAR包 import java sql Conn
  • Mac 电脑python 升级3.7版本

    转自 https www jianshu com p 5f55997ab719 官网下载python3 7 并且安装https www python org downloads mac osx 本人电脑有多个版本的python 为了体验新版
  • ffmpeg推流参考文章

    https www cnblogs com leisure chn p 10623968 html https www bilibili com read cv12294853 基于M3568平台移植nginx https z zlg cn
  • Docker笔记:基本概念、镜像、容器、仓库以及数据卷的使用

    什么是docker docker 是一个开源的应用容器引擎 可以使用docker将应用程序和依赖打包到一个镜像之中 可以非常方便的移植到其他机器上运行 使用docker构建打包我们的应用 build 之后可以像集装箱一样很方便的传输到别的机
  • 【Learning PGM in R】第一章 概率推理

    目录 1 1机器学习 1 1 1监督学习 1 1 2无监督学习和强化学习 1 2概率表示 1 2 1概率计算和随机变量 1 2 2条件概率 联合概率分布和边缘分布 1 3贝叶斯规则 1 4概率图模型基础 1 4 1概率图模型基础理论 1 4
  • 使用Kalman滤波器做目标跟踪

    https www mathworks com help vision examples using kalman filter for object tracking html 由该例整理而来 1 前言 2 介绍 3 目标跟踪的挑战 4
  • Vue2屎山代码大盘点

    前言 相比其他的框架来说 Vue中更容易产出屎山代码 因为Vue中的options就是一个大对象 导致js本身的很多检测都失效了 比如一个函数没有用到的话会 变灰 template中代码提示比较少 较多的mixins等等 遇到屎山代码 大多
  • 【JEECG技术博文】简单实例讲解JEECG ONLINE表单权限控制(jeecg3.6)

    简单实例讲解JEECGONLINE表单权限控制 jeecg3 6 原文 http blog itpub net 30066956 viewspace 1872409 相关博文 http blog itpub net 30066956 vie
  • Atcoder beginner contest 301

    A Overall Winner AC代码 include
  • CVE -2022-26134漏洞复现(Confluence OGNL 注入rce漏洞)

    0x01 Atlassian Confluence Atlassian Confluence是一个专业的企业知识管理与协同软件 主要用于公司内员工创建知识库并建立知识管理流程 也可以用于构建企业wiki 其使用简单 但它强大的编辑和站点管理
  • SpringBoot关于List集合的校验

    1 List集合失效的校验 PostMapping saveBatch ApiOperation 批量保存 LogOperation 批量保存 public Result saveBatch RequestBody Validated Li
  • vue项目 公众号授权

    第一步 获取后台给的跳转链接 获取链接 getAuth this http post this api wxGongAccount this qs stringify access token this token then res gt
  • with recursive ——《寻找没有被执行的任务对》LeetCode Plus 会员专享题【详细解析】Hive / MySQL

    大家早上好 本人姓吴 如果觉得文章写得还行的话也可以叫我吴老师 欢迎大家跟我一起走进数据分析的世界 一起学习 感兴趣的朋友可以关注我的数据分析专栏 里面有许多优质的文章跟大家分享哦 另外也欢迎大家关注我的SQL刷题专栏 里面有我分享的高质量
  • SRS文档

    主要负责人 王希敏201303014099 http www cnblogs com wangximin 日历记事本的用例建模 一 以现实世界中日历中的便签 或记事本等为原型 我们平常所使用的日历 每页都会留有一些空白的部分 用来记录当日
  • 电脑一开机,内存爆满90%以上

    我的是联想电脑 电脑一开机 内存爆满 然后打开任务管理器 并没有显示任何的应用程序所占内存较大 我然后又去打开杀毒软件 关闭电脑自动更新 都没有效果 最后 解决的办法是运行window 内存诊断 最后100 之后重启成功
  • 历次改革学习-20220816

    通过分税制改革 中央政府实现了政治上和经济上的集权 必须指出的是 土地和房地产问题后来发展到如此严峻的程度 也是1994年分税制的结果 因为分税制事实上把土地支配权给了地方政府 地方政府也各显神通 发展出包括地方融资平台在内的各种推动地方发
  • Java代码质量评估工具

    概述 Java代码的质量评估主要包括代码的可维护性 健壮性 以及在运行时能达到既定的性能目标 可维护性主要包括代码的可读性 在关键的代码上提供详细注释 在设计类或方法以及代码逻辑时符合设定的编码规范 健壮性主要包括编写代码时应使用常用的设计
  • Spring系列之@Aspect中5中通知详解

    Aspect中有5种通知 Before 前置通知 在方法执行之前执行 Aroud 环绕通知 围绕着方法执行 After 后置通知 在方法执行之后执行 AfterReturning 返回通知 在方法返回结果之后执行 AfterThrowing