Spring AOP面向切面编程详解
AOP是Spring的另一核心特性,通过面向切面编程实现横切关注点的统一处理。本文详解AOP核心概念、实现原理、常用场景和最佳实践,帮你掌握Spring AOP的精髓。
前言
IOC 解决了对象依赖问题,AOP 解决的是横切关注点问题。
什么意思?比如日志、事务、权限验证,这些功能散布在各个业务方法里,代码重复且难维护。
AOP 就是把这些"横切"的功能抽出来,统一处理。
什么是 AOP
核心概念
AOP(Aspect Oriented Programming)面向切面编程,核心思想是:
把横切关注点从业务逻辑中分离出来,统一处理。
传统方式:
┌─────────────────┐
│ UserService │
├─────────────────┤
│ + 日志记录 │
│ + 权限检查 │
│ + 事务管理 │
│ + 业务逻辑 │ ← 业务代码被淹没
│ + 异常处理 │
└─────────────────┘
AOP方式:
横切关注点
┌─────────────────┐
│ 日志 │ ─┐
└─────────────────┘ │
┌─────────────────┐ │ 切入
│ 权限 │ ─┤
└─────────────────┘ │
┌─────────────────┐ │
│ UserService │ ←┘
│ (纯业务逻辑) │
└─────────────────┘
AOP 术语
理解 AOP,先搞清楚几个关键概念:
AOP术语对照:
┌─────────────┬─────────────┬─────────────────┐
│ 术语 │ 英文 │ 说明 │
├─────────────┼─────────────┼─────────────────┤
│ 切面 │ Aspect │ 功能模块 │
│ 切点 │ Pointcut │ 在哪里切入 │
│ 通知 │ Advice │ 切入后做什么 │
│ 连接点 │ Join Point │ 可以切入的位置 │
│ 织入 │ Weaving │ 把切面应用进去 │
│ 代理 │ Proxy │ 增强后的对象 │
└─────────────┴─────────────┴─────────────────┘
形象理解:
- 切面 = 一把刀(日志切面、事务切面)
- 切点 = 在哪切(哪些方法需要日志)
- 通知 = 切完干啥(记录日志、开启事务)
- 织入 = 动手术的过程
AOP 怎么工作
实际例子
// 原始业务类
@Service
public class UserService {
public User createUser(User user) {
// 纯业务逻辑
return userDao.save(user);
}
public void deleteUser(String id) {
// 纯业务逻辑
userDao.delete(id);
}
}
// 日志切面
@Aspect
@Component
public class LogAspect {
// 切点:UserService的所有公共方法
@Pointcut("execution(* com.example.UserService.*(..))")
public void userServiceMethods() {}
// 前置通知:方法执行前记录日志
@Before("userServiceMethods()")
public void logBefore(JoinPoint jp) {
System.out.println("调用方法: " + jp.getSignature().getName());
}
// 后置通知:方法执行后记录日志
@After("userServiceMethods()")
public void logAfter(JoinPoint jp) {
System.out.println("方法执行完成: " + jp.getSignature().getName());
}
}
运行结果:
调用方法: createUser
方法执行完成: createUser
通知类型
Spring AOP 支持 5 种通知类型:
通知类型详解:
1. @Before 前置通知
┌─────────────────┐
│ 方法执行前 │ ← 记录参数、权限检查
└─────────────────┘
│
▼
┌─────────────────┐
│ 目标方法 │
└─────────────────┘
2. @After 后置通知
┌─────────────────┐
│ 目标方法 │
└─────────────────┘
│
▼
┌─────────────────┐
│ 方法执行后 │ ← 清理资源、记录结果
└─────────────────┘
3. @AfterReturning 返回后通知
┌─────────────────┐
│ 目标方法 │
└─────────────────┘
│ 正常返回
▼
┌─────────────────┐
│ 处理返回值 │ ← 可以修改返回值
└─────────────────┘
4. @AfterThrowing 异常通知
┌─────────────────┐
│ 目标方法 │
└─────────────────┘
│ 抛出异常
▼
┌─────────────────┐
│ 异常处理 │ ← 记录异常、发送告警
└─────────────────┘
5. @Around 环绕通知
┌─────────────────┐
│ 前置处理 │
├─────────────────┤
│ 目标方法 │ ← 完全控制方法执行
├─────────────────┤
│ 后置处理 │
└─────────────────┘
切点表达式
切点表达式决定在哪些方法上应用通知:
// 常用切点表达式
// 1. 匹配方法执行
@Pointcut("execution(* com.example.service.*.*(..))")
// ↑ ↑ ↑ ↑
// 返回值 包名 类名 方法名(参数)
// 2. 匹配注解
@Pointcut("@annotation(com.example.Log)")
public void logMethods() {} // 有@Log注解的方法
// 3. 匹配类型
@Pointcut("within(com.example.service..*)")
public void serviceLayer() {} // service包下的所有类
// 4. 组合表达式
@Pointcut("serviceLayer() && logMethods()")
public void serviceWithLog() {} // service包且有@Log注解
表达式语法:
execution表达式详解:
execution(修饰符 返回值 包名.类名.方法名(参数))
通配符:
* : 匹配任意字符(一层)
.. : 匹配任意字符(多层)或任意参数
+ : 匹配指定类型及其子类
示例:
execution(* com.example..*.*(..))
↑ ↑ ↑
任意返回值 任意方法 任意参数
execution(public * com.example.service.*Service.get*(String))
↑ ↑ ↑ ↑
public 任意返回值 get开头 String参数
AOP 实现原理
动态代理机制
Spring AOP 底层使用动态代理:
Spring AOP代理选择:
目标对象实现接口?
│
┌────┴────┐
│ │
是 否
│ │
▼ ▼
JDK代理 CGLIB代理
(接口代理) (类代理)
JDK动态代理:
┌─────────────┐ 实现 ┌─────────────┐
│ UserService │ ────────▶ │ UserService │
│ (接口) │ │ Impl │
└─────────────┘ └─────────────┘
↑ ↑
│ │
代理对象 目标对象
CGLIB代理:
┌─────────────┐
│ UserService │ ← 目标类
└─────────────┘
↑
│ 继承
┌─────────────┐
│ UserService │ ← 代理子类
│ $Proxy │
└─────────────┘
代理对象创建过程
AOP代理创建流程:
1. Bean初始化
┌─────────────────────────────────┐
│ Spring创建UserService实例 │
└─────────────────────────────────┘
│
▼
2. AOP处理器介入
┌─────────────────────────────────┐
│ BeanPostProcessor检查是否需要代理 │
└─────────────────────────────────┘
│
▼
3. 查找匹配的切面
┌─────────────────────────────────┐
│ 扫描所有@Aspect,找到匹配的切点 │
└─────────────────────────────────┘
│
▼
4. 创建代理对象
┌─────────────────────────────────┐
│ 使用JDK或CGLIB创建代理对象 │
└─────────────────────────────────┘
│
▼
5. 返回代理对象
┌─────────────────────────────────┐
│ 容器中保存的是代理对象,不是原对象│
└─────────────────────────────────┘
常用场景
1. 日志记录
@Aspect
@Component
public class LoggingAspect {
@Around("@annotation(com.example.Loggable)")
public Object logExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行目标方法
long end = System.currentTimeMillis();
System.out.println(pjp.getSignature() + " 耗时: " + (end - start) + "ms");
return result;
}
}
// 使用
@Service
public class UserService {
@Loggable // 自动记录执行时间
public User createUser(User user) {
return userDao.save(user);
}
}
2. 权限控制
@Aspect
@Component
public class SecurityAspect {
@Before("@annotation(requiresRole)")
public void checkPermission(JoinPoint jp, RequiresRole requiresRole) {
String currentUserRole = getCurrentUserRole();
String requiredRole = requiresRole.value();
if (!currentUserRole.equals(requiredRole)) {
throw new SecurityException("权限不足,需要: " + requiredRole);
}
}
}
// 使用
@Service
public class AdminService {
@RequiresRole("ADMIN") // 需要ADMIN权限
public void deleteUser(String userId) {
userDao.delete(userId);
}
}
3. 事务管理
// Spring提供的事务切面
@Service
@Transactional // 类级别事务
public class UserService {
public User createUser(User user) {
// 自动开启事务
User saved = userDao.save(user);
// 发送邮件
emailService.sendWelcome(user.getEmail());
return saved;
// 自动提交事务(或回滚)
}
@Transactional(readOnly = true) // 只读事务
public User getUser(String id) {
return userDao.findById(id);
}
}
4. 缓存处理
@Aspect
@Component
public class CacheAspect {
private Map<String, Object> cache = new ConcurrentHashMap<>();
@Around("@annotation(cacheable)")
public Object handleCache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
String key = generateKey(pjp);
// 先查缓存
Object cached = cache.get(key);
if (cached != null) {
System.out.println("缓存命中: " + key);
return cached;
}
// 执行方法
Object result = pjp.proceed();
// 结果入缓存
cache.put(key, result);
System.out.println("缓存更新: " + key);
return result;
}
}
最佳实践
设计原则
// 1. 切面职责单一
@Aspect
@Component
public class LoggingAspect { // 只负责日志
// 日志相关逻辑
}
@Aspect
@Component
public class SecurityAspect { // 只负责权限
// 权限相关逻辑
}
// 2. 切点表达式精确
// ❌ 不好:范围太广
@Pointcut("execution(* com.example..*.*(..))")
// ✅ 好:范围精确
@Pointcut("execution(* com.example.service.*Service.*(..))")
// 3. 通知类型选择合适
// 简单前置检查 → @Before
// 需要控制执行 → @Around
// 异常处理 → @AfterThrowing
性能考虑
// 1. 避免过度使用AOP
// ❌ 不必要的切面
@Before("execution(* *.get*(..))")
public void logAllGetters() {
// getter方法通常不需要日志
}
// 2. 切点表达式优化
// ❌ 慢:每次都要匹配
@Pointcut("execution(* com.example..*.*(..)) && @annotation(Log)")
// ✅ 快:先匹配注解
@Pointcut("@annotation(Log)")
// 3. 合理使用@Around
@Around("serviceLayer()")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
// 前置处理要轻量
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 必须调用
// 后置处理也要轻量
long time = System.currentTimeMillis() - start;
if (time > 1000) { // 只记录慢查询
log.warn("慢方法: {} 耗时: {}ms", pjp.getSignature(), time);
}
return result;
}
小结
AOP 是 Spring 的另一个核心特性,专门解决横切关注点问题。
核心概念:
- 切面(Aspect):功能模块
- 切点(Pointcut):在哪里切入
- 通知(Advice):切入后做什么
- 织入(Weaving):把切面应用到目标对象
实现原理:
- JDK 动态代理(有接口)
- CGLIB 代理(无接口)
- 在 Bean 初始化时创建代理对象
常用场景:
- 日志记录、权限控制
- 事务管理、缓存处理
- 性能监控、异常处理
最佳实践:
- 切面职责单一
- 切点表达式精确
- 合理选择通知类型
- 注意性能影响
配合 IOC,AOP 让 Spring 成为了一个完整的企业级框架。IOC 管对象,AOP 管功能,两者结合威力无穷。
