Spring AOP 切面使用指南:从入门到实战
📖 前言
Spring AOP(面向切面编程)是Spring框架的核心特性之一,它允许我们在不修改业务代码的情况下,为应用程序添加横切关注点(如日志、缓存、事务管理等)。本文将通过实际项目中的缓存实现案例,详细讲解Spring AOP的使用方式。
🎯 什么是AOP?
AOP(Aspect-Oriented Programming)面向切面编程,是对面向对象编程(OOP)的补充。它的核心思想是:
- 将横切关注点(Cross-cutting Concerns)从业务逻辑中分离出来
- 通过"切面"的方式,在运行时将这些关注点"织入"到目标对象中
核心概念
| 概念 | 说明 | 举例 |
|---|---|---|
| 切面 (Aspect) | 横切关注点的模块化 | 缓存切面、日志切面 |
| 连接点 (Join Point) | 程序执行的特定点 | 方法调用、异常抛出 |
| 切点 (Pointcut) | 连接点的集合 | 所有Service层的方法 |
| 通知 (Advice) | 切面在特定连接点执行的动作 | 方法执行前、后、异常时 |
| 织入 (Weaving) | 将切面应用到目标对象的过程 | 运行时动态代理 |
🛠️ 实战案例:自定义缓存切面
让我们通过一个实际的缓存实现来学习AOP的使用方式。
第一步:创建自定义注解
package com.peng.aspect;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD) // 注解作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时保留注解信息
public @interface MyCache {
int overTime() default 3600; // 缓存过期时间,默认1小时
}
注解说明:
@Target(ElementType.METHOD): 限定注解只能用在方法上@Retention(RetentionPolicy.RUNTIME): 确保运行时可以通过反射获取注解信息overTime(): 自定义属性,支持设置缓存过期时间
第二步:创建切面类
package com.peng.aspect;
import com.peng.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Slf4j
@Aspect // 声明这是一个切面类
@Component // 注册为Spring Bean
public class MyCacheAspect {
@Autowired
private RedisUtil redisUtil;
/**
* 生成缓存Key
* 规则:类名.方法名-参数1-参数2...
*/
private String createCacheKey(ProceedingJoinPoint jp) {
Signature signature = jp.getSignature();
String methodName = signature.getName();
String className = signature.getDeclaringTypeName();
StringBuffer sbKey = new StringBuffer();
sbKey.append(className).append(".").append(methodName);
Object[] args = jp.getArgs(); // 获取方法参数
for (Object arg : args) {
sbKey.append("-").append(arg);
}
return sbKey.toString();
}
/**
* 环绕通知:缓存切面逻辑
* 切点表达式:匹配service.Impl包下所有带@MyCache注解的公共方法
*/
@Order(1) // 切面执行顺序
@Around("execution(public * com.peng.service.Impl..*(..)) && @annotation(myCache)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
String key = createCacheKey(jp);
try {
// 1. 检查缓存
if (redisUtil.hasKey(key)) {
log.info("缓存命中: {}", key);
return redisUtil.get(key);
}
// 2. 缓存未命中,执行原方法
log.info("缓存未命中,执行原方法: {}", key);
Object result = jp.proceed(jp.getArgs());
// 3. 将结果存入缓存
redisUtil.set(key, result, myCache.overTime());
return result;
} catch (Throwable t) {
log.error("缓存切面执行异常: {}", t.getMessage());
return null;
}
}
}
第三步:在业务方法上使用注解
@Service
public class CacheServiceImpl implements ICacheService {
@Autowired
private IBlogService iBlogService;
@Override
@MyCache // 使用默认过期时间3600秒
public PageInfo<Blog> getIndexPage(String title, Integer pageNum) {
return iBlogService.getIndexPage(title, pageNum);
}
@Override
@MyCache(overTime = 7200) // 自定义过期时间7200秒
public List<Type> getIndexTypes() {
return iTypeService.getIndexTypes();
}
}
🔍 切点表达式详解
切点表达式是AOP的核心,它决定了切面在哪些地方生效。
常用切点表达式
// 1. 执行表达式 - 最常用
@Around("execution(public * com.peng.service..*(..))")
// 匹配:com.peng.service包及子包下所有公共方法
// 2. 注解表达式
@Around("@annotation(com.peng.aspect.MyCache)")
// 匹配:标注了@MyCache注解的方法
// 3. 组合表达式
@Around("execution(public * com.peng.service.Impl..*(..)) && @annotation(myCache)")
// 匹配:service.Impl包下所有公共方法 且 标注了@MyCache注解
// 4. 类型表达式
@Around("within(com.peng.service.impl.*)")
// 匹配:指定包下所有类的所有方法
// 5. 参数表达式
@Around("execution(* com.peng.service..*(String, ..)) && @annotation(myCache)")
// 匹配:第一个参数为String类型的方法
表达式语法规则
execution(修饰符 返回类型 包名.类名.方法名(参数类型))
| 通配符 | 含义 | 示例 |
|---|---|---|
* |
匹配任意字符 | *Service 匹配所有以Service结尾的类 |
.. |
匹配任意包层级或参数 | com.peng..* 匹配com.peng下所有子包 |
+ |
匹配子类型 | BaseService+ 匹配BaseService及其子类 |
📋 通知类型详解
Spring AOP提供5种通知类型:
1. 前置通知 (@Before)
@Before("execution(* com.peng.service..*(..))")
public void before(JoinPoint jp) {
log.info("方法执行前: {}", jp.getSignature().getName());
}
2. 后置通知 (@After)
@After("execution(* com.peng.service..*(..))")
public void after(JoinPoint jp) {
log.info("方法执行后: {}", jp.getSignature().getName());
}
3. 返回通知 (@AfterReturning)
@AfterReturning(pointcut = "execution(* com.peng.service..*(..))", returning = "result")
public void afterReturning(JoinPoint jp, Object result) {
log.info("方法正常返回: {}, 返回值: {}", jp.getSignature().getName(), result);
}
4. 异常通知 (@AfterThrowing)
@AfterThrowing(pointcut = "execution(* com.peng.service..*(..))", throwing = "ex")
public void afterThrowing(JoinPoint jp, Exception ex) {
log.error("方法执行异常: {}, 异常: {}", jp.getSignature().getName(), ex.getMessage());
}
5. 环绕通知 (@Around) - 最强大
@Around("execution(* com.peng.service..*(..))")
public Object around(ProceedingJoinPoint jp) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 前置逻辑
log.info("方法开始执行: {}", jp.getSignature().getName());
// 执行原方法
Object result = jp.proceed();
// 后置逻辑
log.info("方法执行成功,耗时: {}ms", System.currentTimeMillis() - startTime);
return result;
} catch (Exception e) {
// 异常逻辑
log.error("方法执行异常: {}", e.getMessage());
throw e;
}
}
🚀 高级特性
1. 切面执行顺序
@Aspect
@Order(1) // 数字越小,优先级越高
@Component
public class CacheAspect {
// 缓存切面逻辑
}
@Aspect
@Order(2)
@Component
public class LogAspect {
// 日志切面逻辑
}
2. 获取方法参数和注解信息
@Around("@annotation(myCache)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
// 获取方法信息
String methodName = jp.getSignature().getName();
String className = jp.getTarget().getClass().getName();
Object[] args = jp.getArgs();
// 获取注解属性
int overTime = myCache.overTime();
// 执行切面逻辑
return result;
}
3. 条件切面
@Around("@annotation(myCache) && @annotation(org.springframework.transaction.annotation.Transactional)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
// 只对同时标注了@MyCache和@Transactional的方法生效
}
💡 最佳实践
1. 切面设计原则
- 单一职责: 每个切面只处理一种横切关注点
- 低耦合: 切面之间应该相互独立
- 高内聚: 相关的切面逻辑应该放在同一个类中
2. 性能优化建议
@Around("@annotation(myCache)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
// ❌ 避免在切面中执行耗时操作
// Thread.sleep(1000);
// ✅ 使用异步处理耗时操作
CompletableFuture.runAsync(() -> {
// 异步日志记录
});
return jp.proceed();
}
3. 异常处理
@Around("@annotation(myCache)")
public Object around(ProceedingJoinPoint jp, MyCache myCache) {
try {
// 切面逻辑
} catch (Exception e) {
log.error("切面执行失败,降级执行原方法", e);
// 发生异常时,确保原方法能正常执行
return jp.proceed();
}
}
🎯 实际运行效果
当调用标注了@MyCache的方法时:
第一次调用:
2024-01-01 10:00:00 INFO - 缓存未命中,执行原方法: com.peng.service.Impl.CacheServiceImpl.getIndexTypes
2024-01-01 10:00:01 INFO - 缓存已存储,Key: com.peng.service.Impl.CacheServiceImpl.getIndexTypes
第二次调用:
2024-01-01 10:00:02 INFO - 缓存命中: com.peng.service.Impl.CacheServiceImpl.getIndexTypes
📚 总结
Spring AOP切面编程通过以下步骤实现:
- 定义切面: 使用
@Aspect和@Component注解 - 编写切点: 使用切点表达式指定拦截规则
- 实现通知: 选择合适的通知类型编写切面逻辑
- 应用切面: 在目标方法上使用注解或匹配切点表达式
核心优势:
- ✅ 无侵入性: 不需要修改业务代码
- ✅ 可重用性: 切面逻辑可以应用到多个方法
- ✅ 易维护性: 横切关注点集中管理
- ✅ 灵活配置: 支持复杂的切点表达式
通过合理使用Spring AOP,我们可以轻松实现缓存、日志、权限控制、事务管理等功能,让代码更加清晰和易于维护。
参考资源:
前端工程师、程序员

浙公网安备 33010602011771号