AOP 面向切面编程
一、为什么需要 AOP
传统 OOP 的问题
java
@Service
public class UserService {
public void save(User user) {
// ========== 记录日志 ==========
log.info("save user: {}", user);
// ========== 权限校验 ==========
if (!hasPermission()) {
throw new SecurityException("No permission!");
}
// ========== 事务开启 ==========
TransactionManager.begin();
try {
// ========== 核心业务逻辑 ==========
userDao.insert(user);
// ========== 事务提交 ==========
TransactionManager.commit();
} catch (Exception e) {
// ========== 事务回滚 ==========
TransactionManager.rollback();
throw e;
}
}
}问题:
- 代码重复(每个方法都要加日志、事务)
- 业务逻辑与横切关注点耦合
- 修改日志逻辑需要改所有类
AOP 解决方案
java
@Service
@Loggable // 自定义注解标记
@Transactional // 声明式事务
public class UserService {
@PreAuthorize("hasRole('USER')")
public void save(User user) {
// 只有核心业务逻辑
userDao.insert(user);
}
}┌──────────────────────────────────────────────────────────────┐
│ 业务方法 save() │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 日志通知 │ │ 权限通知 │ │ 事务通知 │ │ 性能监控 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
└────────┼─────────────┼─────────────┼─────────────┼──────────┘
│ │ │ │
▼ ▼ ▼ ▼
Before Before Before Before
(JoinPoint) (JoinPoint) (JoinPoint)
│ │ │
└─────────────┴─────────────┘
│
Target Method
│
┌───────────┴───────────┐
AfterReturning AfterThrowing
│ │
(Result) (Exception)
│ │
└───────────┬───────────┘
│
After (finally)二、AOP 核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| Aspect(切面) | 通知 + 切点的结合 | 完整的面 |
| Join Point(连接点) | 可以织入通知的时机 | 可织入的点 |
| Pointcut(切点) | 匹配连接点的表达式 | 筛选条件 |
| Advice(通知) | 织入的具体逻辑 | 要做什么 |
| Weaving(织入) | 将通知应用到目标的过程 | 织进去 |
一张图理解
┌─────────────────┐
│ Aspect │ ← 切面 = 通知 + 切点
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Pointcut │ │ Advice │ │ 切面 │
│ (where) │ │ (what) │ │ (通知+切点)│
└───────────┘ └───────────┘ └───────────┘
┌─────────────────────────────────────────────────────┐
│ 目标对象 Target │
│ ┌─────────────────────────────────────────────┐ │
│ │ public void save(User user) { ... } │ │
│ │ │ │
│ │ ← Before → ← AfterReturning → ← After → │ │
│ └─────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘三、五种通知类型
1. @Before(前置通知)
java
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void before(JoinPoint joinPoint) {
// 获取方法签名
String methodName = joinPoint.getSignature().getName();
// 获取参数
Object[] args = joinPoint.getArgs();
System.out.println("[Before] " + methodName + " 开始执行,参数: " + Arrays.toString(args));
}
}2. @AfterReturning(返回通知)
java
@Aspect
@Component
public class PerformanceAspect {
@AfterReturning(value = "execution(* com.example.service.*.*(..))",
returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AfterReturning] " + methodName + " 返回: " + result);
}
}3. @AfterThrowing(异常通知)
java
@Aspect
@Component
public class ExceptionAspect {
@AfterThrowing(value = "execution(* com.example.service.*.*(..))",
throwing = "exception")
public void afterThrowing(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AfterThrowing] " + methodName + " 异常: " + exception.getMessage());
}
}4. @After(后置通知)
java
@Aspect
@Component
public class FinallyAspect {
@After("execution(* com.example.service.*.*(..))")
public void after(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[After] " + methodName + " 执行结束");
}
}5. @Around(环绕通知)- 最强大
java
@Aspect
@Component
public class AroundAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
long start = System.currentTimeMillis();
try {
// 执行目标方法
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - start;
System.out.println("[Around] " + methodName + " 执行耗时: " + cost + "ms");
return result;
} catch (Throwable e) {
long cost = System.currentTimeMillis() - start;
System.out.println("[Around] " + methodName + " 执行异常,耗时: " + cost + "ms");
throw e;
}
}
}执行顺序
try {
// @Before
result = joinPoint.proceed(); // 目标方法
// @AfterReturning + @After
} catch (Exception e) {
// @AfterThrowing + @After
} finally {
// @After (总是执行)
}四、切点表达式
语法
execution([修饰符] 返回类型 [类名.]方法名(参数) [异常])常用示例
java
// 1. 匹配指定类的方法
@Before("execution(public User com.example.UserService.getById(Long))")
// 2. 匹配所有 public 方法
@Before("execution(public * *(..))")
// 3. 匹配 service 包下所有方法
@Before("execution(* com.example.service.*.*(..))")
// 4. 匹配 service 及其子包下所有方法
@Before("execution(* com.example.service..*.*(..))")
// 5. 匹配以 get 开头的方法
@Before("execution(* get*(..))")
// 6. 匹配 UserService 中所有方法
@Before("execution(* com.example.UserService.*(..))")
// 7. 匹配第一个参数为 User 的方法
@Before("execution(* *(com.example.User, ..))")
// 8. 匹配抛出特定异常的方法
@Before("execution(* *(..) throws java.io.IOException)")切点组合
java
@Aspect
@Component
public class CombinedAspect {
// && 组合
@Before("execution(* com.example.service.*.*(..)) && args(..)")
public void beforeService(JoinPoint jp) { }
// || 组合
@Before("execution(* com.example.service.*.*(..)) || execution(* com.example.dao.*.*(..))")
public void beforeAll(JoinPoint jp) { }
// ! 取反
@Before("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.internal.*.*(..))")
public void beforePublic(JoinPoint jp) { }
// @annotation 匹配带特定注解的方法
@Before("@annotation(com.example.MyLoggable)")
public void beforeAnnotated(JoinPoint jp) { }
}@Pointcut 复用切点
java
@Aspect
@Component
public class LogAspect {
// 定义切点
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() { }
@Pointcut("execution(* com.example.dao.*.*(..))")
public void daoLayer() { }
@Pointcut("serviceLayer() || daoLayer()")
public void appLayer() { }
// 复用切点
@Before("serviceLayer()")
public void beforeService(JoinPoint jp) {
System.out.println("Before service");
}
@Before("daoLayer()")
public void beforeDao(JoinPoint jp) {
System.out.println("Before dao");
}
}五、基于注解的 AOP
完整示例
java
// 1. 启用 AOP
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.example")
public class AppConfig { }
// 2. 定义切面
@Aspect
@Component
public class TransactionAspect {
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePointcut() { }
@Around("servicePointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
String methodName = pjp.getSignature().getName();
System.out.println("[" + methodName + "] 开启事务");
try {
Object result = pjp.proceed();
System.out.println("[" + methodName + "] 提交事务");
return result;
} catch (Exception e) {
System.out.println("[" + methodName + "] 回滚事务");
throw e;
}
}
}
// 3. 定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Retryable {
int maxAttempts() default 3;
long delay() default 1000;
}
// 4. 使用注解定义切面
@Aspect
@Component
public class RetryAspect {
@Around("@annotation(retryable)")
public Object around(ProceedingJoinPoint pjp, Retryable retryable) throws Throwable {
int attempts = 0;
while (attempts < retryable.maxAttempts()) {
try {
return pjp.proceed();
} catch (Exception e) {
attempts++;
if (attempts >= retryable.maxAttempts()) throw e;
Thread.sleep(retryable.delay());
}
}
throw new RuntimeException("Max attempts reached");
}
}
// 5. 使用
@Service
public class UserService {
@Retryable(maxAttempts = 3, delay = 500)
public void callRemote() {
// 可能失败的远程调用
}
}注解版本顺序控制
java
@Aspect
@Component
@Order(1) // 数字越小越先执行
public class TransactionAspect { }
@Aspect
@Component
@Order(2) // 后执行
public class LoggingAspect { }六、基于 XML 的 AOP(传统方式)
xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop">
<!-- 1. 启用 AspectJ 自动代理 -->
<aop:aspectj-autoproxy />
<!-- 2. 定义切面 Bean -->
<bean id="loggingAspect" class="com.example.LoggingAspect" />
<bean id="transactionAspect" class="com.example.TransactionAspect" />
<!-- 3. 配置切点 + 通知 -->
<aop:config>
<!-- 切面 -->
<aop:aspect id="logAspect" ref="loggingAspect">
<!-- 切点 -->
<aop:pointcut id="servicePointcut"
expression="execution(* com.example.service.*.*(..))" />
<!-- 前置通知 -->
<aop:before method="before" pointcut-ref="servicePointcut" />
<!-- 返回通知 -->
<aop:after-returning method="afterReturning"
pointcut-ref="servicePointcut" returning="result" />
<!-- 环绕通知 -->
<aop:around method="around" pointcut-ref="servicePointcut" />
</aop:aspect>
</aop:config>
</beans>七、JoinPoint 获取信息
java
@Aspect
@Component
public class InfoAspect {
@Before("execution(* com.example.service.*.*(..))")
public void info(JoinPoint joinPoint) {
// 获取目标对象
Object target = joinPoint.getTarget();
System.out.println("Target: " + target.getClass().getName());
// 获取方法签名
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
System.out.println("Method: " + signature.getName());
System.out.println("ReturnType: " + signature.getReturnType());
System.out.println("ParameterNames: " + Arrays.toString(signature.getParameterNames()));
// 获取参数值
Object[] args = joinPoint.getArgs();
System.out.println("Args: " + Arrays.toString(args));
// 获取 this(代理对象)
Object proxy = joinPoint.getThis();
System.out.println("Proxy: " + proxy.getClass().getName());
}
}八、Spring AOP vs AspectJ
| 对比 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行时 | 编译时/加载时 |
| 复杂度 | 简单 | 复杂 |
| 性能 | 略低(有代理开销) | 更高(编译时织入) |
| 功能 | 方法级别 | 字段/构造函数/静态块 |
| 配置 | 简单 | 需要 AspectJ 编译器 |
| 使用场景 | 业务层(推荐) | 需要精细控制时 |
java
// Spring AOP 支持的 JoinPoint 位置
// 只有方法执行的连接点
public void method() { } ✅ 可织入
private void method() { } ❌ 不可织入(无法外部访问)
static void method() { } ❌ 不可织入九、实际应用场景
1. 日志记录
java
@Aspect
@Component
public class LoggingAspect {
private final Logger log = LoggerFactory.getLogger(getClass());
@Around("execution(* com.example.controller.*.*(..))")
public Object controllerLog(ProceedingJoinPoint pjp) throws Throwable {
HttpServletRequest request = getRequest();
log.info("{} {} - {}", request.getMethod(), request.getRequestURI(), getClientIP());
return pjp.proceed();
}
}2. 性能监控
java
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object monitor(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed();
} finally {
long cost = (System.nanoTime() - start) / 1_000_000;
if (cost > 1000) { // 超过1秒告警
log.warn("Slow method: {}.{} took {}ms",
pjp.getSignature().getDeclaringTypeName(),
pjp.getSignature().getName(), cost);
}
}
}
}3. 权限校验
java
@Aspect
@Component
public class SecurityAspect {
@Before("execution(* com.example.service.admin.*.*(..))")
public void checkAdmin(JoinPoint jp) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (!auth.getAuthorities().contains("ROLE_ADMIN")) {
throw new SecurityException("Admin access required");
}
}
}4. 缓存
java
@Aspect
@Component
public class CacheAspect {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
@Around("@annotation(Cacheable)")
public Object cache(ProceedingJoinPoint pjp, Cacheable cacheable) throws Throwable {
String key = cacheable.value() + ":" + Arrays.toString(pjp.getArgs());
if (cache.containsKey(key)) {
return cache.get(key);
}
Object result = pjp.proceed();
cache.put(key, result);
return result;
}
}十、面试高频问题
Q1: AOP 核心概念有哪些?
- Aspect(切面):通知 + 切点
- Join Point(连接点):可织入点
- Pointcut(切点):匹配表达式
- Advice(通知):要织入的逻辑
- Weaving(织入):应用通知的过程
Q2: Spring AOP 实现原理?
- Spring 在启动时创建代理对象
- 默认使用 JDK 动态代理(接口实现类)
- 如果目标对象没有接口,使用 CGLIB 字节码增强
- 方法调用通过代理对象,通知在调用前后执行
java
// 代理对象调用流程
proxy.save(user);
// 相当于执行:
try {
beforeAdvice(); // 前置通知
target.save(user); // 目标方法
afterReturning(); // 返回通知
} catch (Exception e) {
afterThrowing(); // 异常通知
} finally {
after(); // 后置通知
}Q3: JDK 动态代理 vs CGLIB?
| 对比 | JDK 动态代理 | CGLIB |
|---|---|---|
| 原理 | 反射 | 字节码生成 |
| 要求 | 目标类实现接口 | 不需要接口 |
| 性能 | 略慢 | 更快 |
| 生成物 | $Proxy0 继承自 Proxy | 子类继承目标类 |
java
// 强制使用 CGLIB
@EnableAspectJAutoProxy(proxyTargetClass = true)Q4: @Before 和 @Around 哪个先执行?
@Around 开始
↓
@Before
↓
目标方法
↓
@AfterReturning / @AfterThrowing
↓
@After
↓
@Around 结束Q5: 什么是织入(Weaving)?
将切面的通知应用到目标对象的过程。时机:
- 编译时织入:AspectJ 编译器
- 类加载时织入:AspectJ LTW
- 运行时织入:Spring AOP(创建代理)
十一、下一章预告
下一章我们将学习 Bean 的作用域与生命周期:
- Bean 的 6 种作用域详解
- Bean 的完整生命周期(创建 → 初始化 → 销毁)
- Spring 后置处理器(BeanPostProcessor)
- Spring 初始化和销毁回调