Skip to main content

Spring AOP 详解

AOP(Aspect-Oriented Programming,面向切面编程)是Spring框架的重要特性,它允许开发者将横切关注点(如日志、事务、安全等)从业务逻辑中分离出来,实现关注点的模块化。

核心价值

Spring AOP = 关注点分离 + 代码复用 + 模块化设计 + 动态代理

  • 🔍 关注点分离:将横切关注点与业务逻辑解耦
  • 🔄 代码复用:消除重复代码,提高可维护性
  • 📦 模块化设计:独立管理通用功能,易于扩展
  • 🔧 动态代理:运行时动态增强,无侵入性

1. AOP基础概念

1.1 什么是AOP?

AOP是一种编程范式,它通过预编译方式和运行期动态代理实现程序功能的统一维护。AOP的核心思想是将横切关注点从主业务逻辑中分离出来。

传统编程 vs AOP编程

传统编程方式
java
1// 传统方式 - 业务逻辑和横切关注点混合
2public class UserService {
3 public void createUser(User user) {
4 // 日志记录
5 System.out.println("开始创建用户: " + user.getName());
6
7 // 业务逻辑
8 userRepository.save(user);
9
10 // 日志记录
11 System.out.println("用户创建完成: " + user.getName());
12 }
13
14 public void updateUser(User user) {
15 // 日志记录
16 System.out.println("开始更新用户: " + user.getName());
17
18 // 业务逻辑
19 userRepository.update(user);
20
21 // 日志记录
22 System.out.println("用户更新完成: " + user.getName());
23 }
24}

传统方式的缺点

  • 横切关注点代码与业务逻辑混合
  • 代码重复,难以维护
  • 职责不明确,违反单一职责原则
  • 修改横切逻辑需要修改多处代码

1.2 AOP核心概念

AOP核心概念

概念说明示例
Aspect(切面)横切关注点的模块化@Aspect注解的类
Join Point(连接点)程序执行过程中的某个特定点方法调用、异常抛出
Pointcut(切点)匹配连接点的表达式execution(* com.example.service.*.*(..))
Advice(通知)在切点处要执行的代码@Before@After@Around
Target Object(目标对象)被代理的对象业务服务类
Proxy(代理)AOP框架创建的对象Spring自动创建
Weaving(织入)将切面应用到目标对象的过程编译时、类加载时、运行时
AOP概念示例代码
AOP概念示例
java
1// 1. Aspect(切面)
2@Aspect
3@Component
4public class LoggingAspect {
5 // 切面实现
6}
7
8// 2. Pointcut(切点)
9@Pointcut("execution(* com.example.service.*.*(..))")
10public void serviceMethods() {}
11
12// 3. Advice(通知)
13@Before("serviceMethods()")
14public void beforeAdvice() {
15 System.out.println("方法执行前");
16}
17
18// 4. Target Object(目标对象)
19@Service
20public class UserService {
21 // 目标对象
22}

2. AOP实现原理

2.1 动态代理机制

Spring AOP基于动态代理实现,支持JDK动态代理和CGLIB代理:

JDK动态代理示例
java
1public class JdkDynamicProxyExample {
2
3 public static void main(String[] args) {
4 UserService userService = new UserService();
5
6 // 创建代理对象
7 UserService proxy = (UserService) Proxy.newProxyInstance(
8 UserService.class.getClassLoader(),
9 new Class<?>[]{UserService.class},
10 new LoggingInvocationHandler(userService)
11 );
12
13 // 调用代理方法
14 proxy.createUser(new User("张三"));
15 }
16}
17
18class LoggingInvocationHandler implements InvocationHandler {
19 private final Object target;
20
21 public LoggingInvocationHandler(Object target) {
22 this.target = target;
23 }
24
25 @Override
26 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
27 System.out.println("方法执行前: " + method.getName());
28 Object result = method.invoke(target, args);
29 System.out.println("方法执行后: " + method.getName());
30 return result;
31 }
32}

JDK动态代理特点

  • 要求目标类必须实现接口
  • 基于Java反射机制
  • 生成的代理类实现了相同的接口
  • 通过InvocationHandler拦截方法调用

2.2 代理选择机制

Spring AOP根据目标对象类型自动选择代理方式:

代理选择逻辑
java
1public class ProxySelectionLogic {
2
3 public static Object createProxy(Object target) {
4 // 如果目标对象实现了接口,使用JDK动态代理
5 if (target.getClass().getInterfaces().length > 0) {
6 return createJdkProxy(target);
7 } else {
8 // 否则使用CGLIB代理
9 return createCglibProxy(target);
10 }
11 }
12
13 private static Object createJdkProxy(Object target) {
14 // JDK动态代理实现
15 return Proxy.newProxyInstance(
16 target.getClass().getClassLoader(),
17 target.getClass().getInterfaces(),
18 new LoggingInvocationHandler(target)
19 );
20 }
21
22 private static Object createCglibProxy(Object target) {
23 // CGLIB代理实现
24 Enhancer enhancer = new Enhancer();
25 enhancer.setSuperclass(target.getClass());
26 enhancer.setCallback(new LoggingMethodInterceptor());
27 return enhancer.create();
28 }
29}

3. 切点表达式

3.1 切点表达式语法

Spring AOP使用AspectJ切点表达式语言:

基础切点表达式示例
java
1@Aspect
2@Component
3public class BasicPointcutExamples {
4
5 // 1. 方法执行切点 - 匹配service包下所有类的所有方法
6 @Pointcut("execution(* com.example.service.*.*(..))")
7 public void serviceMethods() {}
8
9 // 2. 方法参数切点 - 匹配有两个参数(String和int)的方法
10 @Pointcut("execution(* *.*(String, int))")
11 public void methodsWithStringAndInt() {}
12
13 // 3. 返回类型切点 - 匹配返回类型为void的方法
14 @Pointcut("execution(void *.*(..))")
15 public void voidMethods() {}
16
17 // 4. 包切点 - 匹配service包及子包中的所有方法
18 @Pointcut("execution(* com.example.service..*.*(..))")
19 public void servicePackageAndSubpackages() {}
20}

执行表达式格式

execution(modifiers-pattern? return-type-pattern declaring-type-pattern? method-name-pattern(parameter-pattern) throws-pattern?)
  • modifiers-pattern:访问修饰符模式,如public、protected(可选)
  • return-type-pattern:返回类型模式,如void、String、*(必选)
  • declaring-type-pattern:声明类型模式,如com.example.service.*(可选)
  • method-name-pattern:方法名模式,如get*、save、*(必选)
  • parameter-pattern:参数模式,如(..)表示任意参数、(String)表示一个String参数(必选)
  • throws-pattern:异常模式(可选)

3.2 常用切点表达式

表达式说明示例
execution(* *.*(..))所有方法执行匹配所有方法
execution(* com.example.service.*.*(..))指定包下的方法匹配service包下所有方法
execution(* *.*(String, int))指定参数的方法匹配有两个参数的方法
@annotation(Transactional)注解方法匹配有@Transactional注解的方法
within(com.example.service.*)指定包内的连接点匹配service包内的所有连接点
this(com.example.service.UserService)代理对象类型匹配代理对象为UserService的连接点
target(com.example.service.UserService)目标对象类型匹配目标对象为UserService的连接点
args(String, ..)参数类型匹配匹配第一个参数为String的方法

4. 通知类型

前置通知示例
java
1@Aspect
2@Component
3public class BeforeAdviceExample {
4
5 @Before("execution(* com.example.service.*.*(..))")
6 public void beforeAdvice(JoinPoint joinPoint) {
7 String methodName = joinPoint.getSignature().getName();
8 Object[] args = joinPoint.getArgs();
9
10 System.out.println("方法执行前: " + methodName);
11 System.out.println("参数: " + Arrays.toString(args));
12 }
13
14 @Before("@annotation(org.springframework.transaction.annotation.Transactional)")
15 public void beforeTransactional(JoinPoint joinPoint) {
16 System.out.println("事务方法执行前: " + joinPoint.getSignature().getName());
17 }
18}

前置通知特点

  • 在目标方法执行前执行
  • 无法阻止方法执行(除非抛出异常)
  • 可以访问方法参数,但不能修改返回值
  • 适用场景:参数校验、权限检查、日志记录

4.1 通知类型对比

通知类型注解执行时机能否控制方法执行能否访问返回值能否访问异常适用场景
前置通知@Before方法执行前参数校验、权限检查
后置通知@After方法执行后(finally)资源清理、日志记录
返回通知@AfterReturning方法正常返回后✅(只读)结果处理、日志记录
异常通知@AfterThrowing方法抛出异常时异常处理、错误通知
环绕通知@Around方法前后都可✅(可修改)✅(可处理)事务管理、性能监控

5. 实际应用场景

5.1 日志切面

日志切面
java
1@Aspect
2@Component
3@Slf4j
4public class LoggingAspect {
5
6 @Around("@annotation(com.example.annotation.LogExecutionTime)")
7 public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
8 String methodName = joinPoint.getSignature().getName();
9 String className = joinPoint.getTarget().getClass().getSimpleName();
10
11 log.info("开始执行方法: {}.{}", className, methodName);
12 long startTime = System.currentTimeMillis();
13
14 try {
15 Object result = joinPoint.proceed();
16 long endTime = System.currentTimeMillis();
17 log.info("方法 {}.{} 执行完成,耗时: {}ms", className, methodName, endTime - startTime);
18 return result;
19 } catch (Exception e) {
20 log.error("方法 {}.{} 执行异常: {}", className, methodName, e.getMessage());
21 throw e;
22 }
23 }
24
25 @Before("@annotation(com.example.annotation.LogParameters)")
26 public void logParameters(JoinPoint joinPoint) {
27 String methodName = joinPoint.getSignature().getName();
28 Object[] args = joinPoint.getArgs();
29
30 log.info("方法 {} 的参数: {}", methodName, Arrays.toString(args));
31 }
32}

5.2 缓存切面

缓存切面
java
1@Aspect
2@Component
3public class CacheAspect {
4
5 @Autowired
6 private RedisTemplate<String, Object> redisTemplate;
7
8 @Around("@annotation(com.example.annotation.Cacheable)")
9 public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
10 String key = generateCacheKey(joinPoint);
11
12 // 尝试从缓存获取
13 Object cachedValue = redisTemplate.opsForValue().get(key);
14 if (cachedValue != null) {
15 return cachedValue;
16 }
17
18 // 执行方法并缓存结果
19 Object result = joinPoint.proceed();
20 redisTemplate.opsForValue().set(key, result, Duration.ofMinutes(30));
21
22 return result;
23 }
24
25 @AfterReturning("@annotation(com.example.annotation.CacheEvict)")
26 public void evictCache(JoinPoint joinPoint) {
27 String key = generateCacheKey(joinPoint);
28 redisTemplate.delete(key);
29 }
30
31 private String generateCacheKey(JoinPoint joinPoint) {
32 String methodName = joinPoint.getSignature().getName();
33 Object[] args = joinPoint.getArgs();
34 return methodName + ":" + Arrays.toString(args);
35 }
36}

5.3 事务切面

事务切面
java
1@Aspect
2@Component
3public class TransactionAspect {
4
5 @Autowired
6 private PlatformTransactionManager transactionManager;
7
8 @Around("@annotation(com.example.annotation.Transactional)")
9 public Object handleTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
10 TransactionStatus status = null;
11
12 try {
13 // 开启事务
14 DefaultTransactionDefinition def = new DefaultTransactionDefinition();
15 status = transactionManager.getTransaction(def);
16
17 // 执行目标方法
18 Object result = joinPoint.proceed();
19
20 // 提交事务
21 transactionManager.commit(status);
22
23 return result;
24 } catch (Exception e) {
25 // 回滚事务
26 if (status != null) {
27 transactionManager.rollback(status);
28 }
29 throw e;
30 }
31 }
32}

事务切面功能

  • 自动事务管理:无需手动开启/提交/回滚事务
  • 声明式事务:使用注解即可启用事务
  • 事务传播:支持不同的事务传播行为
  • 事务隔离:支持不同的事务隔离级别
  • 异常处理:自动根据异常类型回滚事务

5.4 权限切面

权限切面
java
1@Aspect
2@Component
3public class SecurityAspect {
4
5 @Autowired
6 private SecurityService securityService;
7
8 @Before("@annotation(com.example.annotation.RequiresPermission)")
9 public void checkPermission(JoinPoint joinPoint) {
10 MethodSignature signature = (MethodSignature) joinPoint.getSignature();
11 RequiresPermission annotation = signature.getMethod().getAnnotation(RequiresPermission.class);
12 String permission = annotation.value();
13
14 if (!securityService.hasPermission(permission)) {
15 throw new AccessDeniedException("权限不足: " + permission);
16 }
17 }
18
19 @Before("@annotation(com.example.annotation.RequiresRole)")
20 public void checkRole(JoinPoint joinPoint) {
21 MethodSignature signature = (MethodSignature) joinPoint.getSignature();
22 RequiresRole annotation = signature.getMethod().getAnnotation(RequiresRole.class);
23 String role = annotation.value();
24
25 if (!securityService.hasRole(role)) {
26 throw new AccessDeniedException("角色不足: " + role);
27 }
28 }
29}

6. 性能监控切面

6.1 方法执行时间监控

性能监控切面
java
1@Aspect
2@Component
3public class PerformanceMonitorAspect {
4
5 private final MeterRegistry meterRegistry;
6
7 public PerformanceMonitorAspect(MeterRegistry meterRegistry) {
8 this.meterRegistry = meterRegistry;
9 }
10
11 @Around("execution(* com.example.service.*.*(..))")
12 public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
13 String methodName = joinPoint.getSignature().getName();
14 String className = joinPoint.getTarget().getClass().getSimpleName();
15 String metricName = className + "." + methodName;
16
17 Timer.Sample sample = Timer.start(meterRegistry);
18
19 try {
20 Object result = joinPoint.proceed();
21 sample.stop(Timer.builder("method.execution.time")
22 .tag("class", className)
23 .tag("method", methodName)
24 .tag("status", "success")
25 .register(meterRegistry));
26 return result;
27 } catch (Exception e) {
28 sample.stop(Timer.builder("method.execution.time")
29 .tag("class", className)
30 .tag("method", methodName)
31 .tag("status", "error")
32 .register(meterRegistry));
33
34 // 记录异常计数
35 meterRegistry.counter("method.execution.errors",
36 "class", className,
37 "method", methodName,
38 "exception", e.getClass().getSimpleName()).increment();
39
40 throw e;
41 }
42 }
43}

7. 面试题精选

7.1 基础概念题

Q: 什么是AOP?它的核心概念有哪些?

A: AOP(面向切面编程)是一种编程范式,用于将横切关注点从主业务逻辑中分离出来。核心概念包括:

  • Aspect(切面):横切关注点的模块化
  • Join Point(连接点):程序执行过程中的某个特定点
  • Pointcut(切点):匹配连接点的表达式
  • Advice(通知):在切点处要执行的代码
  • Target Object(目标对象):被代理的对象
  • Proxy(代理):AOP框架创建的对象
  • Weaving(织入):将切面应用到目标对象的过程

7.2 实践题

Q: 如何实现一个日志切面?

A: 实现日志切面的步骤:

  1. 创建切面类并使用@Aspect注解
  2. 定义切点表达式
  3. 使用通知注解(如@Around)实现日志逻辑
  4. 在切面中记录方法执行时间、参数、返回值等信息

核心代码示例:

java
1@Aspect
2@Component
3public class LoggingAspect {
4 @Around("execution(* com.example.service.*.*(..))")
5 public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
6 String methodName = joinPoint.getSignature().getName();
7 log.info("开始执行: {}", methodName);
8 long start = System.currentTimeMillis();
9 try {
10 Object result = joinPoint.proceed();
11 log.info("执行完成: {}, 耗时: {}ms", methodName, System.currentTimeMillis() - start);
12 return result;
13 } catch (Exception e) {
14 log.error("执行异常: {}, 异常: {}", methodName, e.getMessage());
15 throw e;
16 }
17 }
18}

7.3 高级题

Q: Spring AOP和AspectJ有什么区别?

A: 主要区别包括:

比较项Spring AOPAspectJ
实现方式基于动态代理基于字节码增强
织入时机运行时织入编译时、编译后、加载时织入
性能较慢更好
功能有限更强大
切点表达式支持部分AspectJ表达式支持完整的切点表达式语言
连接点类型仅方法执行方法调用、字段访问、构造器调用等
易用性简单易用更复杂但功能强大
应用场景简单应用复杂应用
AOP学习要点
  1. 理解核心概念:掌握Aspect、Pointcut、Advice等概念
  2. 熟悉切点表达式:学会编写各种切点表达式
  3. 掌握通知类型:了解五种通知的使用场景
  4. 学会实际应用:掌握日志、缓存、事务等切面实现
  5. 了解性能优化:学会性能监控和优化技巧

通过本章的学习,你应该已经掌握了Spring AOP的核心概念、实现原理和实际应用。AOP是Spring框架的重要特性,掌握AOP可以帮助你编写更加模块化、可维护的代码。在实际项目中,合理使用AOP可以大大减少重复代码,提高开发效率。

8. 实用示例

8.1 性能日志记录器

性能日志记录器
java
1@Aspect
2@Component
3@Slf4j
4public class LoggingAspect {
5 @Around("execution(* com.example.service.*.*(..))")
6 public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
7 String methodName = joinPoint.getSignature().getName();
8 log.info("开始执行: {}", methodName);
9 long start = System.currentTimeMillis();
10 try {
11 Object result = joinPoint.proceed();
12 log.info("执行完成: {}, 耗时: {}ms", methodName, System.currentTimeMillis() - start);
13 return result;
14 } catch (Exception e) {
15 log.error("执行异常: {}, 异常: {}", methodName, e.getMessage());
16 throw e;
17 }
18 }
19}

参与讨论