AOP概念
连接点
JoinPoint:可以被AOP控制的方法(暗含方法执行时的相关信息)

通知
Advice:指重复的逻辑,也就是共性功能(最终体现为一个方法)

切入点
PointCut:匹配连接点的条件,通知仅会在切入点方法执行时被应用。

切面
Aspect:描述通知与切入点的对应关系(通知+切入点)

目标对象
Target:通知所应用的对象

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。

通知类型
| Spring AOP 通知类型 | |
| @Around | 环绕通知,此注解标注的通知方法在目标方法前、后都被执行 |
| @Before | 前置通知,此注解标注的通知方法在目标方法前被执行 |
| @After | 后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 |
| @AfterReturning | 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 |
| @AfterThrowing | 异常后通知,此注解标注的通知方法发生异常后执行 |
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
log.info("around after ...");
return result;
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
程序发生异常的情况下:
- @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
- @Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)
在使用通知时的注意事项:
- @Around环绕通知需要自己调用
ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行 - @Around环绕通知方法的返回值,必须指定为
Object,来接收原始方法的返回值,否则原始方法执行完毕,是获取不到返回值的。
@PointCut
该注解的作用是将公共的切入点表达式抽取出来,需要用到时引用该切入点表达式即可。
@Slf4j
@Component
@Aspect
public class MyAspect1 {
//切入点方法(公共的切入点表达式)
@Pointcut("execution(* site.suiyue.service.*.*(..))")
private void pt(){}
//前置通知(引用切入点)
@Before("pt()")
public void before(JoinPoint joinPoint){
log.info("before ...");
}
//环绕通知
@Around("pt()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("around before ...");
//调用目标对象的原始方法执行
Object result = proceedingJoinPoint.proceed();
//原始方法在执行时:发生异常
//后续代码不在执行
log.info("around after ...");
return result;
}
//后置通知
@After("pt()")
public void after(JoinPoint joinPoint){
log.info("after ...");
}
//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("pt()")
public void afterReturning(JoinPoint joinPoint){
log.info("afterReturning ...");
}
//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("pt()")
public void afterThrowing(JoinPoint joinPoint){
log.info("afterThrowing ...");
}
}
注意:当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:
@Slf4j
@Component
@Aspect
public class MyAspect2 {
//引用MyAspect1切面类中的切入点表达式
@Before("site.suiyue.aspect.MyAspect1.pt()")
public void before(){
log.info("MyAspect2 -> before ...");
}
}
通知顺序
字典序
当定义了多个切面类,而多个切面类中多个切入点都匹配到了同一个目标方法时,这些切面内的通知方法都会执行,执行顺序默认按照切面类的类名字母排序:
目标方法前的通知方法:字母排名靠前的先执行
目标方法后的通知方法:字母排名靠前的后执行
@Slf4j
@Component
@Aspect
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
@Slf4j
@Component
@Aspect
public class MyAspect3 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect3 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect3 -> after ...");
}
}
@Slf4j
@Component
@Aspect
public class MyAspect4 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect4 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect4 -> after ...");
}
}
控制台输出:

@Order
使用@Order注解,控制通知的执行顺序:
前置通知:数字越小先执行; 后置通知:数字越小越后执行
@Slf4j
@Component
@Aspect
@Order(2) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect2 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect2 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect2 -> after ...");
}
}
@Slf4j
@Component
@Aspect
@Order(3) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect3 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect3 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect3 -> after ...");
}
}
@Slf4j
@Component
@Aspect
@Order(1) //切面类的执行顺序(前置通知:数字越小先执行; 后置通知:数字越小越后执行)
public class MyAspect4 {
//前置通知
@Before("execution(* com.itheima.service.*.*(..))")
public void before(){
log.info("MyAspect4 -> before ...");
}
//后置通知
@After("execution(* com.itheima.service.*.*(..))")
public void after(){
log.info("MyAspect4 -> after ...");
}
}

切入点表达式
作用:
主要用来决定项目中的哪些方法需要加入通知
execution
execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:
execution(访问修饰符? 返回值包名.类名.?方法名(方法参数) throws 异常?)
其中带?的表示可以省略的部分
- 访问修饰符:可省略(比如: public、protected)
- 包名.类名: 可省略
- throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)
示例
@Before("execution(void site.suiyue.service.impl.DeptServiceImpl.delete(java.lang.Integer))")
可以使用通配符描述切入点
*:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分..:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
切入点表达式的语法规则:
- 方法的访问修饰符可以省略
- 返回值可以使用
*号代替(任意返回值类型) - 包名可以使用
*号代替,代表任意包(一层包使用一个*) - 使用
..配置包名,标识此包以及此包下的所有子包 - 类名可以使用
*号代替,标识任意类 - 方法名可以使用
*号代替,表示任意方法 - 可以使用
*配置参数,一个任意类型的参数 - 可以使用
..配置参数,任意个任意类型的参数
切入点表达式示例
- 省略方法的修饰符号
execution(void site.suiyue.service.impl.DeptServiceImpl.delete(java.lang.Integer))
- 使用
*代替返回值类型
execution(* site.suiyue.service.impl.DeptServiceImpl.delete(java.lang.Integer))
- 使用
*代替包名(一层包使用一个*)
execution(* site.suiyue.*.*.DeptServiceImpl.delete(java.lang.Integer))
- 使用
..省略包名
execution(* com..DeptServiceImpl.delete(java.lang.Integer))
- 使用
*代替类名
execution(* com..*.delete(java.lang.Integer))
- 使用
*代替方法名
execution(* com..*.*(java.lang.Integer))
- 使用
*代替参数
execution(* site.suiyue.service.impl.DeptServiceImpl.delete(*))
- 使用
..省略参数
execution(* site..*.*(..))
注意事项:
- 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
execution(* site.suiyue.service.DeptService.list(..)) || execution(* site.suiyue.service.DeptService.delete(..))
切入点表达式的书写建议:
- 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头
//业务类
@Service
public class DeptServiceImpl implements DeptService {
public List<Dept> findAllDept() {
//省略代码...
}
public Dept findDeptById(Integer id) {
//省略代码...
}
public void updateDeptById(Integer id) {
//省略代码...
}
public void updateDeptByMoreCondition(Dept dept) {
//省略代码...
}
//其他代码...
}
- //匹配DeptServiceImpl类中以find开头的方法
execution(* com.itheima.service.impl.DeptServiceImpl.find*(..))
- 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
execution(* com.itheima.service.DeptService.*(..))
- 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包
execution(* com.itheima.*.*.DeptServiceImpl.find*(..))
@annotation
自定义注解:LogOperation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation{
}
业务类:DeptServiceImpl
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptMapper deptMapper;
@Override
@LogOperation //自定义注解(表示:当前方法属于目标方法)
public List<Dept> list() {
List<Dept> deptList = deptMapper.list();
//模拟异常
//int num = 10/0;
return deptList;
}
@Override
@LogOperation //自定义注解(表示:当前方法属于目标方法)
public void delete(Integer id) {
//1. 删除部门
deptMapper.delete(id);
}
}
切面类
@Slf4j
@Component
@Aspect
public class MyAspect6 {
//针对list方法、delete方法进行前置通知和后置通知
//前置通知
@Before("@annotation(com.itheima.anno.LogOperation)")
public void before(){
log.info("MyAspect6 -> before ...");
}
//后置通知
@After("@annotation(com.itheima.anno.LogOperation)")
public void after(){
log.info("MyAspect6 -> after ...");
}
}
重启SpringBoot服务,测试查询所有部门数据,查看控制台日志:

AOP使用
导入依赖
<!-- AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
编写AOP
@Component
@Aspect //当前类为切面类
@Slf4j
public class RecordTimeAspect {
@Around("execution(* site.suiyue.service.impl.DeptServiceImpl.*(..))")
public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
//记录方法执行开始时间
long begin = System.currentTimeMillis();
//执行原始方法
Object result = pjp.proceed();
//记录方法执行结束时间
long end = System.currentTimeMillis();
//计算方法执行耗时
log.info("方法执行耗时: {}毫秒",end-begin);
return result;
}
}
自定义注解@LogOperation
/**
* 自定义注解,用于标识哪些方法需要记录日志
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogOperation {
}
定义AOP记录日志的切面类
package com.sky.dto;
import com.itheima.anno.LogOperation;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
/**
* 当 Spring 解析这个切面时,它发现:切点表达式
* 里写了一个变量log,通知方法的参数列表中有一个
* 名为log、类型为LogOperation的参数
* 于是 Spring 就明白了:这个log变量应该
* 被绑定到目标方法上的LogOperation注解实例。
* 因此,切点表达式@annotation(log)实际的含义是:
* 匹配所有被一个类型为LogOperation的注解所标注的
* 方法,并且把该注解实例传递给名为log的参数。
*/
@Around("@annotation(log)")
public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable {
// 记录开始时间
long startTime = System.currentTimeMillis();
// 执行方法
Object result = joinPoint.proceed();
// 当前时间
long endTime = System.currentTimeMillis();
// 耗时
long costTime = endTime - startTime;
// 构建日志对象
OperateLog operateLog = new OperateLog();
operateLog.setOperateEmpId(getCurrentUserId()); // 需要实现 getCurrentUserId 方法
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setClassName(joinPoint.getTarget().getClass().getName());
operateLog.setMethodName(joinPoint.getSignature().getName());
operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
operateLog.setReturnValue(result.toString());
operateLog.setCostTime(costTime);
// 插入日志
operateLogMapper.insert(operateLog);
return result;
}
// 示例方法,获取当前用户ID
private int getCurrentUserId() {
// 这里应该根据实际情况从认证信息中获取当前登录用户的ID
return 1; // 示例返回值
}
}
在需要记录的日志的Controller层的方法上,加上注解 @LogOperation
@RestController
@RequestMapping("/clazzs")
public class ClazzController {
@Autowired
private ClazzService clazzService;
//新增班级
@LogOperation
@PostMapping
public Result save(@RequestBody Clazz clazz){
clazzService.save(clazz);
return Result.success();
}
}
连接点
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。
对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型

对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型

ThreadLocal
定义:
- ThreadLocal并不是一个Thread,而是Thread的局部变量。
- ThreadLocal为每个线程提供一份单独的存储空间,具有线程隔离的效果,不同的线程之间不会相互干扰。

常见方法:
public void set(T value)设置当前线程的线程局部变量的值public T get()返回当前线程所对应的线程局部变量的值public void remove()移除当前线程的线程局部变量
案例:
- 记录当前登录员工
- 定义ThreadLocal操作的工具类,用于操作当前登录员工ID。
在 site.suiyue.utils 引入工具类 CurrentHolder
package com.itheima.utils;
public class CurrentHolder {
private static final ThreadLocal<Integer> CURRENT_LOCAL = new ThreadLocal<>();
public static void setCurrentId(Integer employeeId) {
CURRENT_LOCAL.set(employeeId);
}
public static Integer getCurrentId() {
return CURRENT_LOCAL.get();
}
public static void remove() {
CURRENT_LOCAL.remove();
}
}
- 在
TokenFilter中,解析完当前登录员工ID,将其存入ThreadLocal(用完之后需将其删除)。
package com.itheima.filter;
import com.itheima.utils.CurrentHolder;
import com.itheima.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.*;
import jakarta.servlet.annotation.WebFilter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
@Slf4j
@WebFilter(urlPatterns = "/*")
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
//1. 获取请求的url地址
String uri = request.getRequestURI(); // /employee/login
//String url = request.getRequestURL().toString(); // http://localhost:8080/employee/login
//2. 判断是否是登录请求, 如果url地址中包含 login, 则说明是登录请求, 放行
if (uri.contains("login")) {
log.info("登录请求, 放行");
filterChain.doFilter(request, response);
return;
}
//3. 获取请求中的token
String token = request.getHeader("token");
//4. 判断token是否为空, 如果为空, 响应401状态码
if (token == null || token.isEmpty()) {
log.info("token为空, 响应401状态码");
response.setStatus(401); // 响应401状态码
return;
}
//5. 如果token不为空, 调用JWtUtils工具类的方法解析token, 如果解析失败, 响应401状态码
try {
Claims claims = JwtUtils.parseJWT(token);
Integer empId = Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId);log.info("token解析成功, 放行");
} catch (Exception e) {
log.info("token解析失败, 响应401状态码");
response.setStatus(401);
return;
}
//6. 放行
filterChain.doFilter(request, response);
//7. 清空当前线程绑定的id
CurrentHolder.remove();
}
}
- 在AOP程序中,从ThreadLocal中获取当前登录员工的ID。
package com.itheima.aop;
import com.itheima.anno.LogOperation;
import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import com.itheima.utils.CurrentHolder;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
@Aspect
@Component
public class OperationLogAspect {
@Autowired
private OperateLogMapper operateLogMapper;
// 环绕通知
@Around("@annotation(log)")
public Object around(ProceedingJoinPoint joinPoint, LogOperation log) throws Throwable {
// 记录开始时间
long startTime = System.currentTimeMillis();
// 执行方法
Object result = joinPoint.proceed();
// 当前时间
long endTime = System.currentTimeMillis();
// 耗时
long costTime = endTime - startTime;
// 构建日志对象
OperateLog operateLog = new OperateLog();
operateLog.setOperateEmpId(getCurrentUserId()); // 需要实现 getCurrentUserId 方法
operateLog.setOperateTime(LocalDateTime.now());
operateLog.setClassName(joinPoint.getTarget().getClass().getName());
operateLog.setMethodName(joinPoint.getSignature().getName());
operateLog.setMethodParams(Arrays.toString(joinPoint.getArgs()));
operateLog.setReturnValue(result.toString());
operateLog.setCostTime(costTime);
// 插入日志
operateLogMapper.insert(operateLog);
return result;
}
// 示例方法,获取当前用户ID
private int getCurrentUserId() {
return CurrentHolder.getCurrentId();
}
}
代码优化完毕之后,重新启动服务测试。就可以看到,可以获取到不同的登录用户信息了。
在同一个线程/同一个请求中,进行数据共享就可以使用 ThreadLocal。
因为在 Spring Boot(准确说是其默认的 Tomcat 容器)处理 Web 请求时,每一个独立的 HTTP 请求,在默认同步处理的场景下,都会分配到一个专属的 Tomcat 工作线程来全程处理
Comments NOTHING