springAOP

通知执行顺序

Created: 9/2/2025 14:34
Updated: 9/2/2025 23:15
Exported: 9/2/2025 23:15
Link: https://chatgpt.com/c/68b68fe0-9498-8324-90b3-982caf2cb49a

Prompt:spring aop是什么

Response:

Spring AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 框架提供的一种编程思想和实现机制,它主要用来处理程序中的横切关注点(cross-cutting concerns),比如日志记录、安全控制、事务管理、性能监控等。

在传统的面向对象编程(OOP)里,这些横切逻辑通常会分散在多个类的多处方法中,导致代码冗余、耦合度高、不易维护。AOP 的目标就是 把这些横切逻辑抽离出来,以切面的形式统一管理


核心概念

Spring AOP 里有几个重要的概念:

  1. 切面(Aspect)
    横切逻辑的模块化单元,通常是一个类(比如日志切面、事务切面)。
  2. 连接点(Join Point)
    程序运行中的某个点,比如方法调用、异常抛出等。在 Spring AOP 中,连接点一般就是方法的执行。
  3. 切入点(Pointcut)
    用来选择一组连接点的表达式(比如指定某个包下的所有方法)。
  4. 通知(Advice)
    在连接点上要执行的动作。Spring AOP 支持多种通知类型:
    • Before:方法执行前
    • After Returning:方法成功执行后
    • After Throwing:方法抛异常后
    • **After (Finally)**:方法执行后(无论是否异常)
    • Around:环绕通知(在方法前后都可以执行逻辑)
  5. 目标对象(Target Object)
    被代理的业务类对象。
  6. 代理对象(Proxy)
    AOP 运行时生成的对象,封装了目标对象和切面逻辑。
    • JDK 动态代理(基于接口)
    • CGLIB 动态代理(基于子类)

工作原理

Spring AOP 是基于 代理模式 实现的:

  • 当调用目标对象的方法时,Spring 实际上调用的是代理对象;
  • 代理对象会在合适的连接点插入切面逻辑(Advice),然后再执行目标方法;
  • 从而达到在不修改业务代码的情况下实现横切功能。

示例

假设要给所有 service 包下的方法加日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Aspect
@Component
public class LogAspect {

@Pointcut("execution(* com.example.service..*(..))")
public void serviceMethods() {}

@Before("serviceMethods()")
public void beforeLog(JoinPoint joinPoint) {
System.out.println("调用方法: " + joinPoint.getSignature().getName());
}

@AfterReturning("serviceMethods()")
public void afterLog(JoinPoint joinPoint) {
System.out.println("方法执行完成: " + joinPoint.getSignature().getName());
}
}

当你调用 UserService.saveUser() 时,Spring 会先执行切面里的日志逻辑,再执行实际的 saveUser 方法。


👉 总结一句话:
Spring AOP 是 Spring 提供的面向切面编程实现,用于把横切关注点抽离出来,通过代理机制在运行时织入业务代码,从而实现解耦和代码复用。

Prompt:同一类型的通知的执行顺序

Response:

好问题 👍

在 Spring AOP 里,如果你在同一个连接点上定义了 多个相同类型的通知(比如多个 @Before),它们的执行顺序是可以控制的,否则会存在默认顺序。


1. 默认规则

  • 同一个切面类中,相同类型的通知执行顺序由 方法名的字典序(ASCII 排序) 决定。
    例如:
    1
    2
    3
    4
    @Before("execution(* com.example.service..*(..))")
    public void aBefore() { System.out.println("aBefore"); }
    @Before("execution(* com.example.service..*(..))")
    public void bBefore() { System.out.println("bBefore"); }
    输出顺序是:aBefore → bBefore
  • 不同切面类中,相同类型的通知执行顺序是 不确定的,除非你显式指定顺序。

2. 指定执行顺序

Spring 提供了两种方式来控制多个切面或通知的执行顺序:

方式一:@Order

你可以在 切面类 上加 @Order 注解(值越小优先级越高)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Aspect
@Order(1)
@Component
public class FirstAspect {
@Before("execution(* com.example.service..*(..))")
public void before() {
System.out.println("FirstAspect Before");
}
}

@Aspect
@Order(2)
@Component
public class SecondAspect {
@Before("execution(* com.example.service..*(..))")
public void before() {
System.out.println("SecondAspect Before");
}
}

输出顺序:FirstAspect Before → SecondAspect Before

方式二:实现 Ordered 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
@Aspect
@Component
public class ThirdAspect implements Ordered {
@Override
public int getOrder() {
return 0; // 越小优先级越高
}

@Before("execution(* com.example.service..*(..))")
public void before() {
System.out.println("ThirdAspect Before");
}
}

3. 特殊情况:@Around

  • @Around 通知的优先级最高,因为它可以包裹整个方法调用。
  • 在一个切面类中,@Around 会先于 @Before@After 执行。
  • 当多个切面都定义了 @Around,它们的嵌套顺序也受 @Order/Ordered 控制。

总结

  • 同类通知(同一个切面类):按方法名字典序执行
  • 不同切面类:用 @OrderOrdered 控制,值越小优先级越高
  • @Around 通知优先级最高,可以控制方法整体执行流程

Prompt:切入点表达式的写法

Response:

切入点表达式(Pointcut Expression)是 Spring AOP 用来匹配目标方法的规则,写法灵活但有套路。常用的就是基于 AspectJ 表达式


1. 常见的切入点表达式类型

(1)execution —— 最常用的方式

匹配方法执行。语法:

1
execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(param-pattern) throws-pattern?)
  • modifiers-pattern:方法修饰符(可选),如 public
  • return-type-pattern:返回值类型,* 代表任意类型。
  • declaring-type-pattern:方法所属类(可选)。
  • method-name-pattern:方法名,* 代表任意方法。
  • param-pattern:方法参数。
  • throws-pattern:抛出的异常类型(可选)。

示例:

1
2
3
4
5
6
7
8
execution(* com.example.service.UserService.*(..)) 
// UserService 类的所有方法

execution(public * com.example.service..*.*(..))
// com.example.service 包及子包下所有 public 方法

execution(* save*(..))
// 以 save 开头的任意方法

(2)within —— 限定包或类

1
2
3
4
5
within(com.example.service.*)
// service 包下的所有类的方法

within(com.example.service..*)
// service 包及其子包下的所有类的方法

(3)this / target

  • this:匹配 代理对象 实现的接口类型。
  • target:匹配 目标对象(真实对象)的类型。
1
2
this(com.example.service.UserService)
target(com.example.service.UserServiceImpl)

(4)args —— 按参数类型匹配

1
2
3
4
5
args(String)
// 只有一个 String 类型参数的方法

args(String, ..)
// 第一个参数是 String,后面任意参数

(5)@annotation —— 按方法注解匹配

1
2
@annotation(org.springframework.transaction.annotation.Transactional)
// 匹配带有 @Transactional 注解的方法

(6)@within / @target / @args

  • @within:类上有指定注解。
  • @target:目标对象的类有指定注解。
  • @args:方法参数的类有指定注解。
1
2
@within(org.springframework.stereotype.Service)
// 匹配带有 @Service 注解的类中的所有方法

2. 组合表达式

切入点可以用逻辑运算符组合:

  • &&:与
  • ||:或
  • !:非
1
2
3
execution(* com.example.service..*.*(..)) 
&& @annotation(org.springframework.transaction.annotation.Transactional)
// service 包下带有 @Transactional 的方法

3. 实用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 匹配 service 包下所有类的所有方法
@Pointcut("execution(* com.example.service..*(..))")

// 匹配任意以 "find" 开头的方法
@Pointcut("execution(* find*(..))")

// 匹配带有 @Log 注解的方法
@Pointcut("@annotation(com.example.annotation.Log)")

// 匹配 UserServiceImpl 类中所有方法
@Pointcut("within(com.example.service.UserServiceImpl)")

// 匹配参数中第一个为 Long 的方法
@Pointcut("args(Long,..)")

总结口诀

  • 常用execution(方法)
  • 按类/包within
  • 按对象类型this / target
  • 按参数args
  • 按注解@annotation / @within

Prompt:什么是threadlocal

Response:

很好,这个问题经常出现在面试里 👍


1. 什么是 ThreadLocal

ThreadLocalJava 提供的一种线程本地存储工具类,可以让 每个线程都拥有自己独立的变量副本,从而实现 线程隔离

换句话说:

  • 普通变量是 多个线程共享 的;
  • ThreadLocal 变量是 每个线程独享 的(线程自己存取自己的那份数据)。

2. 工作原理

  • 每个线程(Thread)内部都有一个 ThreadLocalMap,用来存储本线程的变量副本;
  • 当调用 threadLocal.set(value) 时,实际上是把 (ThreadLocal实例 -> value) 这一对数据放到 当前线程的 ThreadLocalMap 里;
  • 当调用 threadLocal.get() 时,就会从当前线程的 ThreadLocalMap 里取出对应的值;
  • 因此,不同线程互不干扰。

3. 简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ThreadLocalDemo {
// 定义一个 ThreadLocal 变量
private static final ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {
Runnable task = () -> {
// 每个线程设置自己独立的值
threadLocal.set((int) (Math.random() * 100));
System.out.println(Thread.currentThread().getName() + " => " + threadLocal.get());
// 用完最好 remove,避免内存泄漏
threadLocal.remove();
};

new Thread(task, "Thread-A").start();
new Thread(task, "Thread-B").start();
}
}

输出示例:

1
2
Thread-A => 42
Thread-B => 87

(每个线程看到的值不同,且不会互相影响)


4. 应用场景

  • 数据库连接、事务管理:保证一个线程在一次请求中使用同一个连接。
  • Session 管理:为每个线程保存用户信息。
  • Spring 事务管理:底层就大量用到 ThreadLocal 保存事务状态。

5. 注意事项

  • 内存泄漏风险
    • ThreadLocalMap 的 key 是弱引用,但 value 是强引用,如果线程长期不结束,value 可能无法回收。
    • 解决办法:用完一定要调用 remove()
  • 不要把大对象放进 ThreadLocal,否则容易导致内存占用过高。

一句话总结
ThreadLocal 提供了一种 以线程为作用域存储数据 的机制,每个线程都维护自己的副本,常用于解决多线程环境下的 数据隔离问题,是实现无锁线程安全的一种手段。


要不要我帮你画一张 ThreadLocal 工作原理示意图(线程、ThreadLocalMap、变量副本的关系)?

Prompt:我用拦截器怎么remove:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Component
@Slf4j
public class TokenInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

//3. 获取请求头中的令牌(token)。
String jwt = request.getHeader("token");

//4. 判断令牌是否存在,如果不存在,返回错误结果(未登录)。
if(!StringUtils.hasLength(jwt)){ //jwt为空
log.info("获取到jwt令牌为空, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}

//5. 解析token,如果解析失败,返回错误结果(未登录)。
try {
Claims claims = JwtUtils.parseJWT(jwt);
Integer empId = Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId); //存入
log.info("解析到员工ID为:{}", empId);
} catch (Exception e) {
e.printStackTrace();
log.info("解析令牌失败, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}

//6. 放行。
log.info("令牌合法, 放行");
return true;
}

}

Response:

你这个场景里用到了 ThreadLocal(应该是在 CurrentHolder 里用的),**关键点就是在请求结束时要记得调用 remove()**,否则线程池里的线程不会释放这个变量,会导致内存泄漏或用户信息串号。

在 Spring 的拦截器里,你可以在 afterCompletion 方法里清理:


✅ 修改后的拦截器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Component
@Slf4j
public class TokenInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1. 获取请求头中的令牌(token)
String jwt = request.getHeader("token");

if (!StringUtils.hasLength(jwt)) {
log.info("获取到jwt令牌为空, 返回错误结果");
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}

// 2. 解析token
try {
Claims claims = JwtUtils.parseJWT(jwt);
Integer empId = Integer.valueOf(claims.get("id").toString());
CurrentHolder.setCurrentId(empId); // 存入 ThreadLocal
log.info("解析到员工ID为:{}", empId);
} catch (Exception e) {
log.info("解析令牌失败, 返回错误结果", e);
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
return false;
}

log.info("令牌合法, 放行");
return true;
}

/**
* 请求完成后回调(不管是否抛异常都会执行)
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
CurrentHolder.removeCurrentId(); // 清理 ThreadLocal
log.info("清理当前线程的用户信息");
}
}

CurrentHolder 写法(示例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class CurrentHolder {
private static final ThreadLocal<Integer> currentId = new ThreadLocal<>();

public static void setCurrentId(Integer id) {
currentId.set(id);
}

public static Integer getCurrentId() {
return currentId.get();
}

public static void removeCurrentId() {
currentId.remove();
}
}

这样,每次请求处理完毕后,无论成功失败,afterCompletion 都会被调用,从而清理掉 ThreadLocal 中的数据,避免 内存泄漏数据串号

Prompt:总结一下我学了什么

Response:

👌 来个简洁总结,你这几轮问答里主要学了这些:

  1. Spring AOP 基础
    • 解决横切关注点(日志、事务、权限)。
    • 核心概念:切面、连接点、切入点、通知、目标对象、代理对象。
    • 基于 JDK 动态代理 / CGLIB 实现。
  2. AOP 通知执行顺序
    • 同一类里:同类型通知按方法名字典序执行。
    • 不同切面:用 @OrderOrdered 控制,值越小优先级越高。
    • @Around 优先级最高,可控制整个方法调用流程。
  3. 切入点表达式写法
    • 常用:execution(方法)、within(类/包)、args(参数)、@annotation(注解)。
    • 可以用 &&||! 组合。
    • 熟悉了几个常用场景的写法。
  4. ThreadLocal
    • 每个线程有独立副本,解决线程隔离问题。
    • 常见应用:事务管理、保存用户信息、数据库连接。
    • 注意:要及时 remove(),避免内存泄漏。
  5. 在拦截器中使用 ThreadLocal
    • preHandle 里存入用户信息(比如用户ID)。
    • afterCompletion 里调用 remove() 清理,避免线程池复用导致的数据串号。

👉 一句话总结:
你学会了 Spring AOP 的原理和使用(切面/切入点/通知顺序),掌握了 ThreadLocal 的作用和风险点,并且能在 拦截器里结合 ThreadLocal 正确存取和清理用户信息