目录
一、AOP概述
AOP是spring框架的第二大核心(第一大核心是IoC)。
AOP:Aspect Oriented Programming(面向切面编程)。
切面就是指某一类特定问题,所以AOP也可以理解为面向特定方法编程。拦截器、统一数据返回格式和统一异常处理, 也是AOP思想的一种实现。
AOP是一种思想,是对某一类事情的集中处理。它的实现方法有很多,有Spring AOP,也有AspectJ、CGLIB等。Spring AOP是其中的一种实现方式。
二、Spring AOP 快速使用
2.1 引入AOP依赖
在pom.xml文件中添加配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 编写AOP程序
记录Controller中每个方法的执行时间:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class TimeAspect {
/**
* 记录⽅法耗时
*/
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录⽅法执⾏开始时间
long begin = System.currentTimeMillis();
//执⾏原始⽅法
Object result = pjp.proceed();
//记录⽅法执⾏结束时间
long end = System.currentTimeMillis();
//记录⽅法执⾏耗时
log.info(pjp.getSignature() + "执⾏耗时: {}ms", end - begin);
return result;
}
}
对程序进行简单的讲解:
- @Aspect: 标识这是一个切面类;
- @Around: 环绕通知, 在目标⽅法的前后都会被执行。 后面的表达式表示对哪些方法进行增强;
- ProceedingJoinPoint.proceed() 让原始方法执行。
整个代码划分为三部分:
![](https://i-blog.csdnimg.cn/direct/24272ebfd0624bbf8a04b2bbabb66f95.png)
通过上面的程序也可以感受到AOP面向切面编程的一些优势:
- 代码无侵入:不修改原始的业务方法,就可以对原始的业务方法进行功能的增强或者功能的改变;
- 减少了重复代码;
- 提高开发效率;
- 维护方便。
三、Spring AOP 详解
3.1 Spring AOP核心概念
3.1.1 切点(Pointcut)
切点(Pointcut), 也称之为"切入点"。
Pointcut 的作用就是提供一组规则 (使用 AspectJ pointcut expression language 来描述), 告诉程序对
哪些方法来进行功能增强。
3.1.2 连接点(Join Point)
满足切点表达式规则的方法,就是连接点。也就是可以被AOP控制的方法。
package com.example.demo.controller;
@RequestMapping("/book")
@RestController
public class BookController {
@RequestMapping("/addBook")
public Result addBook(BookInfo bookInfo) {
//...代码省略
}
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId){
//...代码省略
}
@RequestMapping("/updateBook")
public Result updateBook(BookInfo bookInfo) {
//...代码省略
}
}
上述BookController 中的方法都是连接点。
切点和连接点的关系:
连接点是满足切点表达式的元素。 切点可以看做是保存了众多连接点的一个集合。比如:切点表达式:学校全体教师。连接点就是:张三,李四等各个老师。
3.1.3 通知(Advice)
通知就是具体要做的工作,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)。
![](https://i-blog.csdnimg.cn/direct/ba43cef5772146cdb36d4fccbb9b924c.png)
在AOP面向切面编程当中, 这部分重复的代码逻辑可以抽取出来单独定义,这部分代码就是通知的内容。
3.1.4 切面(Aspect)
切面(Aspect) = 切点(Pointcut) + 通知(Advice) 。通过切面就能够描述当前AOP程序需要针对于哪些方法,在什么时候执行什么样的操作。
切面既包含了通知逻辑的定义, 也包括了连接点的定义。
![](https://i-blog.csdnimg.cn/direct/3402c43a396148eebeb2aa9e4b1167c3.png)
切面所在的类,一般称为切面类(被@Aspect注解标识的类)。
3.2 通知类型
Spring中AOP的通知类型有以下几种:
- @Around: 环绕通知,此注解标注的通知方法在目标方法前、后都被执行。
- @Before: 前置通知,此注解标注的通知方法在目标方法前被执行。
- @After: 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行。
- @AfterReturning: 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行。
- @AfterThrowing: 异常后通知,此注解标注的通知方法发生异常后执行。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect
@Component
public class AspectDemo {
//前置通知
@Before("execution(* com.example.demo.controller.*.*(..))")
public void doBefore() {
log.info("执⾏ Before ⽅法");
}
//后置通知
@After("execution(* com.example.demo.controller.*.*(..))")
public void doAfter() {
log.info("执⾏ After ⽅法");
}
//返回后通知
@AfterReturning("execution(* com.example.demo.controller.*.*(..))")
public void doAfterReturning() {
log.info("执⾏ AfterReturning ⽅法");
}
//抛出异常后通知
@AfterThrowing("execution(* com.example.demo.controller.*.*(..))")
public void doAfterThrowing() {
log.info("执⾏ doAfterThrowing ⽅法");
}
//添加环绕通知
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
log.info("Around ⽅法开始执⾏");
Object result = joinPoint.proceed();
log.info("Around ⽅法结束执⾏");
return result;
}
}
测试:
package com.example.demo.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
int a = 10 / 0;
return true;
}
}
程序正常运行,查看打印日志:
程序正常运行的情况下,
@AfterThrowing
标识的通知方法不会执行。
从上图也可以看出来,
@Around
标识的通知方法包含两部分,一个"前置逻辑",一个"后置逻辑"。其中"前置逻辑" 会先于 @Before
标识的通知方法执行,"后置逻辑" 会晚于
@After
标识的通知方法执行。
程序异常时,查看日志:
程序发生异常的情况下:
- @AfterReturning 标识的通知方法不会执行, @AfterThrowing 标识的通知方法执行了。
- @Around 环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了(因为原始方法调用出异常了)。
注意事项:
- @Around 环绕通知需要调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他 通知不需要考虑目标方法执行。
- @Around 环绕通知方法的返回值, 必须指定为Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
- 一个切面类可以有多个切点。
3.3 @PointCut
上面代码:存在大量重复的切点表达式
execution(*com.example.demo.controller.*.*(..)),
Spring提供了 @PointCut
注解,把公共的切点表达式提取出来,需要用到时引用该切入点表达式即可。
上述代码就可以修改为:
Slf4j
@Aspect
@Component
public class AspectDemo {
//定义切点(公共的切点表达式)
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void pt(){}
//前置通知
@Before("pt()")
public void doBefore() {
//...代码省略
}
//后置通知
@After("pt()")
public void doAfter() {
//...代码省略
}
//返回后通知
@AfterReturning("pt()")
public void doAfterReturning() {
//...代码省略
}
//抛出异常后通知
@AfterThrowing("pt()")
public void doAfterThrowing() {
//...代码省略
}
//添加环绕通知
@Around("pt()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
//...代码省略
}
}
当切点定义使用private修饰时,仅能在当前切面类中使用,当其他切面类也要使用当前切点定义时, 就需要把private改为public。引用方式为:
全限定类名.方法名()
@Slf4j
@Aspect
@Component
public class AspectDemo2 {
//前置通知
@Before("com.example.demo.aspect.AspectDemo.pt()")
public void doBefore() {
log.info("执⾏ AspectDemo2 -> Before ⽅法");
}
}
3.4 切面优先级 @Order
在项目中定义多个切面类时,并且这些切面类的多个切入点都匹配到同一个目标方法。当目标方法运行时,这些切面类中的通知方法都会执行。这些执行也具有优先级:
当存在多个切面时,默认按照切面类的类名字母排序:
- @Before 通知:字母排名靠前的先执行
- @After 通知:字母排名靠前的后执行
但这种方式不方便管理, 我们的类名更多还是具备一定含义的。Spring 提供了一个新的注解, 来控制这些切面通知的执行顺序: @Order,如下使用:
@Aspect
@Component
@Order(1)
public class AspectDemo2 {
//...代码省略
}
@Aspect
@Component
@Order(2)
public class AspectDemo3 {
//...代码省略
}
@Aspect
@Component
@Order(3)
public class AspectDemo4 {
//...代码省略
}
@Order 注解标识的切面类, 执型顺序如下:
- @Before 通知:数字越小先执行
- @After 通知:数字越大先执行
@Order
控制切面的优先级, 先执行优先级较高的切面, 再执行优先级较低的切面, 最终执行目标方法。
![](https://i-blog.csdnimg.cn/direct/0f95971138b944979bfe023608302559.png)
3.5 切面表达式
切点表达式常见有两种表达方式:
- execution(RR):根据方法的签名来匹配
- @annotation(RR) :根据注解匹配
3.5.1 execution表达式
execution() 是最常用的切点表达式,用来匹配方法,语法为:
execution(< 访问修饰符 > < 返回类型 > < 包名 . 类名 .方 法 ( 方法参数 )> < 异常 >)
其中,访问修饰符和异常可以省略。
![](https://i-blog.csdnimg.cn/direct/68453756965749849a042df68607e682.png)
切点表达式支持通配符表达:
1. * :匹配任意字符,只匹配一个元素(返回类型, 包, 类名, 方法或者方法参数)
- 包名使用 * 表示任意包(一层包使用以个*)
- 类名使用 * 表示任意类
- 返回值使用 * 表示任意返回值类型
- 方法名使用 * 表示任意方法
- 参数使用 * 表示一个任意类型的参数
2. .. :匹配多个连续的任意符号, 可以通配任意层级的包, 或任意类型, 任意个数的参数
- 使用 .. 配置包名,标识此包以及此包下的所有子包
- 可以使用 .. 配置参数,任意个任意类型的参数
切点表达式示例:
TestController 下的 public修饰, 返回类型为String 方法名为t1, 无参方法:
execution(public String com.example.demo.controller.TestController.t1())
省略访问修饰符:
execution(String com.example.demo.controller.TestController.t1())
匹配所有返回类型:
execution(* com.example.demo.controller.TestController.t1())
匹配TestController 下的所有无参方法:
execution(* com.example.demo.controller.TestController.*())
匹配TestController 下的所有方法:
execution(* com.example.demo.controller.TestController.*(..))
匹配controller包下所有的类的所有方法:
execution(* com.example.demo.controller.*.*(..))
匹配所有包下面的TestController:
execution(* com..TestController.*(..))
匹配com.example.demo包下, 子孙包下的所有类的所有方法:
execution(* com.example.demo..*(..))
3.5.2 @annotation
execution表达式更适用有规则的,如果要匹配多个无规则的方法,比如:TestController中的t1()
和UserController中的u1()这两个方法。此时使用execution这种切点表达式来描述就不是很方便。
我们可以借助自定义注解的方式以及另一种切点表达式
@annotation
来描述这一类的切点。
实现步骤:
- 编写自定义注解
- 使用 @annotation 表达式来描述切点
- 在连接点的方法上添加自定义注解
准备测试代码:
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@RequestMapping("/t2")
public boolean t2() {
return true;
}
}
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/u1")
public String u1(){
return "u1";
}
@RequestMapping("/u2")
public String u2(){
return "u2";
}
}
3.5.2.1 自定义注解 @MyAspect
创建一个注解类(和创建Class文件一样的流程, 选择Annotation就行)
![](https://i-blog.csdnimg.cn/direct/711e2eeb1a78457da785d7c5c59182ca.png)
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAspect {
}
@Target 标识了 Annotation 所修饰的对象范围,即该注解可以用在什么地方,常用取值:ElementType.TYPE: 用于描述类、接口(包括注解类型) 或enum声明ElementType.METHOD: 描述方法ElementType.PARAMETER: 描述参数ElementType.TYPE_USE: 可以标注任意类型
@Retention 指Annotation被保留的时间长短, 标明注解的生命周期。@Retention 的取值有三种:
- RetentionPolicy.SOURCE:表示注解仅存在于源代码中,编译成字节码后会被丢弃。这意味着在运行时无法获取到该注解的信息, 只能在编译时使用。比如@SuppressWarnings,以及 lombok提供的注解 @Data , @Slf4j。
- RetentionPolicy.CLASS:编译时注解。表示注解存在于源代码和字节码中,但在运行时会被丢弃。这意味着在编译时和字节码中可以通过反射获取到该注解的信息,但在实际运行时⽆法获取。通常⽤于一些框架和工具的注解.
- RetentionPolicy.RUNTIME:运行时注解。表示注解存在于源代码,字节码和运行时中. 这意味着在编译时,字节码中和实际运行时都可以通过反射获取到该注解的信息。通常用于一些需要在运行时处理的注解,如Spring的 @Controller @ResponseBody。
3.5.2.2 切面类
使用
@annotation
切点表达式定义切点, 只对
@MyAspect 生
效,示例如下:
@Slf4j
@Component
@Aspect
public class MyAspectDemo {
//前置通知
@Before("@annotation(com.example.demo.aspect.MyAspect)")
public void before(){
log.info("MyAspect -> before ...");
}
//后置通知
@After("@annotation(com.example.demo.aspect.MyAspect)")
public void after(){
log.info("MyAspect -> after ...");
}
}
3.5.2.3 添加自定义注解
在TestController中的t1()和UserController中的u1()这两个方法上添加自定义注解
@MyAspect
, 其
它方法不添加:
@MyAspect
@RequestMapping("/t1")
public String t1() {
return "t1";
}
@MyAspect
@RequestMapping("/u1")
public String u1(){
return "u1";
}
测试接口,查看日志:发现切面通知被执行了。
Spring AOP的实现方式(常见面试题)
- 基于注解 @Aspect
- 基于自定义注解 (参考自定义注解 @annotation 部分的内容)
- 基于Spring API (通过xml配置的方式, 自从SpringBoot 广泛使用之后, 这种方法几乎看不到了)
- 基于代理来实现(更加久远的一种实现方式, 写法笨重, 不建议使用)