【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录


前言

在微服务流行的当下在使用SpringCloud/Springboot框架开发中AOP使用的非常广泛尤其是@Aspect注解方式当属最流行的不止功能强大性能也很优秀还很舒心所以本系列就结合案例详细介绍@Aspect方式的切面的各种用法力求覆盖日常开发中的各种场景。本文带来的案例打印Log主要介绍@Pointcut切点表达式的@annotation方式以及 五种通知Advice注解@Before、@After、@AfterRunning、@AfterThrowing、@Around

AOP与Spring AOP

在正式开始之前我们还是先了解一下AOP与Spring AOP~
在软件开发过程中有一些逻辑横向遍布在各个业务模块中像权限、监控、日志、事务、异常重试等等所以造成代码分散且冗余度高和业务代码混夹在一起, 写起来不够优雅改起来更是一种折磨为了解决这些问题AOPAspect Oriented Programming面向切面编程也就应运而生了它是一种编程思想就像OOP面向对象编程也是一种编程思想所以AOP不是某种语言或某个框架特有的它实现的是将横向逻辑与业务逻辑解耦实现对业务代码无侵入从而让我们更专注于业务逻辑本身本质是在不改变原有业务逻辑的情况下增强横切逻辑
在这里插入图片描述

在Spring中AOP共有3种实现方式

  • Spring1.2 基于接口的配置Spring最早的AOP实现是完全基于接口虽然兼容但已经不推荐了.
  • Spring2.0+ schema-based 配置 Spring2.0之后提供了 schema-based 配置也就是xml的方式配置.
  • Spring2.0+ @Aspect配置Spring2.0之后也提供了 @Aspect 基于注解的实现方式也就是本文的主角也是目前最方便、最广泛使用的方式(推荐)

@Aspect简单案例快速入门

@Aspect注解方式它的概念像@Aspect、@Pointcut、@Before、@After、@Around等注解都是来自于 AspectJ但是功能的实现是纯 Spring AOP 自己实现的主要有两大核心

  • 定义[切入点]使用 @Pointcut 切点表达式你可以理解成类似于正则表达式的强大东东。(本文先只介绍@annotation方式)
  • 定义[切入时机] 和 [增强处理逻辑]五种通知Advice注解 对[切入点]执行增强处理, 包括@Before、@After、@AfterRunning、@AfterThrowing、@Around

如果没有AOP基础对于概念可能会比较懵所以先上一个最简单案例基于@Aspect注解方式如何实现切面

// @Aspect和@Component定义一个切面类
@Aspect
@Component
public class MethodLogAspect {
    // 核心一定义切点(使用@annotation方式)
    @Pointcut(value = "@annotation(com.tiangang.aop.MethodLog)")
    public void pointCut() {

    }
    // 核心二对切点增强处理(这是5种通知中的前置通知)
    @Before("pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("前置通知:" + joinPoint);
    }
}

一共没有几行代码就非常简单实现在方法执行前打印日志的功能注解类如下对于打上这个注解的方法 都会被切面类增强处理

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

}

pom.xml依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

本文所有源码预览一共没有几行代码很容易掌握
在这里插入图片描述

ok接下来我们分别具体来看这两大核心 @PointcutAdvice .


一、@Pointcut

@Pointcut切点表达式非常丰富可以将 方法(method)、类(class)、接口(interface)、包(package) 等作为切入点非常灵活常用的有@annotation、@within、execution等方式由于篇幅原因本文先只介绍@annotation方式。

@annotation

@annotation方式是指切入点 是指定作用于方法上的注解即被Spring扫描到方法上带有该注解 就会执行切面通知。

@Pointcut(value = "@annotation(com.tiangang.aop.MethodLog)")
public void pointCut() {

}

案例给出的@Pointcut说明
语法@Pointcut(value = "@annotation(注解类名)")

注只有注解类名是动态的其它是固定写法.


二、五种通知Advice

通过@Pointcut定义的切点共有五种通知Advice方式

注解说明
@Before前置通知在被切的方法执行前执行
@After后置通知在被切的方法执行后执行比return更后
@AfterRunning返回通知在被切的方法return后执行
@AfterThrowing异常通知在被切的方法抛异常时执行
@Around环绕通知这是功能最强大的Advice可以自定义执行顺序

执行顺序如下

在这里插入图片描述

我这里在Service里定义了一个除法方法divide()在这个方法也打上@MethodLog注解让它可以被切面横切。

@Service
public class DemoService {
    @MethodLog
    public Integer divide(Integer a, Integer b) {
        System.out.printf("方法内打印: a=%d  b=%d %n", a, b);
        return a / b;
    }
}

用于测试的controller代码都很简单

@RestController
@RequestMapping("/demo")
public class DemoController {

    @Autowired
    private DemoService demoService;

    @GetMapping("/divide")
    public Integer divide(Integer a, Integer b) {
        return demoService.divide(a, b);
    }
}

1. @Before前置通知

前置通知在被切的方法执行之前执行!

@Before("pointCut()")
public void before(JoinPoint joinPoint) throws NoSuchMethodException {
    printMethod(joinPoint, "[前置通知before]");
}

注解语法@Before("切点方法名()")

注只有《切点方法名》是动态的其它是固定写法.

方法语法public void 方法名(JoinPoint joinPoint)

这里有个非常重要参数JoinPoint连接点 。因为Spring只支持方法类型的连接点所以在Spring中连接点指的就是被拦截到的方法. 里面有三个常用的方法

  • getSignature()获取签名

    MethodSignature signature = (MethodSignature) joinPoint.getSignature();

    通过signature可以获取名称 getName() 和 参数类型 getParameterTypes()

  • getTarget()获取目标类
    Class<?> clazz = joinPoint.getTarget().getClass();

    如果被切的类 是 被别的切面切过的类可以使用AopUtils.getTargetClass获取一个数组再从数组中找你期望的类。

    import org.springframework.aop.support.AopUtils;
    Class<?>[] targets = AopUtils.getTargetClass(joinPoint.getTarget()).getInterfaces();
    
  • getArgs()获取入参值

    Object[] args = joinPoint.getArgs()

基于这3个方法可以轻松打印被切的类名、方法名、方法参数值、方法参数类型等printMethod方法如下

private void printMethod(JoinPoint joinPoint, String name) throws NoSuchMethodException {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    Class<?> clazz = joinPoint.getTarget().getClass();
    Method method = clazz.getMethod(signature.getName(), signature.getParameterTypes());
    System.out.printf("[MethodLogAspect]切面 %s 打印 -> [className]:%s  ->  [methodName]:%s  ->  [methodArgs]:%s%n", name, clazz.getName(), method.getName(), Arrays.toString(joinPoint.getArgs()));
}

调用测试类输出结果如下

[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
方法内打印: a=10  b=2 

2. @After后置通知

后置通知在被切的方法执行之后执行无论被切方法是否异常都会执行!

@After("pointCut()")
public void after(JoinPoint joinPoint) throws NoSuchMethodException {
    printMethod(joinPoint, "[后置通知after]");
}

注解语法@After("切点方法名()")

注只有《切点方法名》是动态的其它是固定写法.

方法语法public void 方法名(JoinPoint joinPoint)

调用测试类输出结果如下

[MethodLogAspect]切面 [前置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
方法内打印: a=10  b=2 
[MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]

3. @AfterRunning返回通知

返回通知在被切的方法return后执行带有返回值如果被切方法异常则不会执行!

这里多了一个参数Object result注解上也多了一个参数returning

@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) throws NoSuchMethodException {
    printMethod(joinPoint, "[返回通知afterReturning]");
    System.out.printf("[MethodLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:%s%n", result);
}

注解语法@AfterReturning(value = "切点方法名(), returning = "返回值参数名")

注只有《切点方法名》和 《返回值参数名》是动态的其它是固定写法.

方法语法public void 方法名(JoinPoint joinPoint, Object result)

调用测试类输出结果如下

[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
方法内打印: a=10  b=2 
[MethodLogAspect]切面 [返回通知afterReturning] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
[MethodLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:5
[MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]

4. @AfterThrowing异常通知

异常通知只在被切方法异常时执行否则不执行。

这里多了一个参数Exception e表示捕获所有异常也可以设置为具体某一个异常例如NullPointerException、RpcException等等。注解上也多了一个参数throwing

@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) throws NoSuchMethodException {
    printMethod(joinPoint, "[异常通知afterThrowing]");
    System.out.printf("[MethodLogAspect]切面 [异常通知afterThrowing] 打印异常 -> Exception:%s%n", e);
}

注解语法@AfterThrowing(value = "切点方法名(), throwing = "异常参数名")

注只有《切点方法名》和 《异常参数名》是动态的其它是固定写法.

方法语法public void 方法名(JoinPoint joinPoint, Exception e)

调用测试类输出结果如下

[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 0]
方法内打印: a=10  b=0 
[MethodLogAspect]切面 [异常通知afterThrowing] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 0]
[MethodLogAspect]切面 [异常通知afterThrowing] 打印异常 -> Exception:java.lang.ArithmeticException: / by zero
[MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 0]
2023-01-06 21:05:06.536 ERROR 15436 --- [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is java.lang.ArithmeticException: / by zero] with root cause

5. @Around环绕通知

环绕通知方法可以包含上面四种通知方法是最全面最灵活的通知方法。

这里的参数类型和其它通知方法不同从JoinPoint变为ProceedingJoinPoint

@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    printMethod(joinPoint, "[环绕通知around][proceed之前]");
    // 执行方法, 可以对joinPoint.proceed()加try catch处理异常
    Object result = joinPoint.proceed();
    System.out.printf("[MethodLogAspect]切面 [环绕通知around][proceed之后]打印 -> [result]:%s%n", result);
    return result;
}

注解语法@Around("切点方法名()")

注只有《切点方法名》是动态的其它是固定写法.

方法语法public Object 方法名(ProceedingJoinPoint joinPoint) throws Throwable

调用测试类输出结果如下

[MethodLogAspect]切面 [环绕通知around][proceed之前] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
[MethodLogAspect]切面 [前置通知before] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
方法内打印: a=10  b=2 
[MethodLogAspect]切面 [返回通知afterReturning] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
[MethodLogAspect]切面 [返回通知afterReturning] 打印结果 -> result:5
[MethodLogAspect]切面 [后置通知after] 打印 -> [className]:com.tiangang.service.DemoService  ->  [methodName]:divide  ->  [methodArgs]:[10, 2]
[MethodLogAspect]切面 [环绕通知around][proceed之后]打印 -> [result]:5

总结

本文主要说明了如何通过@Aspect定义一个切面类并结合打印Log案例主要介绍了两大核心的用法

  • @Pointcut使用 @annotation 方式定义切入点
  • 五种通知(Advice)注解用法@Before、@After、@AfterRunning、@AfterThrowing、@Around

源码0积分下载地址https://download.csdn.net/download/scm_2008/87375584

如果感觉不错欢迎关注我 天罡gg 分享更多干货 https://blog.csdn.net/scm_2008
大家的「关注 + 点赞 + 收藏」就是我创作的最大动力谢谢大家的支持我们下文见

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: Spring

“【Spring AOP】@Aspect结合案例详解(一): @Pointcut使用@annotation + 五种通知Advice注解(已附源码)” 的相关文章