跳到主要内容

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 默认策略:

  1. 如果目标类实现了接口,使用 JDK 动态代理
  2. 如果目标类没有实现接口,使用 CGLIB 代理
  3. 可以通过配置强制使用 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 的核心概念和使用方法:

  1. AOP 思想:将横切关注点与业务逻辑分离
  2. 核心概念:切面、连接点、切入点、通知、目标对象、代理、织入
  3. 切入点表达式:execution、within、@annotation、bean 等
  4. 通知类型:@Before、@AfterReturning、@AfterThrowing、@After、@Around
  5. 参数绑定:绑定方法参数、注解、目标对象
  6. 代理机制:JDK 动态代理和 CGLIB 代理
  7. 实际应用:日志、校验、异常处理等切面示例

下一章我们将学习 Spring 的事务管理,这是 AOP 的典型应用场景。