Spring AOP面向切面编程
文章目录
- [Spring AOP面向切面编程](#Spring AOP面向切面编程)
-
- 前言
- 一、AOP基础概念
-
- [1.1 什么是AOP](#1.1 什么是AOP)
- [1.2 为什么需要AOP](#1.2 为什么需要AOP)
- [1.3 核心术语](#1.3 核心术语)
- 二、AOP通知类型
- 三、切点表达式详解
- 四、AOP实际应用场景
-
- [4.1 方法执行时间统计](#4.1 方法执行时间统计)
- [4.2 统一日志记录](#4.2 统一日志记录)
- [4.3 声明式事务管理](#4.3 声明式事务管理)
- [4.4 权限校验](#4.4 权限校验)
- 五、AOP的实现原理
- 六、AOP注意事项
- 总结
- [✅ 亮点总结](#✅ 亮点总结)
- 适用场景
- 扩展方向
前言
在开发中,我们经常会遇到一些横跨多个模块的通用功能,如日志记录、事务管理、权限校验等。如果把这些代码硬编码在每个方法中,会导致大量重复代码和耦合。AOP(Aspect-Oriented Programming)面向切面编程正是解决这一问题的利器。
AOP与OOP的关系:OOP(面向对象编程)通过类和继承来组织代码,能够很好地处理"纵向"的业务逻辑。但当关注点跨越多个类和模块时(如所有Service方法都要记录日志),OOP就显得力不从心了------你不得不在每个类中重复写相似的代码。AOP通过"横切"的方式处理这些通用关注点,将其集中管理并动态织入目标对象。Spring的声明式事务(@Transactional)就是AOP最经典的应用------你只需要加一个注解,Spring就在背后帮你完成了开启事务、提交、回滚等一系列操作。面试中AOP的原理和实现方式是必考题。
本文将带你理解AOP的核心概念,并通过实例掌握Spring AOP的使用。
一、AOP基础概念
1.1 什么是AOP
AOP是一种编程范式,通过预编译方式 和运行期动态代理,在不修改源代码的情况下给程序动态统一添加功能。它是对OOP(面向对象编程)的补充和完善。
1.2 为什么需要AOP
java
// 没有AOP时的代码:每个方法都要写日志和权限校验
public void addUser(User user) {
System.out.println("日志: addUser被调用"); // 横切关注点
checkPermission("admin"); // 横切关注点
userDao.insert(user); // 核心业务
}
public void deleteUser(int id) {
System.out.println("日志: deleteUser被调用");
checkPermission("admin");
userDao.delete(id);
}
可以看到,日志和权限代码与业务逻辑混杂在一起。AOP可以将这些横切关注点抽取出来独立管理,让业务代码保持纯粹。
横切关注点的特征:1)分散在多个模块中(不是只存在于某个类);2)与核心业务逻辑无关(但仍不可或缺);3)具有高度重复性(每个方法都差不多)。识别这些特征是判断是否适合使用AOP的标准。除了文中提到的日志和权限,常见的横切关注点还包括:缓存处理、性能统计、异常统一处理、请求参数校验等。
1.3 核心术语
| 术语 | 说明 |
|---|---|
| 切面(Aspect) | 横切关注点的模块化,包含通知和切点 |
| 通知(Advice) | 切面在特定切点执行的动作 |
| 切点(Pointcut) | 匹配连接点的表达式,定义通知在何处执行 |
| 连接点(Join Point) | 程序执行过程中的某个点,如方法调用 |
| 引入(Introduction) | 为现有类动态添加新方法或属性 |
| 织入(Weaving) | 将切面应用到目标对象并创建代理对象的过程 |
二、AOP通知类型
Spring AOP定义了五种通知类型。理解这五种通知的执行时机是掌握AOP的关键。以目标方法执行为中心:
- @Before:目标方法执行之前
- @AfterReturning:目标方法正常返回后(不捕获异常)
- @AfterThrowing:目标方法抛出异常后
- @After:目标方法执行后(不管是否异常,类似于finally)
- @Around:环绕目标方法,可以完全控制执行(最强大)
@Around的注意事项 :@Around是最灵活的通知类型,但也是最容易出错的。必须调用joinPoint.proceed()来执行目标方法,否则目标方法不会被执行。另外,如果目标方法有返回值,@Around必须返回该值(或处理后的值),否则调用方会收到null。忘记调用proceed()或忘记返回结果,是初学AOP时最常见的两个错误。
java
@Aspect
@Component
public class LogAspect {
// 前置通知:目标方法执行前执行
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("前置通知: " + methodName + " 参数: " + Arrays.toString(args));
}
// 后置通知:目标方法执行后执行(不论是否异常)
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
System.out.println("后置通知: " + joinPoint.getSignature().getName() + " 执行完毕");
}
// 返回通知:目标方法正常返回后执行
@AfterReturning(value = "execution(* com.example.service.*.*(..))", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
System.out.println("返回通知: " + joinPoint.getSignature().getName() + " 返回: " + result);
}
// 异常通知:目标方法抛出异常后执行
@AfterThrowing(value = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void afterThrowing(JoinPoint joinPoint, Exception ex) {
System.out.println("异常通知: " + joinPoint.getSignature().getName() + " 异常: " + ex.getMessage());
}
// 环绕通知:包裹目标方法,可控制执行时机和参数
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
System.out.println("环绕通知-开始: " + joinPoint.getSignature().getName());
Object result = joinPoint.proceed(); // 执行目标方法
long endTime = System.currentTimeMillis();
System.out.println("环绕通知-结束: " + joinPoint.getSignature().getName()
+ " 耗时: " + (endTime - startTime) + "ms");
return result;
}
}
多个通知的执行顺序:当一个切面中定义了多个通知时,执行顺序是:@Around(前) → @Before → 目标方法 → @Around(后) → @After → @AfterReturning(正常)或 @AfterThrowing(异常)。如果多个切面都匹配同一个目标方法,可以用@Order注解控制切面的执行顺序------数字越小,切面的外层越靠外(最先进入,最后退出)。
三、切点表达式详解
切点表达式使用 execution 指示器来匹配方法执行:
execution(修饰符? 返回类型 包名.类名.方法名(参数列表) 异常类型?)
常用通配符:
*匹配任意单个部分..匹配任意多层路径或任意参数+匹配类及其所有子类
java
// 匹配所有public方法
@Pointcut("execution(public * *(..))")
// 匹配service包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
// 匹配service包及其子包下所有类的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")
// 匹配save开头的方法
@Pointcut("execution(* com.example..*.save*(..))")
// 匹配参数为Long类型的方法
@Pointcut("execution(* com.example..*.*(Long, ..))")
// 组合切点:多个条件取交集
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.UserService.get*(..))")
@annotation 指示器:匹配带有特定注解的方法
java
// 定义自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {}
// 使用注解作为切点
@Pointcut("@annotation(com.example.annotation.LogExecution)")
public void logPointcut() {}
四、AOP实际应用场景
4.1 方法执行时间统计
java
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service..*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.nanoTime();
Object result = joinPoint.proceed();
long elapsed = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
String methodName = joinPoint.getSignature().toShortString();
if (elapsed > 100) { // 超过100ms记录
System.out.println("性能警告: " + methodName + " 耗时 " + elapsed + "ms");
}
return result;
}
}
4.2 统一日志记录
java
@Aspect
@Component
public class WebLogAspect {
private static final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
@Around("execution(* com.example.controller..*.*(..))")
public Object logRequest(ProceedingJoinPoint joinPoint) throws Throwable {
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
logger.info("URL: {} {} - 参数: {}",
request.getMethod(), request.getRequestURL(),
Arrays.toString(joinPoint.getArgs()));
Object result = joinPoint.proceed();
logger.info("响应: {}", result);
return result;
}
}
Controller层日志的注意事项:打印请求和响应是很常见的做法,但有几个坑需要注意:
- 敏感信息脱敏:密码、手机号、身份证号等敏感信息不应该明文打印在日志中,需要做脱敏处理
- 大响应体截断:如果响应是一个几十MB的文件或大JSON,直接打印会撑爆日志文件,应该做截断处理
- 耗时统计:在打印响应日志时,最好同时记录方法执行耗时,方便后期性能分析
- 异常情况:目标方法抛出异常时,@After最终会执行但@AfterReturning不会执行,所以要确保异常时也能记录到关键信息
4.3 声明式事务管理
Spring的事务管理正是基于AOP实现的:
java
@Service
public class OrderService {
@Transactional(rollbackFor = Exception.class)
public void createOrder(Order order) {
orderDao.insert(order);
inventoryService.deduct(order.getProductId(), order.getQuantity());
// 任何异常都会自动回滚
}
}
@Transactional 注解的背后,Spring通过AOP在方法执行前开启事务,正常返回时提交,抛出异常时回滚。开发者完全不需要手动管理事务生命周期。
@Transactional常见失效场景:以下情况事务不会生效,面试和实际开发中都要注意:
- 方法不是public的:Spring AOP只能拦截public方法,protected/private/默认的方法上@Transactional不生效
- 同类内方法调用:如方法A调用本类的@Transactional方法B,调用的是this而非代理对象,AOP不生效
- 异常被catch了:如果方法内部使用try-catch捕获了异常且没有再次抛出,Spring感知不到异常,事务会提交
- rollbackFor未正确配置 :@Transactional默认只回滚RuntimeException和Error,对于受检异常(如IOException)需要显式配置
rollbackFor = Exception.class - 数据库引擎不支持事务:如果使用MyISAM引擎(而非InnoDB),事务不会生效
4.4 权限校验
java
@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(requiresPermission)")
public Object checkPermission(ProceedingJoinPoint joinPoint,
RequiresPermission requiresPermission) throws Throwable {
String permission = requiresPermission.value();
User currentUser = getCurrentUser();
if (!currentUser.hasPermission(permission)) {
throw new AccessDeniedException("缺少权限: " + permission);
}
return joinPoint.proceed();
}
}
// 使用
@RequiresPermission("user:delete")
public void deleteUser(Long id) {
userDao.deleteById(id);
}
五、AOP的实现原理
Spring AOP基于动态代理实现,根据目标类是否实现接口,选择不同的代理方式:
- JDK动态代理 :目标类实现了接口时使用,通过
java.lang.reflect.Proxy创建代理对象 - CGLIB代理:目标类没有实现接口时使用,通过继承目标类创建子类代理对象
JDK vs CGLIB代理的深入对比:
- JDK代理:基于接口,代理对象和目标对象是"兄弟"关系(都实现了相同接口),性能稍好,但只能代理接口中定义的方法
- CGLIB代理:基于继承,代理对象是目标对象的"子类",可以代理所有非final方法,但性能稍差(生成字节码开销大),且final方法无法代理
- Spring Boot 2.x之后默认使用CGLIB代理(即使实现了接口),可以通过
spring.aop.proxy-target-class=false配置改回JDK代理
java
// Spring AOP自动选择代理方式
// 可以通过配置强制使用CGLIB
@EnableAspectJAutoProxy(proxyTargetClass = true)
@Configuration
public class AopConfig {}
六、AOP注意事项
- 自调用问题:类内部方法互相调用时,AOP不会生效。这是因为调用的是this对象而不是代理对象。解决方法是注入自身或使用AopContext。
自调用问题的根本原因 :Spring AOP是通过代理对象来实现的。当你通过Spring注入的userService调用方法时,实际上调用的是代理对象的方法,代理会在方法前后执行AOP逻辑。但当userService内部通过this调用自己的另一个方法时,this是原始对象而非代理对象,AOP自然无法生效。解决方案:(1)将需要AOP的方法拆分到另一个Bean中;(2)通过AopContext.currentProxy()获取代理对象再调用;(3)注入自身:在类中使用@Autowired private UserService self;然后用self调用。
- final方法不能被代理:CGLIB通过继承实现代理,final方法无法被覆写。
- 切面执行顺序 :多个切面可以指定执行顺序,使用
@Order注解,值越小优先级越高。
java
@Aspect
@Component
@Order(1) // 数字越小越先执行
public class FirstAspect {}
总结
本文系统介绍了Spring AOP的核心概念、五种通知类型、切点表达式以及实际应用场景。AOP的核心价值在于将横切关注点与业务逻辑分离,让代码更加清晰、可维护。在实际项目中,AOP广泛应用于日志记录、事务管理、权限校验、缓存处理等场景。建议在项目中合理使用AOP,但不要过度使用导致代码流程难以追踪。
✅ 亮点总结
- 五种通知类型(@Before/@After/@Around等)覆盖方法执行全生命周期
- 切点表达式:execution匹配方法签名,@annotation匹配自定义注解,组合表达式精准定位
- 四大实战场景:性能监控(记录耗时)、日志记录(请求参数打印)、事务管理(@Transactional原理)、权限校验(自定义注解+环绕通知)
- 动态代理原理:实现接口用JDK代理,无接口用CGLIB代理,Spring自动选择
- 切面执行顺序:@Order注解控制多个切面的优先级
适用场景
- Controller层统一日志记录:通过AOP自动打印请求URL、参数和响应结果
- Service层声明式事务管理:@Transactional注解背后的AOP自动开启/提交/回滚事务
- 自定义权限校验:定义@RequiresPermission注解,AOP环绕通知拦截并验证用户权限
扩展方向
- 学习AspectJ的编译期织入(compile-time weaving)和更强大的切点表达式语法
- 深入Spring事务传播机制(REQUIRED/REQUIRES_NEW/NESTED)和隔离级别
- 推荐阅读下一篇文章:Spring Boot快速入门(./46_Spring Boot快速入门),体验"约定优于配置"的开发体验