小工具      在线工具  汉语词典  css  js  c++  java

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

springboot,spring,java,aop,pointcut,annotation 额外说明

收录于:40天前


前言

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

AOP与Spring AOP

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

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

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

@Aspect简单案例快速入门

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

  • 定义[切入点]:使用 @切入点 切点表达式,你可以理解成类似于正则表达式的强大东东。(本文先只介绍@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,接下来我们分别具体来看这两大核心 @切入点建议 .


一、@Pointcut

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

@annotation

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

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

}

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

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


二、五种通知Advice

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

注解 阐明
@前 预先通知,在cut方法执行之前执行
@后 post通知,cut方法执行后执行,晚于return
@运行后 返回通知,cut方法返回后执行
@投掷后 异常通知,当cut方法抛出异常时执行
@大约 Surround notification,这是最强大的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;
    }
}

用于测试的控制器代码非常简单:

@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]");
}

注释语法@前(”切点方法名()")

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

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

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

  • getSignature()获取签名:

    MethodSignature签名 = (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()

基于这三个方法,可以方便的打印:剪切的类名、方法名、方法参数值、方法参数类型等。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]");
}

注释语法@后(”切点方法名()")

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

方法语法: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(值=“切点方法名(),返回=“返回值参数名”)

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

方法语法: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异常通知

异常通知只有在cut方法出现异常时才执行,否则不执行。

这里多了一个参数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(值=“切点方法名(), 投掷 = "异常参数名”)

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

方法语法: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;
}

注释语法@大约(”切点方法名()")

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

方法语法: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案例主要介绍了两大核心的用法:

  • @切入点使用 @注解 方式定义切入点
  • 使用建议注释的五种方法:@Before、@After、@AfterRunning、@AfterThrowing、@Around

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

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

. . .

相关推荐

额外说明

线程和进程有什么区别?进程=火车,线程=马车

线程和进程的区别是什么? - 知乎   看了一遍排在前面的答案,类似”进程是资源分配的最小单位,线程是CPU调度的最小单位“这样的回答感觉太抽象,都不太容易让人理解。 做个简单的比喻:进程=火车,线程=车厢 线程在进程下行进(单纯的车厢无法运行) 一个进

额外说明

Stream——流式计算

文章目录 前言 Demo操作 前言 在互联网的开发中,往往都离不开存储和计算操作。 在程序中,存储方式分为很多种类,如Java中的集合、数据库、Redis等。计算应该由流来操作。 Demo操作 假设有这个一个User类,如下所示: package dem

额外说明

25岁之前农村青年人应该阅读的一篇文章

  25岁是一个男人心态抑或思考方式的转折点。对男人来讲很重要,也是一个里程碑的时刻。从这一刻后,你需要具备责任心与事业心。   25岁之前,你不需要具备责任心与事业心,因为这不是你应该考虑的问题,不是这个年龄段思考的问题。 25岁之前,你不需要跟周围的

额外说明

Java基础——转换流

 (1)字符输入转换流:InputStreamReader 可把原始的字节流按照指定编码转换成字符输出流。 可解决字符流读取不同编码乱码的问题。  (2)字符输出转换流:OutputStreamWriter 可把字节输出流按照指定编码转换成字符输出流。

额外说明

100天精通Oracle-实战系列(第16天)使用 RMAN 备份快速恢复误删数据表

使用 RMAN 备份快速恢复误删数据(第16天) ->返回总目录<- 上一讲介绍了如何使用 RMAN 备份进行异机恢复,但是只适用于全库的恢复,如果只是误删了部分数据或者几张表,通过全库恢复的话,如果数据量很大的话,未免有些浪费时间。所以本文就介绍如何使

额外说明

C++类的包含编译模型

C++类的包含编译模型 一、C++普通类的包含编译模型 1、类定义头文件student.h class Student { public: void print(); }; #include "student.cpp" 2、类实现文件stude

额外说明

GridView导出Excel研究

Introduction: 将 GridView中的数据导出为 Excel是web应用中的常见功能。在不同的应用场景下有不同的导出技术。在本文中我将介绍一些导出的技术,希望对您有所帮助 GridView Export the  Excel (Basic 

额外说明

Java【算法分享 01】道格拉斯-普克 Douglas-Peucker 抽稀算法(算法流程图解+使用JDK8方法实现+详细注解源码)

1.算法说明   道格拉斯-普克算法 Douglas-Peucker Algorithm 简称 D-P 算法,亦称为拉默-道格拉斯-普克算法、迭代适应点算法、分裂与合并算法,是将曲线近似表示为一系列点,并减少点的数量的一种算法。该算法的原始类型分别由乌尔

额外说明

wordpress漏洞利用_利用WordPress和Facebook功能的优秀教程

WordPress 漏洞利用 Facebook 可以在博客的发展中发挥至关重要的作用,因为它是网络上最大的社交媒体网络之一。在本文中,我们将分享一些最有价值的技巧和教程,让您充分利用 Facebook 和 WordPress 的强大功能。 由于 Face

额外说明

Java web 访问工程下的静态资源

只需将以下静态资源路径映射添加到 springMVC 配置文件中即可。 <mvc:resources mapping="/images/**/" location="classpath:/images/"/>   访问工程下的图片 http://loca

ads via 小工具