45_Spring AOP面向切面编程

Spring 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层日志的注意事项:打印请求和响应是很常见的做法,但有几个坑需要注意:

  1. 敏感信息脱敏:密码、手机号、身份证号等敏感信息不应该明文打印在日志中,需要做脱敏处理
  2. 大响应体截断:如果响应是一个几十MB的文件或大JSON,直接打印会撑爆日志文件,应该做截断处理
  3. 耗时统计:在打印响应日志时,最好同时记录方法执行耗时,方便后期性能分析
  4. 异常情况:目标方法抛出异常时,@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常见失效场景:以下情况事务不会生效,面试和实际开发中都要注意:

  1. 方法不是public的:Spring AOP只能拦截public方法,protected/private/默认的方法上@Transactional不生效
  2. 同类内方法调用:如方法A调用本类的@Transactional方法B,调用的是this而非代理对象,AOP不生效
  3. 异常被catch了:如果方法内部使用try-catch捕获了异常且没有再次抛出,Spring感知不到异常,事务会提交
  4. rollbackFor未正确配置 :@Transactional默认只回滚RuntimeException和Error,对于受检异常(如IOException)需要显式配置rollbackFor = Exception.class
  5. 数据库引擎不支持事务:如果使用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注意事项

  1. 自调用问题:类内部方法互相调用时,AOP不会生效。这是因为调用的是this对象而不是代理对象。解决方法是注入自身或使用AopContext。

自调用问题的根本原因 :Spring AOP是通过代理对象来实现的。当你通过Spring注入的userService调用方法时,实际上调用的是代理对象的方法,代理会在方法前后执行AOP逻辑。但当userService内部通过this调用自己的另一个方法时,this是原始对象而非代理对象,AOP自然无法生效。解决方案:(1)将需要AOP的方法拆分到另一个Bean中;(2)通过AopContext.currentProxy()获取代理对象再调用;(3)注入自身:在类中使用@Autowired private UserService self;然后用self调用。

  1. final方法不能被代理:CGLIB通过继承实现代理,final方法无法被覆写。
  2. 切面执行顺序 :多个切面可以指定执行顺序,使用 @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快速入门),体验"约定优于配置"的开发体验