AOP 面向切面编程
AOP(Aspect-Oriented Programming,面向切面编程)是 Spring 的核心特性之一,它通过将横切关注点与业务逻辑分离,实现了代码的模块化和复用。
什么是 AOP?
横切关注点
在软件开发中,有些功能会跨越多个模块,比如日志记录、事务管理、安全检查、性能监控等。这些功能被称为"横切关注点"(Cross-cutting Concerns)。
传统的 OOP 方式中,这些横切关注点会分散在各个业务类中:
public class UserService {
public User getUser(Long id) {
long startTime = System.currentTimeMillis();
log.info("开始获取用户: {}", id);
try {
User user = userDao.findById(id);
log.info("获取用户成功");
return user;
} catch (Exception e) {
log.error("获取用户失败", e);
throw e;
} finally {
long endTime = System.currentTimeMillis();
log.info("耗时: {}ms", endTime - startTime);
}
}
}
可以看到,真正业务逻辑只有一行代码,但日志和性能监控代码占据了大部分篇幅。如果每个方法都这样写,代码会变得臃肿且难以维护。
AOP 的解决方案
AOP 将这些横切关注点封装成独立的模块(称为"切面"),通过声明的方式应用到需要的地方:
public class UserService {
public User getUser(Long id) {
return userDao.findById(id);
}
}
@Aspect
@Component
public class LoggingAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
log.info("开始执行: {}", joinPoint.getSignature().getName());
try {
Object result = joinPoint.proceed();
log.info("执行成功");
return result;
} catch (Exception e) {
log.error("执行失败", e);
throw e;
} finally {
log.info("耗时: {}ms", System.currentTimeMillis() - startTime);
}
}
}
业务代码变得纯粹,横切关注点被封装在切面中,实现了关注点分离。
AOP 核心概念
切面(Aspect)
切面是横切关注点的模块化封装。在 Spring 中,切面是一个带有 @Aspect 注解的类:
@Aspect
@Component
public class LoggingAspect {
}
连接点(Join Point)
连接点是程序执行过程中的某个特定点,如方法调用、异常抛出等。Spring AOP 只支持方法级别的连接点。
@Around("execution(* com.example.service.*.*(..))")
public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
Object target = joinPoint.getTarget();
Object result = joinPoint.proceed();
return result;
}
切入点(Pointcut)
切入点是匹配连接点的表达式,定义了通知应该在哪些地方执行:
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
log.info("执行方法: {}", joinPoint.getSignature().getName());
}
通知(Advice)
通知是切面在特定连接点执行的动作。Spring 提供五种通知类型:
| 通知类型 | 注解 | 说明 |
|---|---|---|
| 前置通知 | @Before | 方法执行前执行 |
| 后置通知 | @AfterReturning | 方法成功返回后执行 |
| 异常通知 | @AfterThrowing | 方法抛出异常后执行 |
| 最终通知 | @After | 方法执行后执行(无论成功或异常) |
| 环绕通知 | @Around | 完全控制方法执行 |
目标对象(Target Object)
被切面增强的对象。Spring AOP 使用代理模式实现,目标对象被包装在代理对象中。
代理(Proxy)
Spring AOP 通过代理对象实现切面功能。Spring 默认使用 JDK 动态代理(基于接口)或 CGLIB(基于类)。
织入(Weaving)
将切面应用到目标对象并创建代理对象的过程。Spring AOP 在运行时进行织入。
切入点表达式
切入点表达式是 AOP 的核心,用于匹配连接点。Spring 使用 AspectJ 的切入点表达式语法。
execution 表达式
execution 是最常用的切入点表达式,用于匹配方法执行:
execution(modifiers? return-type declaring-type? method-name(param-types) throws?)
匹配所有 public 方法:
execution(public * *(..))
匹配所有 setter 方法:
execution(* set*(..))
匹配特定包下的所有方法:
execution(* com.example.service.*.*(..))
匹配特定包及其子包下的所有方法:
execution(* com.example.service..*.*(..))
匹配特定类的所有方法:
execution(* com.example.service.UserService.*(..))
匹配特定方法:
execution(* com.example.service.UserService.getUser(..))
匹配特定参数类型的方法:
execution(* com.example.service.UserService.getUser(Long))
匹配任意参数的方法:
execution(* com.example.service.UserService.getUser(..))
within 表达式
匹配特定类型内的所有连接点:
@within(com.example.annotation.Loggable)
public void logMethod() { }
within(com.example.service.UserService)
within(com.example.service..*)
@annotation 表达式
匹配带有特定注解的方法:
@annotation(com.example.annotation.Loggable)
@within 表达式
匹配带有特定注解的类:
@within(com.example.annotation.ServiceLog)
bean 表达式
匹配特定名称的 Bean:
bean(userService)
bean(*Service)
组合切入点
使用 &&、||、! 组合多个切入点:
@Pointcut("execution(* com.example.service.*.*(..)) && args(id,..)")
public void serviceMethodWithId(Long id) {}
@Pointcut("execution(* com.example.service.*.*(..)) || execution(* com.example.dao.*.*(..))")
public void serviceOrDao() {}
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.UserService.getUser(..))")
public void serviceExceptGetUser() {}
通知类型详解
@Before 前置通知
在目标方法执行前执行:
@Aspect
@Component
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("执行方法: {}, 参数: {}", methodName, Arrays.toString(args));
}
}
前置通知不能阻止目标方法执行,除非抛出异常。
@AfterReturning 后置通知
在目标方法成功返回后执行:
@Aspect
@Component
public class LoggingAspect {
@AfterReturning(
pointcut = "execution(* com.example.service.*.*(..))",
returning = "result"
)
public void logAfterReturning(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
log.info("方法 {} 执行成功, 返回值: {}", methodName, result);
}
}
returning 属性指定接收返回值的参数名,参数类型会限制匹配范围。
@AfterThrowing 异常通知
在目标方法抛出异常后执行:
@Aspect
@Component
public class ExceptionAspect {
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex"
)
public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
log.error("方法 {} 执行异常: {}", methodName, ex.getMessage());
}
}
throwing 属性指定接收异常的参数名,参数类型会限制匹配的异常类型。
@After 最终通知
在目标方法执行后执行,无论成功或异常:
@Aspect
@Component
public class LoggingAspect {
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
log.info("方法 {} 执行完成", methodName);
}
}
最终通知类似于 try-catch-finally 中的 finally 块。
@Around 环绕通知
环绕通知是最强大的通知类型,可以完全控制目标方法的执行:
@Aspect
@Component
public class PerformanceAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object logPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
log.info("开始执行方法: {}", methodName);
try {
Object result = joinPoint.proceed();
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("方法 {} 执行成功, 耗时: {}ms", methodName, elapsedTime);
return result;
} catch (Exception e) {
long elapsedTime = System.currentTimeMillis() - startTime;
log.error("方法 {} 执行失败, 耗时: {}ms, 异常: {}", methodName, elapsedTime, e.getMessage());
throw e;
}
}
}
环绕通知的特点:
必须调用 proceed():否则目标方法不会执行。
可以修改参数:joinPoint.proceed(newArgs) 传入新参数。
可以修改返回值:直接返回不同的值。
可以捕获或转换异常:捕获异常后可以返回默认值或抛出不同异常。
通知执行顺序
当多个切面作用于同一连接点时,执行顺序如下:
切面1 @Around 前半部分
切面1 @Before
切面2 @Around 前半部分
切面2 @Before
目标方法
切面2 @After / @AfterReturning / @AfterThrowing
切面2 @Around 后半部分
切面1 @After / @AfterReturning / @AfterThrowing
切面1 @Around 后半部分
使用 @Order 控制切面执行顺序:
@Aspect
@Component
@Order(1)
public class LoggingAspect { }
@Aspect
@Component
@Order(2)
public class TransactionAspect { }
数字越小优先级越高,越先执行。
切入点参数绑定
切入点表达式可以绑定参数,在通知方法中直接使用:
绑定方法参数
@Aspect
@Component
public class ValidationAspect {
@Before("execution(* com.example.service.UserService.getUser(..)) && args(id)")
public void validateId(Long id) {
if (id == null || id <= 0) {
throw new IllegalArgumentException("无效的用户ID");
}
log.info("验证用户ID: {}", id);
}
}
绑定多个参数
@Before("execution(* com.example.service.UserService.updateUser(..)) && args(id, user)")
public void validateUpdate(Long id, User user) {
if (id == null || id <= 0) {
throw new IllegalArgumentException("无效的用户ID");
}
if (user == null || user.getName() == null) {
throw new IllegalArgumentException("用户信息不完整");
}
}
绑定注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value() default "";
}
@Aspect
@Component
public class LoggableAspect {
@Around("@annotation(loggable)")
public Object log(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
String operation = loggable.value();
log.info("开始执行操作: {}", operation);
try {
Object result = joinPoint.proceed();
log.info("操作 {} 执行成功", operation);
return result;
} catch (Exception e) {
log.error("操作 {} 执行失败", operation, e);
throw e;
}
}
}
@Service
public class UserService {
@Loggable("获取用户")
public User getUser(Long id) {
return userDao.findById(id);
}
}
绑定目标对象
@Before("execution(* com.example.service.*.*(..)) && target(service)")
public void logTarget(JoinPoint joinPoint, Object service) {
log.info("目标对象: {}", service.getClass().getSimpleName());
}
引入(Introduction)
引入允许切面为现有类添加新的接口和实现:
public interface Lockable {
void lock();
void unlock();
boolean isLocked();
}
public class LockableImpl implements Lockable {
private boolean locked = false;
@Override
public void lock() {
this.locked = true;
}
@Override
public void unlock() {
this.locked = false;
}
@Override
public boolean isLocked() {
return this.locked;
}
}
@Aspect
@Component
public class LockableAspect {
@DeclareParents(value = "com.example.service.*+", defaultImpl = LockableImpl.class)
public static Lockable lockable;
}
@Service
public class UserService {
public void updateUser(User user) {
if (((Lockable) this).isLocked()) {
throw new IllegalStateException("服务已锁定");
}
}
}
代理机制
Spring AOP 使用两种代理机制:
JDK 动态代理
基于接口的代理,要求目标类实现至少一个接口:
public interface UserService {
User getUser(Long id);
}
@Service
public class UserServiceImpl implements UserService {
@Override
public User getUser(Long id) {
return userDao.findById(id);
}
}
CGLIB 代理
基于类的代理,通过生成子类实现代理:
@Service
public class UserService {
public User getUser(Long id) {
return userDao.findById(id);
}
}
代理选择策略
Spring 默认策略:
- 如果目标类实现了接口,使用 JDK 动态代理
- 如果目标类没有实现接口,使用 CGLIB 代理
- 可以通过配置强制使用 CGLIB:
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig {
}
代理的限制
Spring AOP 是基于代理的 AOP 实现,有以下限制:
不能拦截 final 方法和类:CGLIB 通过继承实现,final 无法被继承。
不能拦截私有方法:私有方法对外不可见,代理无法访问。
同类内部调用不经过代理:
@Service
public class UserService {
public void methodA() {
methodB();
}
@Transactional
public void methodB() {
}
}
methodA() 调用 methodB() 时,事务不会生效,因为是内部调用,不经过代理。
解决方案:
@Service
public class UserService {
@Autowired
private UserService self;
public void methodA() {
self.methodB();
}
@Transactional
public void methodB() {
}
}
或者使用 AopContext:
@Service
public class UserService {
public void methodA() {
((UserService) AopContext.currentProxy()).methodB();
}
@Transactional
public void methodB() {
}
}
需要配置:
@EnableAspectJAutoProxy(exposeProxy = true)
实际应用示例
日志切面
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Around("@annotation(com.example.annotation.Log)")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("{}.{} 开始执行, 参数: {}", className, methodName, Arrays.toString(args));
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long elapsedTime = System.currentTimeMillis() - startTime;
log.info("{}.{} 执行成功, 耗时: {}ms, 返回值: {}", className, methodName, elapsedTime, result);
return result;
} catch (Exception e) {
long elapsedTime = System.currentTimeMillis() - startTime;
log.error("{}.{} 执行失败, 耗时: {}ms", className, methodName, elapsedTime, e);
throw e;
}
}
}
参数校验切面
@Aspect
@Component
public class ValidationAspect {
@Before("@annotation(com.example.annotation.Validate)")
public void validate(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
if (arg == null) {
throw new IllegalArgumentException("参数不能为空");
}
if (arg instanceof String && ((String) arg).isEmpty()) {
throw new IllegalArgumentException("字符串参数不能为空");
}
if (arg instanceof Long && (Long) arg <= 0) {
throw new IllegalArgumentException("ID必须大于0");
}
}
}
}
异常处理切面
@Aspect
@Component
@RestControllerAdvice
public class ExceptionAspect {
@AfterThrowing(pointcut = "execution(* com.example.controller.*.*(..))", throwing = "ex")
public ResponseEntity<ErrorResponse> handleException(JoinPoint joinPoint, Exception ex) {
String methodName = joinPoint.getSignature().getName();
log.error("方法 {} 发生异常: {}", methodName, ex.getMessage(), ex);
ErrorResponse error = new ErrorResponse();
error.setTimestamp(LocalDateTime.now());
error.setMessage(ex.getMessage());
error.setPath(joinPoint.getSignature().getDeclaringTypeName() + "." + methodName);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
小结
本章详细介绍了 Spring AOP 的核心概念和使用方法:
- AOP 思想:将横切关注点与业务逻辑分离
- 核心概念:切面、连接点、切入点、通知、目标对象、代理、织入
- 切入点表达式:execution、within、@annotation、bean 等
- 通知类型:@Before、@AfterReturning、@AfterThrowing、@After、@Around
- 参数绑定:绑定方法参数、注解、目标对象
- 代理机制:JDK 动态代理和 CGLIB 代理
- 实际应用:日志、校验、异常处理等切面示例
下一章我们将学习 Spring 的事务管理,这是 AOP 的典型应用场景。