面向切面编程 AOP
从 AOP 概念出发,系统讲解切面、连接点、通知、切点、织入等核心概念,以及 Spring AOP 的实现与应用。
🎯学习目标
- 理解 AOP 的基本概念,掌握 AOP 与 OOP 的关系和互补性;
- 掌握 AOP 的五大核心概念:切面、连接点、通知、切点、织入;
- 了解 AOP 的三种实现方式:编译时织入、类加载时织入、运行时织入;
- 掌握 Spring AOP 的注解配置和 XML 配置方式;
- 能够运用 AOP 解决日志记录、事务管理、权限控制等实际问题。
1AOP 概述
💡 通俗理解:什么是 AOP?
想象你在经营一家餐厅:
- OOP 思维:每个厨师负责做菜(核心业务),但每个厨师还要自己洗菜、切菜、打扫卫生(横切关注点)
- AOP 思维:专门雇一个"后勤团队",统一负责洗菜、切菜、打扫,厨师只管做菜
AOP(面向切面编程)就是把那些"散落在各处的公共功能"(如日志、事务、安全)抽取出来,统一管理,让核心业务代码更纯粹。
什么是 AOP?
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,旨在将横切关注点(Cross-Cutting Concerns)从业务逻辑中分离出来,实现关注点的模块化。
图1 · AOP 将分散的横切关注点抽取为独立的切面模块
为什么需要 AOP?
| 问题 | 说明 | AOP 解决方案 |
|---|---|---|
| 代码重复 | 日志、事务等代码在每个方法中重复出现 | 抽取为切面,一处定义,多处使用 |
| 职责不清 | 业务代码与非业务代码混杂,难以维护 | 业务逻辑与横切关注点分离 |
| 耦合度高 | 修改日志格式需要改动所有业务类 | 修改切面即可,不影响业务代码 |
| 扩展困难 | 新增功能(如性能监控)需要侵入式修改 | 通过新增切面实现,对原有代码无侵入 |
AOP 与 OOP 的关系
🧱
OOP:纵向抽象
通过类、继承、多态实现"自上而下"的功能分解。擅长处理业务逻辑的层次结构,但对"横跨多个模块"的公共功能力不从心。
✂️
AOP:横向抽象
通过切面将横切关注点模块化,实现"横向上"的功能整合。是 OOP 的有力补充,二者结合才能实现更彻底的模块化。
⭐ 核心关系AOP 不是 OOP 的替代品,而是 OOP 的补充。OOP 解决业务逻辑的模块化,AOP 解决横切关注点的模块化。二者结合 = 完整的模块化方案。
2AOP 核心概念
图2 · AOP 五大核心概念及其关系
📐
切面 Aspect
关注点的模块化切面是横切关注点的模块化,是"切入点 + 通知"的组合。一个切面可以包含多个通知和切入点。在 Spring 中,用 @Aspect 注解的类就是一个切面。
📍
连接点 Join Point
程序执行的某个点程序执行过程中能够插入切面的点,如方法调用、异常处理、字段修改等。Spring AOP 中连接点仅支持方法执行(方法级别的连接点)。
🎯
切点 Pointcut
匹配连接点的规则匹配连接点的断言/表达式,用于定义"在哪些地方"应用通知。通过切入点表达式(如 execution(* com.example.*.*(..)))来匹配连接点。
📢
通知 Advice
切面的具体动作在切点匹配的连接点上执行的动作(代码)。分为:前置通知(Before)、后置通知(After)、返回通知(AfterReturning)、异常通知(AfterThrowing)、环绕通知(Around)。
💡 织入 Weaving
织入是将切面应用到目标对象并创建新的代理对象的过程。根据织入时机不同,分为:
- 编译时织入:在编译 Java 源码时织入(如 AspectJ 编译器)
- 类加载时织入:在 JVM 加载类时织入(如 AspectJ LTW)
- 运行时织入:在运行期通过动态代理织入(如 Spring AOP)
五种通知类型详解
| 通知类型 | 执行时机 | 注解 | 典型用途 |
|---|---|---|---|
| 前置通知 | 方法执行前 | @Before | 参数校验、权限检查、日志记录 |
| 后置通知 | 方法执行后(无论是否异常) | @After | 资源清理、审计记录 |
| 返回通知 | 方法正常返回后 | @AfterReturning | 处理返回值、成功日志 |
| 异常通知 | 方法抛出异常后 | @AfterThrowing | 异常处理、错误日志、告警 |
| 环绕通知 | 方法执行前后(包裹) | @Around | 性能监控、事务管理、缓存 |
3AOP 实现方式
根据织入时机的不同,AOP 有三种主要实现方式:
图3 · AOP 三种织入方式对比
| 织入方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 编译时织入 | 性能最好,无运行时开销 | 需要特殊编译器,构建复杂 | 对性能要求极高的场景 |
| 类加载时织入 | 对代码无侵入,部署灵活 | 需要配置 Agent,调试困难 | 无法修改源码的第三方库 |
| 运行时织入 | 使用简单,Spring 原生支持 | 有一定性能开销(代理) | 大多数企业应用场景 |
⭐ Spring AOP vs AspectJ
- Spring AOP:基于代理,运行时织入,仅支持方法级连接点,使用简单,适合大多数场景
- AspectJ:基于字节码修改,编译时/类加载时织入,支持所有连接点(方法、字段、构造器),功能强大但复杂
选择建议:Spring 项目优先用 Spring AOP,需要更细粒度控制时再考虑 AspectJ。
4Spring AOP ⭐
Spring AOP 是 Spring 框架的核心模块之一,通过动态代理技术实现 AOP 功能。
Spring AOP 的代理机制
🔗
JDK 动态代理
基于接口的代理。目标类实现了接口时,Spring 默认使用 JDK 动态代理。通过 java.lang.reflect.Proxy 创建代理对象。
🧬
CGLIB 代理
基于继承的代理。目标类没有实现接口时,Spring 使用 CGLIB 生成目标类的子类作为代理。通过字节码技术创建代理类。
注解配置方式(推荐)⭐
// 1. 定义切面
@Aspect
@Component
public class LoggingAspect {
// 定义切点:匹配 service 包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
// 前置通知:方法执行前
@Before("serviceLayer()")
public void beforeAdvice(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("调用方法: " + methodName + ",参数: " + Arrays.toString(args));
}
// 后置通知:方法执行后(无论是否异常)
@After("serviceLayer()")
public void afterAdvice(JoinPoint joinPoint) {
System.out.println("方法执行完毕: " + joinPoint.getSignature().getName());
}
// 返回通知:方法正常返回后
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
System.out.println("方法返回值: " + result);
}
// 异常通知:方法抛出异常后
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
System.out.println("方法异常: " + ex.getMessage());
}
// 环绕通知:包裹方法执行
@Around("serviceLayer()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 执行目标方法
long time = System.currentTimeMillis() - start;
System.out.println("方法耗时: " + time + "ms");
return result;
}
}
切入点表达式
| 表达式 | 说明 | 示例 |
|---|---|---|
execution() | 匹配方法执行 | execution(* com.example.service.*.*(..)) |
within() | 匹配指定类型内的方法 | within(com.example.service.*) |
@annotation() | 匹配带指定注解的方法 | @annotation(com.example.Loggable) |
args() | 匹配指定参数类型的方法 | args(java.lang.String,..) |
bean() | 匹配指定名称的 Bean | bean(userService) |
💡 execution 表达式格式
execution(修饰符? 返回类型 类名?方法名(参数) 异常?)
*:匹配任意返回类型com.example.service.*:匹配 service 包下所有类.*(..):匹配任意方法名、任意参数com.example..*:匹配 com.example 及其子包
XML 配置方式
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- 定义切面 Bean -->
<bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>
<!-- AOP 配置 -->
<aop:config>
<!-- 定义切点 -->
<aop:pointcut id="servicePointcut"
expression="execution(* com.example.service.*.*(..))"/>
<!-- 定义切面 -->
<aop:aspect ref="loggingAspect">
<aop:before method="beforeAdvice" pointcut-ref="servicePointcut"/>
<aop:after method="afterAdvice" pointcut-ref="servicePointcut"/>
<aop:around method="aroundAdvice" pointcut-ref="servicePointcut"/>
</aop:aspect>
</aop:config>
</beans>
启用 AOP 支持
// 方式1:注解启用
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}
// 方式2:XML 启用
<aop:aspectj-autoproxy/>
5AOP 应用场景
📝
日志记录
最经典的应用场景统一记录方法调用日志,包括方法名、参数、返回值、执行时间。无需在每个业务方法中手动添加日志代码。
🔄
事务管理
Spring 声明式事务通过 @Transactional 注解实现声明式事务。Spring AOP 在方法执行前开启事务,正常返回时提交,异常时回滚。
🔒
权限控制
方法级安全检查在方法执行前检查用户权限,无权限则抛出异常。常用于 RBAC(基于角色的访问控制)场景。
⏱️
性能监控
方法执行时间统计记录每个方法的执行时间,识别性能瓶颈。通过环绕通知实现,在方法前后记录时间戳并计算差值。
完整示例:日志切面
@Aspect
@Component
@Slf4j
public class LogAspect {
@Pointcut("execution(* com.example.controller.*.*(..))")
public void controllerLayer() {}
@Around("controllerLayer()")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
String className = pjp.getTarget().getClass().getSimpleName();
String methodName = pjp.getSignature().getName();
Object[] args = pjp.getArgs();
log.info("[{}] 方法开始执行,参数: {}", className + "." + methodName, args);
long start = System.currentTimeMillis();
try {
Object result = pjp.proceed();
long time = System.currentTimeMillis() - start;
log.info("[{}] 方法执行成功,耗时: {}ms,返回: {}",
className + "." + methodName, time, result);
return result;
} catch (Exception e) {
long time = System.currentTimeMillis() - start;
log.error("[{}] 方法执行异常,耗时: {}ms,异常: {}",
className + "." + methodName, time, e.getMessage());
throw e;
}
}
}
完整示例:权限校验切面
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
String value();
}
// 权限切面
@Aspect
@Component
public class AuthAspect {
@Around("@annotation(requireRole)")
public Object checkAuth(ProceedingJoinPoint pjp, RequireRole requireRole) throws Throwable {
// 获取当前用户
User currentUser = SecurityContext.getCurrentUser();
if (currentUser == null) {
throw new UnauthorizedException("用户未登录");
}
// 检查角色
String requiredRole = requireRole.value();
if (!currentUser.hasRole(requiredRole)) {
throw new ForbiddenException("权限不足,需要角色: " + requiredRole);
}
return pjp.proceed();
}
}
// 使用方式
@Service
public class UserService {
@RequireRole("ADMIN")
public void deleteUser(Long userId) {
// 只有管理员才能执行
}
}
6AOP 最佳实践
⚠️ 常见误区
- 过度使用 AOP:不是所有横切功能都需要 AOP,简单的工具方法即可解决的场景不需要引入 AOP
- 切面逻辑过重:切面应该保持轻量,复杂逻辑应该委托给专门的服务类
- 切入点表达式过于宽泛:避免使用
execution(* *.*(..))这种匹配所有方法的表达式 - 忽略代理限制:Spring AOP 基于代理,同类方法调用不会触发 AOP(自调用问题)
最佳实践清单
| 实践 | 说明 |
|---|---|
| 保持切面单一职责 | 一个切面只关注一个横切关注点(如日志切面只做日志) |
| 使用命名切点 | 将切入点表达式定义为 @Pointcut 方法,便于复用和维护 |
| 避免循环依赖 | 切面之间不要相互依赖,会导致 Spring 容器启动失败 |
| 注意执行顺序 | 多个切面作用于同一方法时,使用 @Order 指定优先级 |
| 处理自调用问题 | 同类方法调用不走代理,可通过注入自身或使用 AopContext 解决 |
| 测试切面逻辑 | 切面也需要单元测试,确保通知逻辑正确执行 |
切面执行顺序
@Aspect
@Component
@Order(1) // 数字越小,优先级越高
public class SecurityAspect {
// 安全校验(先执行)
}
@Aspect
@Component
@Order(2)
public class LoggingAspect {
// 日志记录(后执行)
}
💡 自调用问题解决方案
// 方案1:注入自身
@Service
public class UserService {
@Autowired
private UserService self; // 注入代理对象
public void methodA() {
self.methodB(); // 通过代理调用,触发 AOP
}
@Transactional
public void methodB() { }
}
// 方案2:启用 exposeProxy
@EnableAspectJAutoProxy(exposeProxy = true)
public class AppConfig { }
// 使用方式
public void methodA() {
((UserService) AopContext.currentProxy()).methodB();
}
⭐重点例题
例题1:理解 AOP 核心概念
题目:在 Spring AOP 中,以下代码定义了什么?
@Pointcut("execution(* com.example.service.UserServiceImpl.*(..))")
public void userServiceMethods() {}
分析:
- 这是一个切入点定义(Pointcut),不是通知
- 表达式
execution(* com.example.service.UserServiceImpl.*(..))匹配 UserServiceImpl 类的所有方法 *:任意返回类型.*(..):任意方法名、任意参数userServiceMethods()是切入点的方法名,可在通知中引用
例题2:选择合适的通知类型
题目:需要记录每个 Service 方法的执行时间,应该使用哪种通知类型?
分析:
- 前置通知 @Before:只能记录开始时间,无法计算耗时
- 后置通知 @After:只能记录结束时间,无法计算耗时
- 返回通知 @AfterReturning:只能在方法正常返回后执行,异常时无法记录
- 环绕通知 @Around:✅ 可以在方法前后分别记录时间,计算差值,且能处理异常情况
例题3:AOP 与 OOP 结合设计
题目:设计一个电商系统的订单服务,需要:创建订单、查询订单、取消订单。要求添加日志记录和事务管理。
分析:
- OOP 部分:OrderService 类实现业务逻辑(创建、查询、取消)
- AOP 部分:
- 日志切面:记录所有方法的调用日志
- 事务切面:在创建和取消方法上开启事务(@Transactional)
🎯自测(点击展开)
什么是 AOP?它与 OOP 是什么关系?
AOP(面向切面编程)是将横切关注点从业务逻辑中分离出来的编程范式。AOP 是 OOP 的补充,OOP 解决业务逻辑的模块化,AOP 解决横切关注点的模块化,二者结合实现更彻底的模块化。
AOP 的五大核心概念是什么?
切面(Aspect)= 切入点 + 通知,是横切关注点的模块化;连接点(Join Point)程序执行中可插入切面的点;切点(Pointcut)匹配连接点的表达式;通知(Advice)在切点上执行的动作;织入(Weaving)将切面应用到目标对象的过程。
Spring AOP 有哪几种通知类型?各有什么用途?
@Before(前置,参数校验)、@After(后置,资源清理)、@AfterReturning(返回,处理返回值)、@AfterThrowing(异常,错误处理)、@Around(环绕,性能监控、事务管理)。
Spring AOP 的代理机制是什么?
Spring AOP 基于动态代理:目标类实现了接口时使用 JDK 动态代理(基于接口);目标类没有实现接口时使用 CGLIB 代理(基于继承,生成子类)。
什么是自调用问题?如何解决?
自调用问题:同类中方法 A 调用方法 B,不会触发 AOP,因为调用绕过了代理对象。解决方案:1. 注入自身代理对象;2. 启用 exposeProxy 并使用 AopContext.currentProxy()。
AOP 有哪些典型应用场景?
日志记录(统一方法调用日志)、事务管理(声明式事务 @Transactional)、权限控制(方法级安全检查)、性能监控(方法执行时间统计)、缓存管理、异常处理等。
📝强化题库
选择题点选即时判分;填空题输入后"检查"或"显示答案"。
已答 0/0答对 0正确率 —
已答 0/0答对 0