欢迎关注个人主页:逸狼
创造不易,可以点点赞吗
如有错误,欢迎指出~
AOP是Spring框架的第⼆⼤核⼼(第⼀⼤核⼼是 IoC)
什么是AOP?
• AspectOrientedProgramming(⾯向切⾯编程) 什么是⾯向切⾯编程呢?
切⾯就是指某⼀类特定问题,所以AOP也可以理解为⾯向特定⽅法编程.
什么是⾯向特定⽅法编程呢?⽐如"登录校验",就是⼀类特定问题.登录校验拦截器,就是对"登录校验"这类问题的统⼀处理.所以,拦截器也是AOP的⼀种应⽤.AOP是⼀种思想,拦截器是AOP 思想的⼀种实现.Spring框架实现了这种思想,提供了拦截器技术的相关接⼝.
同样的,统⼀数据返回格式和统⼀异常处理,也是AOP思想的⼀种实现. 简单来说: AOP是⼀种思想,是对某⼀类事情的集中处理.
什么是SpringAOP?
AOP是⼀种思想,它的实现⽅法有很多,有SpringAOP,也有AspectJ、CGLIB等. SpringAOP是其中的⼀种实现⽅式. 学会了统⼀功能之后,是不是就学会了SpringAOP呢,当然不是. 拦截器作⽤的维度是URL(⼀次请求和响应),@ControllerAdvice 应⽤场景主要是全局异常处理 (配合⾃定义异常效果更佳),数据绑定,数据预处理.AOP作⽤的维度更加细致(可以根据包、类、⽅法 名、参数等进⾏拦截),能够实现更加复杂的业务逻辑.
举个例⼦: 我们现在有⼀个项⽬,项⽬中开发了很多的业务功能

比如想要记录每个方法的耗时 ,记录开始时间,结束时间,再计算耗时,如果是常规写法,每个方法都要重复书写这些代码,AOP就是将这些重复代码提取出来,
AOP可以在不改变原有的代码的前提下, 增强原来方法的功能(⽆侵⼊性:解耦)
//通过id查询图书
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId){
long start = System.currentTimeMillis();
log.info("获取图书信息, bookId: "+ bookId);
//参数校验,不能为null,不能<=0...省略
BookInfo bookInfo = bookService.queryBookById(bookId);
long end = System.currentTimeMillis();
log.info("queryBookById 耗时: " + (end - start) + "ms");
return bookInfo;
}
SpringAOP快速⼊⻔
学习什么是AOP后,我们先通过下⾯的程序体验下AOP的开发,并掌握Spring中AOP的开发步骤.
需求:统计图书系统各个接⼝⽅法的执⾏时间.
引⼊AOP依赖
在pom.xml⽂件中添加配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
统计执⾏时间
package com.example.demo.aspect;
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;
@Aspect
@Component
@Slf4j
public class TimeRecordAspect {
//作用域,执行路径
@Around("execution(* com.example.demo.controller.*.*(..))")
public Object timeRecord(ProceedingJoinPoint pjt){
//1.记录开始时间
//2.执行目标方法时间
//3.记录结束时间
//4.返回结果
long start = System.currentTimeMillis();
//执行目标方法
Object o = null;
try {
o = pjt.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
log.info(pjt.getSignature() + "耗时: "+ (end - start)+ "ms");
return o;
}
}
-
- @Aspect:标识这是⼀个切⾯类
-
- @Around:环绕通知,在⽬标⽅法的前后都会被执⾏.后⾯的表达式表⽰对哪些⽅法进⾏增强.
-
- ProceedingJoinPoint.proceed()让原始⽅法执⾏

我们通过AOP⼊⻔程序完成了业务接⼝执⾏耗时的统计. 通过上⾯的程序,我们也可以感受到AOP⾯向切⾯编程的⼀些优势:
- 代码⽆侵⼊:不修改原始的业务⽅法,就可以对原始的业务⽅法进⾏了功能的增强或者是功能的改变
- 减少了重复代码
- 提⾼开发效率
- 维护⽅便
SpringAOP核⼼概念
切点(Pointcut)
切点(Pointcut),也称之为"切⼊点" Pointcut的作⽤就是提供⼀组规则(使⽤AspectJpointcutexpressionlanguage来描述),告诉程序对 哪些⽅法来进⾏功能增强.
表达式execution(* com.example.demo.controller.*.*(..)) 就是切点表达式
连接点(JoinPoint)
满⾜切点表达式规则的⽅法,就是连接点.也就是可以被AOP控制的具体⽅法 以⼊⻔程序举例,所有com.example.demo.controller 路径下的⽅法,都是连接点.
切点和连接点的关系 :
连接点是满⾜切点表达式的元素.
切点可以看做是保存了众多连接点的⼀个集合.
通知(Advice)
通知就是具体要做的⼯作,指哪些重复的逻辑,也就是共性功能(最终体现为⼀个⽅法) ⽐如上述程序中记录业务⽅法的耗时时间,就是通知.
切⾯(Aspect)
切⾯(Aspect)=切点(Pointcut)+通知(Advice) 通过切⾯就能够描述当前AOP程序需要针对于哪些⽅法,在什么时候执⾏什么样的操作.切⾯既包含了通知逻辑的定义,也包括了连接点的定义.
切⾯所在的类,我们⼀般称为切⾯类(被@Aspect注解标识的类
通知类型
Spring中AOP的通知类型有以下⼏种:
- @Around:环绕通知,此注解标注的通知⽅法在⽬标⽅法前,后都被执⾏
- @Before:前置通知,此注解标注的通知⽅法在⽬标⽅法前被执⾏
- @After:后置通知,此注解标注的通知⽅法在⽬标⽅法后被执⾏,⽆论是否有异常都会执⾏
- @AfterReturning:返回后通知,此注解标注的通知⽅法在⽬标⽅法后被执⾏,有异常不会执⾏
- @AfterThrowing:异常后通知,此注解标注的通知⽅法发⽣异常后执⾏
示例代码
package com.example.demo.aspect;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Slf4j
@Component
@Aspect
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;
}
}

程序正常运⾏的情况下,@AfterThrowing 标识的通知⽅法不会执⾏
从上图也可以看出来,@Around 标识的通知⽅法包含两部分,⼀个"前置逻辑",⼀个"后置逻辑".其 中"前置逻辑"会先于 @Before 标识的通知⽅法执⾏,"后置逻辑"会晚于 @After 标识的通知⽅法执⾏

如果发生异常

程序发⽣异常的情况下:
@AfterReturning 标识的通知⽅法不会执⾏, @AfterThrowing 标识的通知⽅法执⾏了
@Around 环绕通知中原始⽅法调⽤时有异常,通知中的环绕后的代码逻辑也不会在执⾏了(因为 原始⽅法调⽤出异常了)

@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() {
//...代码省略
}
当切点定义使⽤private修饰时,仅能在当前切⾯类中使⽤,当其他切⾯类也要使⽤当前切点定义时,就需 要把private改为public.引⽤⽅式为:全限定类名.⽅法名()
public class TimeRecordAspect {
// @Around("execution(* com.example.demo.controller.*.*(..))")
@Around("com.example.demo.aspect.AspectDemo.pt()")
public Object timeRecord(ProceedingJoinPoint pjt){
...}
切⾯优先级@Order
当我们在⼀个项⽬中,定义了多个切⾯类时,并且这些切⾯类的多个切⼊点都匹配到了同⼀个⽬标⽅法. 当⽬标⽅法运⾏的时候,这些切⾯类中的通知⽅法都会执⾏,那么这⼏个通知⽅法的执⾏顺序是什么样 的呢?

存在多个切⾯类时,默认按照切⾯类的类名字⺟排序: • @Before 通知:字⺟排名靠前的先执⾏ • @After 通知:字⺟排名靠前的后执⾏
但这种⽅式不⽅便管理,我们的类名更多还是具备⼀定含义的. Spring给我们提供了⼀个新的注解,来控制这些切⾯通知的执⾏顺序:@Order使⽤⽅式如下:
@Slf4j
@Component
@Aspect
@Order(3)
public class demo1 {
...
}
...
@Order(2)
public class demo2 {
...}
...
@Order(1)
public class demo3 {
...}

@Order 控制切⾯的优先级,先执⾏优先级较⾼的切⾯,再执⾏优先级较低的切⾯,最终执⾏⽬标⽅法.数字越小,优先级越高

切点表达式
上⾯的代码中,我们⼀直在使⽤切点表达式来描述切点.下⾯我们来介绍⼀下切点表达式的语法. 切点表达式常⻅有两种表达⽅式
execution
@annotation
execution表达式
execution()是最常⽤的切点表达式,⽤来匹配⽅法,语法为:
execution(访问修饰符> 返回类型> 包名.类名.⽅法(⽅法参数)> 异常>)
其中:访问 修饰符 和 异常 可以省略


切点表达式⽰例
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..*(..))
@annotation
execution表达式更适⽤有规则的,如果我们要匹配多个⽆规则的⽅法 呢,⽐如:TestController中的t1() 和UserController中的u1()这两个⽅法. 这个时候我们使⽤execution这种切点表达式来描述就不是很⽅便了. 我们可以借助**⾃定义注解的**⽅式以及另⼀种切点表达式 @annotation 来描述这⼀类的切点
实现步骤:
-
编写⾃定义注解
-
使⽤ @annotation 表达式来描述切点
-
在连接点的⽅法上添加⾃定义注解
⾃定义注解
@TimeRecord 创建⼀个注解类(和创建Class⽂件⼀样的流程,选择Annotation就可以了)
package com.example.demo.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)//运行时
@Target({ElementType.METHOD})//表示作用在方法上
public @interface TimeRecord {
}
@Target 标识了 Annotation 所修饰的对象范围,即该注解可以⽤在什么地⽅.
@Retention 指Annotation被保留的时间⻓短,标明注解的⽣命周期
切⾯类
使⽤ @annotation 切点表达式定义切点,只对@TimeRecord⽣效
@Aspect
@Component
@Slf4j
public class TimeRecordAspect {
@Around("@annotation(com.example.demo.aspect.TimeRecord)")
public Object timeRecord(ProceedingJoinPoint pjt){
//1.记录开始时间
//2.执行目标方法时间
//3.记录结束时间
//4.返回结果
long start = System.currentTimeMillis();
log.info("timeRecord.Around ⽅法开始执⾏");
//执行目标方法
Object o = null;
try {
o = pjt.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
log.info(pjt.getSignature() + "耗时: "+ (end - start)+ "ms");
log.info("timeRecord.Around ⽅法结束执⾏");
return o;
}
}
在TestController中的t1()和UserController中的u1()这两个⽅法上添加⾃定义注解@TimeRecord ,其他⽅法不添加
@RequestMapping("/test")
@RestController
@Slf4j
public class TestController {
@TimeRecord
@RequestMapping("/t1")
public String t1(){
log.info("执行t1");
return "t1";
}
@RequestMapping("/t2")
public int t2(){
log.info("执行t2");
return "t2";
}
@RequestMapping("/user")
@RestController
@Slf4j
public class UserController {
@TimeRecord
@RequestMapping("/u1")
public String u1(){
log.info("执行u1");
return "u1";
}
@RequestMapping("/u2")
public String u2(){
log.info("执行u2");
return "u2";
}
}

如果要让所有带有@RequestMapping注解的方法都实现记录时间,只需要将上面的切点表达式换成以下
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)")