Spring框架之AOP

一、什么是AOP

1.1 定义

  • AOP,即面向切面编程。
  • 它是一种编程范式,旨在将程序中横切关注点业务逻辑分离。

1.2 横切关注点

  • 指在整个应用程序中都会涉及的功能,但是又与核心业务无关,例如:
    • 日志记录
    • 权限管理
    • 异常处理
  • 这些功能往往散布在代码的各个角落,如果直接写在业务代码中,会导致代码冗余、难以维护。

1.3 AOP核心思想

  • 将横切关注点模块化:通过AOP,可以将这些横切关注点封装成模块,成为切面。
  • 动态的将切面应用到目标对象:不需要修改原有代码,就能为其添加新的功能。

二、AOP有什么用

2.1 代码解耦

  • 降低耦合度:业务逻辑与横切关注点分离,代码更清晰,职责更明确。
  • 提高可维护性:修改横切关注点的实现时,只需要修改对应的切面,无需修改业务代码。

2.2 代码复用

  • 统一管理:将通用的功能(如日志、事务)集中管理,避免重复代码。
  • 便于扩展:添加新的横切关注点时,不需要修改原有业务代码。

三、Spring AOP简介

3.1 Spring AOP是什么?

  • Spring框架提供的面向切面编程的模块,允许在程序运行时将切面织入到Spring管理的bean中。
  • 织入:将切面应用到目标对象的过程。

3.2 AOP的相关概念

3.2.1 通知(Advice)
  • 定义:切面要执行的动作,即具体要做什么。
  • 类型
    • 前置通知(Before Advice):在方法执行前执行。
    • 后置通知(After Advice):在方法执行后执行。
    • 返回后通知(After Returning Advice):在方法成功返回后执行。
    • 异常后通知(After Throwing Advice):在方法抛出异常后执行。
    • 环绕通知(Around Advice):包裹目标方法,能在方法执行前后做处理。
3.2.1 连接点(Joinpoint)
  • 定义:程序执行的某个点,如方法的调用或异常的抛出。Spring AOP 只支持方法级别的连接点。
3.2.3 切入点(Pointcut)
  • 定义:匹配连接点的表达式,用于筛选出哪些连接点应用通知。
  • 作用:决定了通知应用于哪些方法。
3.2.4 切面(Aspect)
  • 定义:通知和切入点的结合,即切面的定义包括了要做什么(通知)以及在哪里做(切入点)。
3.2.5 目标对象(Target Object)
  • 定义:被通知的对象,即被代理的业务对象。
3.2.6 代理(Proxy)
  • 定义:AOP 织入切面后,创建的增强对象,包含了原有对象的功能和切面的功能。

四、Spring AOP的实现

4.1 动态代理

  • Spring AOP 基于 动态代理 实现,即在运行时创建目标对象的代理对象。
  • 两种代理方式
    • JDK 动态代理:基于接口的代理,仅能代理实现了接口的类。
    • CGLIB 动态代理:基于继承的代理,对目标类生成子类,覆盖其方法。适用于没有实现接口的类。

4.2 AOP在Spring中的工作流程

  1. 定义切面:编写一个包含通知和切入点的类。
  2. 配置切面:通过注解或 XML,将切面声明为 Spring 管理的 Bean。
  3. 织入切面:Spring 容器在启动时,扫描并织入切面,生成代理对象。
  4. 调用代理对象:应用程序获取的是代理对象,调用方法时,AOP 框架会根据切入点和通知的配置,执行相应的切面逻辑。

五、实战讲解

5.1 引入依赖

若使用 Spring Boot,在 pom.xml 中:

<!-- AOP 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

5.2 编写业务代码

假设有一个用户服务:

public interface UserService {
    void createUser(String name);
}

@Service
public class UserServiceImpl implements UserService {
    @Override
    public void createUser(String name) {
        // 模拟创建用户的业务逻辑
        System.out.println("创建用户:" + name);
    }
}

5.3 编写切面

我们想在用户创建前后记录日志。

@Aspect // 标记为切面
@Component // 由 Spring 管理
public class LoggingAspect {

    // 定义切入点,匹配 UserService 的所有方法
    @Pointcut("execution(* com.example.service.UserService.*(..))")
    public void userServiceMethods() {}

    // 前置通知
    @Before("userServiceMethods()")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("方法执行前:" + joinPoint.getSignature().getName());
    }

    // 后置通知
    @After("userServiceMethods()")
    public void afterAdvice(JoinPoint joinPoint) {
        System.out.println("方法执行后:" + joinPoint.getSignature().getName());
    }

    // 环绕通知
    @Around("userServiceMethods()")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("环绕通知开始");
        Object result = pjp.proceed(); // 执行目标方法
        System.out.println("环绕通知结束");
        return result;
    }
}

5.4 说明

  • @Aspect:标记该类为切面。
  • @Component:将该类交由 Spring 容器管理。
  • @Pointcut:定义切入点,execution(* com.example.service.UserService.*(..)) 表示匹配 UserService 接口下的所有方法。
  • @Before@After@Around:分别定义不同类型的通知,关联到指定的切入点。

六、深入原理

6.1 动态代理原理

6.1.1 JDK 动态代理
  • 适用条件:目标对象实现了接口。
  • 实现方式
    • 通过反射机制,在运行时动态创建接口的实现类。
    • 代理对象实现了与目标对象相同的接口。
    • 方法调用时,代理对象负责拦截,并在适当的时机执行通知逻辑。
6.1.2 CGLIB 动态代理
  • 适用条件:目标对象未实现接口。
  • 实现方式
    • 通过继承目标类,创建子类作为代理对象。
    • 利用字节码操作,在运行时生成代理类的字节码。
    • 方法调用时,代理对象覆盖父类的方法,加入通知逻辑。

6.2 Spring AOP 的代理选择

  • 默认策略
    • 优先使用 JDK 动态代理:如果目标对象实现了接口。
    • 使用 CGLIB 动态代理:如果目标对象没有实现接口。
  • 可以强制使用 CGLIB
    • 在配置中设置 spring.aop.proxy-target-class=true

6.3 AOP 的执行流程

  1. 容器启动:Spring 容器启动时,加载所有的 Bean 定义。
  2. 扫描切面:检测所有标记了 @Aspect 的类。
  3. 解析通知和切入点:建立通知方法与切入点之间的关系。
  4. 代理对象创建:对于匹配了切入点的目标 Bean,生成对应的代理对象。
  5. 方法调用:应用程序调用目标方法时,实际上调用的是代理对象的方法。
  6. 通知执行:代理对象根据通知类型,在合适的时机执行切面逻辑。

七、AOP 的应用场景

7.1 日志记录

  • 需求:在方法调用前后记录日志。
  • 实现:使用前置通知和后置通知,打印方法名、参数、执行时间等信息。

7.2 事务管理

  • 需求:在方法执行时保证事务的一致性,出现异常时回滚。
  • 实现:使用环绕通知,在方法执行前开启事务,成功后提交事务,异常时回滚。

7.3 安全检查

  • 需求:在方法执行前检查用户权限。
  • 实现:使用前置通知,在方法执行前验证权限,不通过则阻止方法执行。

7.4 缓存管理

  • 需求:在方法调用时,先检查缓存中是否有结果,有则返回缓存,没有则调用方法并缓存结果。
  • 实现:使用环绕通知,先从缓存获取数据,控制方法执行与否。

八、注意事项

8.1 AOP 只对 Spring 管理的 Bean 生效

  • 解释:只有被 Spring 容器托管的对象,才能被 AOP 代理或增强。

8.2 自调用问题

  • 现象:同一个类内部的方法调用,AOP 可能失效。
  • 原因:自调用时,调用的是对象自身的方法,而不是代理对象的方法。
  • 解决方案
    • 将自调用的方法提取到另一个类中。
    • 使用 AOP 的 @Configurable 注解(较为复杂)。

8.3 使用 CGLIB 的限制

  • final 方法无法代理:由于 CGLIB 基于继承,final 方法无法被重写,因此无法被代理。
  • 接口优先:如果目标类实现了接口,默认会使用 JDK 动态代理。

九、总结

  • AOP 的核心:通过将横切关注点从业务逻辑中分离出来,达到解耦和复用的目的。
  • Spring AOP:利用动态代理机制,实现了在运行时织入切面的功能,简化了 AOP 的使用。
  • 实践建议
    • 从简单开始:先尝试编写简单的切面,理解基本概念。
    • 逐步深入:了解底层的代理机制,有助于理解 AOP 的执行流程。
    • 注意细节:留意自调用问题、代理方式的选择等。

十、延伸阅读

  • AspectJ:Spring AOP 是基于代理的,只能在运行时织入,而 AspectJ 是完整的 AOP 框架,支持编译时、类加载时织入,功能更强大。
  • AOP 与 OOP 的关系
    • OOP(面向对象编程):关注的是模块化,封装数据和行为。
    • AOP:关注的是横切关注点,弥补了 OOP 在处理横切关注点时的不足。
分类: 标签: Spring AOP 切面编程

评论

全部评论