5.3 五种通知

 

5.3 五种通知

Spring 中的 Aop 的通知类型有 5 种:

  • 前置通知
  • 后置通知
  • 异常通知
  • 返回通知
  • 环绕通知

具体实现,这里的案例和 5.2 中的一样,依然是给计算器的方法增强功能。

首先,在项目中,引入 Spring 依赖(这次需要引入 Aop 相关的依赖):

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.9.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.5</version>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.5</version>
    </dependency>
</dependencies>

接下来,定义切点,这里介绍两种切点的定义方式:

  • 使用自定义注解
  • 使用规则

其中,使用自定义注解标记切点,是侵入式的,所以这种方式在实际开发中不推荐,仅作为了解,另一种使用规则来定义切点的方式,无侵入,一般推荐使用这种方式。

自定义注解

首先自定义一个注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Action {
}

然后在需要拦截的方法上,添加该注解,在 add 方法上添加了 @Action 注解,表示该方法将会被 Aop 拦截,而其他未添加该注解的方法则不受影响。

@Component
public class MyCalculatorImpl {
    @Action
    public int add(int a, int b) {
        return a + b;
    }

    public void min(int a, int b) {
        System.out.println(a + "-" + b + "=" + (a - b));
    }
}

接下来,定义增强(通知、Advice):

@Component
@Aspect//表示这是一个切面
public class LogAspect {

    /**
     * @param joinPoint 包含了目标方法的关键信息
     * @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
     */
    @Before(value = "@annotation(Action)")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法开始执行了...");
    }

    /**
     * 后置通知
     * @param joinPoint 包含了目标方法的所有关键信息
     * @After 表示这是一个后置通知,即在目标方法执行之后执行
     */
    @After("@annotation(Action)")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法执行结束了...");
    }

    /**
     * @param joinPoint
     * @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object
     */
    @AfterReturning(value = "@annotation(Action)",returning = "r")
    public void returing(JoinPoint joinPoint,Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:"+r);
    }

    /**
     * 异常通知
     * @param joinPoint
     * @param e 目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获。如果想拦截所有,参数类型声明为 Exception
     */
    @AfterThrowing(value = "@annotation(Action)",throwing = "e")
    public void afterThrowing(JoinPoint joinPoint,Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法抛异常了:"+e.getMessage());
    }

    /**
     * 环绕通知
     *
     * 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法
     * @param pjp
     * @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配
     */
    @Around("@annotation(Action)")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

通知定义完成后,接下来在配置类中,开启包扫描和自动代理:

@Configuration
@ComponentScan
@EnableAspectJAutoProxy//开启自动代理
public class JavaConfig {
}

然后,在 Main 方法中,开启调用:

public class Main {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(JavaConfig.class);
        MyCalculatorImpl myCalculator = ctx.getBean(MyCalculatorImpl.class);
        myCalculator.add(3, 4);
        myCalculator.min(3, 4);
    }
}

再来回顾 LogAspect 切面,我们发现,切点的定义不够灵活,之前的切点是直接写在注解里边的,这样,如果要修改切点,每个方法上都要修改,因此,我们可以将切点统一定义,然后统一调用。

@Component
@Aspect//表示这是一个切面
public class LogAspect {

    /**
     * 可以统一定义切点
     */
    @Pointcut("@annotation(Action)")
    public void pointcut() {

    }

    /**
     * @param joinPoint 包含了目标方法的关键信息
     * @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
     */
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法开始执行了...");
    }

    /**
     * 后置通知
     * @param joinPoint 包含了目标方法的所有关键信息
     * @After 表示这是一个后置通知,即在目标方法执行之后执行
     */
    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法执行结束了...");
    }

    /**
     * @param joinPoint
     * @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object
     */
    @AfterReturning(value = "pointcut()",returning = "r")
    public void returing(JoinPoint joinPoint,Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:"+r);
    }

    /**
     * 异常通知
     * @param joinPoint
     * @param e 目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获。如果想拦截所有,参数类型声明为 Exception
     */
    @AfterThrowing(value = "pointcut()",throwing = "e")
    public void afterThrowing(JoinPoint joinPoint,Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法抛异常了:"+e.getMessage());
    }

    /**
     * 环绕通知
     *
     * 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法
     * @param pjp
     * @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

但是,大家也注意到,使用注解是侵入式的,我们还可以继续优化,改为非侵入式的。重新定义切点,新切点的定义就不在需要 @Action 注解了,要拦截的目标方法上也不用添加 @Action 注解。下面这种方式是更为通用的拦截方式:

@Component
@Aspect//表示这是一个切面
public class LogAspect {

    /**
     * 可以统一定义切点
     */
    @Pointcut("@annotation(Action)")
    public void pointcut2() {

    }
    /**
     * 可以统一定义切点
     * 第一个 * 表示要拦截的目标方法返回值任意(也可以明确指定返回值类型
     * 第二个 * 表示包中的任意类(也可以明确指定类
     * 第三个 * 表示类中的任意方法
     * 最后面的两个点表示方法参数任意,个数任意,类型任意
     */
    @Pointcut("execution(* org.javaboy.aop.commons.*.*(..))")
    public void pointcut() {

    }

    /**
     * @param joinPoint 包含了目标方法的关键信息
     * @Before 注解表示这是一个前置通知,即在目标方法执行之前执行,注解中,需要填入切点
     */
    @Before(value = "pointcut()")
    public void before(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法开始执行了...");
    }

    /**
     * 后置通知
     * @param joinPoint 包含了目标方法的所有关键信息
     * @After 表示这是一个后置通知,即在目标方法执行之后执行
     */
    @After("pointcut()")
    public void after(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法执行结束了...");
    }

    /**
     * @param joinPoint
     * @@AfterReturning 表示这是一个返回通知,即有目标方法有返回值的时候才会触发,该注解中的 returning 属性表示目标方法返回值的变量名,这个需要和参数一一对应吗,注意:目标方法的返回值类型要和这里方法返回值参数的类型一致,否则拦截不到,如果想拦截所有(包括返回值为 void),则方法返回值参数可以为 Object
     */
    @AfterReturning(value = "pointcut()",returning = "r")
    public void returing(JoinPoint joinPoint,Integer r) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法返回:"+r);
    }

    /**
     * 异常通知
     * @param joinPoint
     * @param e 目标方法所抛出的异常,注意,这个参数必须是目标方法所抛出的异常或者所抛出的异常的父类,只有这样,才会捕获。如果想拦截所有,参数类型声明为 Exception
     */
    @AfterThrowing(value = "pointcut()",throwing = "e")
    public void afterThrowing(JoinPoint joinPoint,Exception e) {
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        System.out.println(name + "方法抛异常了:"+e.getMessage());
    }

    /**
     * 环绕通知
     *
     * 环绕通知是集大成者,可以用环绕通知实现上面的四个通知,这个方法的核心有点类似于在这里通过反射执行方法
     * @param pjp
     * @return 注意这里的返回值类型最好是 Object ,和拦截到的方法相匹配
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            //这个相当于 method.invoke 方法,我们可以在这个方法的前后分别添加日志,就相当于是前置/后置通知
            proceed = pjp.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
}

喜欢这篇文章吗?扫码关注公众号【江南一点雨】【江南一点雨】专注于 SPRING BOOT+微服务以及前后端分离技术,每天推送原创技术干货,关注后回复 JAVA,领取松哥为你精心准备的 JAVA 干货!

本文遵守 Attribution-NonCommercial 4.0 International 许可协议。 Attribution-NonCommercial 4.0 International