Spring AOP 干货分享:切面编程的核心原理与应用

134 阅读14分钟

关于AOP

面向切面编程(Aspect-oriented Programming,俗称AOP)提供了一种面向对象编程(Object-oriented Programming,俗称OOP)的补充,面向对象编程最核心的单元是类(class),然而面向切面编程最核心的单元是切面(Aspects)。与面向对象的顺序流程不同,AOP采用的是横向切面的方式,注入与主业务流程无关的功能,例如事务管理和日志管理。

image.png

AOP 是一种编程范式,最早由 AOP 联盟的组织提出的,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。它是 OOP的延续。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率

AOP 的概念

在深入学习SpringAOP 之前,让我们先对AOP的几个基本术语有个大致的概念,这些概念不是很容易理解,比较抽象,可以知道有这么几个概念,下面一起来看一下:

  • 切面(Aspect): Aspect 声明类似于 Java 中的类声明,事务管理是AOP一个最典型的应用。在AOP中,切面一般使用 @Aspect 注解来使用,在XML 中,可以使用  <aop:aspect>  来定义一个切面。
  • 连接点(Join Point): 一个在程序执行期间的某一个操作,就像是执行一个方法或者处理一个异常。在Spring AOP中,一个连接点就代表了一个方法的执行。
  • 通知(Advice):在切面中(类)的某个连接点(方法出)采取的动作,会有四种不同的通知方式: around(环绕通知),before(前置通知),after(后置通知), exception(异常通知),return(返回通知) 。许多AOP框架(包括Spring)将建议把通知作为为拦截器,并在连接点周围维护一系列拦截器。
  • 切入点(Pointcut):表示一组连接点,通知与切入点表达式有关,并在切入点匹配的任何连接点处运行(例如执行具有特定名称的方法)。由切入点表达式匹配的连接点的概念是AOP的核心,Spring默认使用AspectJ切入点表达式语言。
  • 介绍(Introduction): introduction可以为原有的对象增加新的属性和方法。例如,你可以使用introduction使bean实现IsModified接口,以简化缓存。
  • 目标对象(Target Object): 由一个或者多个切面代理的对象。也被称为"切面对象"。由于Spring AOP是使用运行时代理实现的,因此该对象始终是代理对象。
  • AOP代理(AOP proxy): 由AOP框架创建的对象,在Spring框架中,AOP代理对象有两种:JDK动态代理和CGLIB代理
  • 织入(Weaving): 是指把增强应用到目标对象来创建新的代理对象的过程,它(例如 AspectJ 编译器)可以在编译时期,加载时期或者运行时期完成。与其他纯Java AOP框架一样,Spring AOP在运行时进行织入。

Spring AOP 中通知的分类

  • 前置通知(Before Advice): 在目标方法被调用前调用通知功能;相关的类org.springframework.aop.MethodBeforeAdvice
  • 后置通知(After Advice): 在目标方法被调用之后调用通知功能;相关的类org.springframework.aop.AfterReturningAdvice
  • 返回通知(After-returning): 在目标方法成功执行之后调用通知功能;
  • 异常通知(After-throwing): 在目标方法抛出异常之后调用通知功能;相关的类org.springframework.aop.ThrowsAdvice
  • 环绕通知(Around): 把整个目标方法包裹起来,在被调用前和调用之后分别调用通知功能相关的类org.aopalliance.intercept.MethodInterceptor

Spring AOP 中织入的三种时期

  • 编译期: 切面在目标类编译时被织入,这种方式需要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的。
  • 类加载期: 切面在目标类加载到 JVM 时被织入,这种方式需要特殊的类加载器( ClassLoader ),它可以在目标类引入应用之前增强目标类的字节码。
  • 运行期: 切面在应用运行的某个时期被织入。一般情况下,在织入切面时,AOP容器会为目标对象动态创建一个代理对象,Spring AOP 采用的就是这种织入方式。

Spring Boot AOP 注意事项

在使用 Spring Boot AOP 时,需要注意以下事项:

  • 切面的顺序: 如果有多个切面,确保它们的执行顺序正确。你可以使用@Order注解来指定切面的执行顺序。
  • 连接点表达式的正确性: 确保连接点表达式(Pointcut Expression)正确匹配到你想要拦截的方法。不正确的表达式可能导致通知不被触发。
  • 性能开销: AOP 会带来一定的性能开销,特别是使用@Around通知时。因此,在使用 AOP 时要注意性能影响。
  • 版本兼容性: 确保 Spring Boot 版本与 Spring AOP 版本兼容。不同的版本可能会有不同的行为和功能。

Spring Boot AOP 实践案例

下面是一个使用 Spring Boot AOP 的简单实践案例,我们将使用 AOP 来记录方法的执行时间。

步骤 1: 创建一个 Spring Boot 项目

首先,创建一个新的 Spring Boot 项目。你可以使用 Spring Initializer(start.spring.io/)来方便地生成一个新的 Spring Boot 项目。

步骤 2: 添加依赖

确保在项目的pom.xml文件中添加 Spring AOP 依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

步骤 3: 创建 AOP 切面类

创建一个 AOP 切面类,用于记录方法的执行时间。这个切面将包含一个通知方法,在方法调用之前和之后记录时间。

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Aspect
@Component
public class MethodExecutionTimeAspect {

    private static final Logger logger = LoggerFactory.getLogger(MethodExecutionTimeAspect.class);

    private long startTime;

    @Before("execution(* com.example.myapp.service.*.*(..))")
    public void beforeServiceMethodExecution() {
        startTime = System.currentTimeMillis();
    }

    @After("execution(* com.example.myapp.service.*.*(..))")
    public void afterServiceMethodExecution() {
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;
        logger.info("Method execution time: {} ms", executionTime);
    }
}

在上述代码中,我们创建了一个名为MethodExecutionTimeAspect的切面类,并使用@Before@After注解来分别定义了在方法调用前和调用后执行的通知方法。通知方法记录了方法执行的开始时间和结束时间,并计算出执行时间。

步骤 4: 创建一个服务类

创建一个简单的服务类,该服务类包含一个需要被记录执行时间的方法。

import org.springframework.stereotype.Service;

@Service
public class MyService {

    public void doSomething() {
        // 模拟一个耗时操作
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

步骤 5: 启动 Spring Boot

应用程序创建一个 Spring Boot 启动类,启动整个应用程序。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

步骤 6: 运行应用程序

现在,你可以运行应用程序。当调用MyServicedoSomething方法时,AOP 切面会拦截该方法的执行并记录执行时间。你可以在控制台中看到类似以下的输出:

Method execution time: 1001 ms

AOP 的两种实现方式

AOP 采用了两种实现方式:静态织入(AspectJ 实现)和动态代理(Spring AOP实现)

AspectJ

AspectJ 是一个采用Java 实现的AOP框架,它能够对代码进行编译(一般在编译期进行),让代码具有AspectJ 的 AOP 功能,AspectJ 是目前实现 AOP 框架中最成熟,功能最丰富的语言。ApectJ 主要采用的是编译期静态织入的方式。在这个期间使用 AspectJ 的 acj 编译器(类似 javac)把 aspect 类编译成 class 字节码后,在 java 目标类编译时织入,即先编译 aspect 类再编译目标类。

image.png

Spring AOP 实现

Spring AOP 是通过动态代理技术实现的,而动态代理是基于反射设计的。Spring AOP 采用了两种混合的实现方式:JDK 动态代理和 CGLib 动态代理,分别来理解一下

image.png

  1. 基于 JDK 动态代理
    JDK 动态代理是 Java 原生支持的一种代理机制,适用于为实现了接口的目标类创建代理对象。
  • 特点
    • 需要目标类实现接口。
    • 基于 java.lang.reflect.Proxy 和 InvocationHandler 实现。
    • 代理类是目标类的代理,所有方法调用都会被拦截。

(1) 创建业务接口和实现类

public interface UserService {
    void addUser(String name);
    void deleteUser(String name);
}

public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String name) {
        System.out.println("添加用户:" + name);
    }

    @Override
    public void deleteUser(String name) {
        System.out.println("删除用户:" + name);
    }
}

(2) 创建动态代理类

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class JdkDynamicProxy implements InvocationHandler {
    // 目标对象
    private Object target;

    // 构造方法:传入目标对象
    public JdkDynamicProxy(Object target) {
        this.target = target;
    }

    // 动态代理方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("开始执行方法:" + method.getName());
        Object result = method.invoke(target, args); // 调用目标方法
        System.out.println("结束执行方法:" + method.getName());
        return result;
    }

    // 获取代理对象实例
    public Object getProxy() {
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(), 
            target.getClass().getInterfaces(), 
            this
        );
    }
}

(3) 测试动态代理

public class JdkDynamicProxyTest {
    public static void main(String[] args) {
        // 创建目标对象
        UserService userService = new UserServiceImpl();

        // 创建代理对象
        JdkDynamicProxy proxy = new JdkDynamicProxy(userService);
        UserService userServiceProxy = (UserService) proxy.getProxy();

        // 调用方法
        userServiceProxy.addUser("张三");
        userServiceProxy.deleteUser("李四");
    }
}

运行结果

开始执行方法:addUser
添加用户:张三
结束执行方法:addUser
开始执行方法:deleteUser
删除用户:李四
结束执行方法:deleteUser

2. 基于 CGLIB 动态代理
CGLIB(Code Generation Library)是一种动态字节码生成技术,适用于为目标类创建代理对象,即便目标类没有实现接口。

  • 特点
    • 不要求目标类实现接口。
    • 使用第三方库(CGLIB)生成目标类的子类,代理类是目标类的子类。
    • CGLIB 不能代理 final 方法,因为它是通过继承实现的。

(1) 添加依赖(Maven 项目)

pom.xml 文件中添加以下依赖:

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

(2) 创建业务类

public class ProductService {
    public void addProduct(String name) {
        System.out.println("添加商品:" + name);
    }

    public void deleteProduct(String name) {
        System.out.println("删除商品:" + name);
    }
}

(3) 创建 CGLIB 动态代理类

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CglibDynamicProxy implements MethodInterceptor {
    // 目标对象
    private Object target;

    // 构造方法:传入目标对象
    public CglibDynamicProxy(Object target) {
        this.target = target;
    }

    // 获取代理对象实例
    public Object getProxy() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass()); // 设置目标类
        enhancer.setCallback(this);               // 设置回调方法
        return enhancer.create();                 // 创建代理对象
    }

    // 拦截方法
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("开始执行方法:" + method.getName());
        Object result = proxy.invokeSuper(obj, args); // 调用目标方法
        System.out.println("结束执行方法:" + method.getName());
        return result;
    }
}

(4) 测试 CGLIB 动态代理

public class CglibDynamicProxyTest {
    public static void main(String[] args) {
        // 创建目标对象
        ProductService productService = new ProductService();

        // 创建代理对象
        CglibDynamicProxy proxy = new CglibDynamicProxy(productService);
        ProductService productServiceProxy = (ProductService) proxy.getProxy();

        // 调用方法
        productServiceProxy.addProduct("iPhone");
        productServiceProxy.deleteProduct("Samsung Galaxy");
    }
}

运行结果

开始执行方法:addProduct
添加商品:iPhone
结束执行方法:addProduct
开始执行方法:deleteProduct
删除商品:Samsung Galaxy
结束执行方法:deleteProduct

JDK 动态代理 vs CGLIB 动态代理

特点JDK 动态代理CGLIB 动态代理
依赖Java 原生需要 CGLIB 库
是否需要接口需要目标类实现接口不需要目标类实现接口
实现方式基于接口的代理基于继承的代理
性能效率较高,适合实现接口的类效率稍低,适合没有接口的类
是否支持 final 方法支持不支持
使用场景目标类实现了接口目标类没有实现接口
  • JDK 动态代理 适用于目标类实现了接口的情况,代码简单且性能较高。
  • CGLIB 动态代理 适用于目标类没有实现接口的情况,但不能代理 final 方法。

在实际开发中,如果使用 Spring AOP,Spring 会在底层自动选择 JDK 动态代理或 CGLIB 动态代理:

  • 如果目标类实现了接口,默认使用 JDK 动态代理
  • 如果目标类没有实现接口,Spring 则会切换到 CGLIB 动态代理

Spring 对 AOP的支持

在 Spring 中,AOP(面向切面编程)提供了两种主要的实现方式:


1. 基于注解式配置

特点:

  • 使用注解来定义切面及通知,配置简单直观。
  • 更加贴近现代开发风格,代码逻辑清晰。
  • 推荐用于新项目开发。

实现步骤:

根据@AspectJ切面配置Spring AOP,并配置自动代理。自动代理意味着,Spring 会根据自动代理为 Bean 生成代理来拦截方法的调用,并确保根据需要执行拦截。

  1. 引入依赖:确保项目中包含 Spring AOP 的相关依赖。
  2. 开启 AOP 支持:在配置类上添加 @EnableAspectJAutoProxy 注解。
  3. 定义切面类
    • 使用 @Aspect 注解标识切面类。
    • 使用 @Before@After@Around 等注解定义通知方法。
  4. 业务类正常编写,无需修改

代码示例:### 开启@AspectJ 支持

使用@Configuration 支持@AspectJ 的时候,需要添加 @EnableAspectJAutoProxy 注解,就像下面例子展示的这样来开启 AOP代理

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
}

启用了@AspectJ支持的情况下,在应用程序上下文中定义的任何bean都具有@AspectJ方面的类(具有@Aspect注释),Spring会自动检测并用于配置Spring AOP

@Aspect
@Component
public class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Before executing: " + joinPoint.getSignature().getName());
    }
}

一个切点由两部分组成:包含名称和任何参数以及切入点表达式的签名,该表达式能够确定我们想要执行的方法。在@AspectJ注释风格的AOP中,切入点表达式需要用@Pointcut注解标注(这个表达式作为方法的签名,它的返回值必须是 void)。

@Service
public class UserService {
    public void addUser(String name) {
        System.out.println("Adding user: " + name);
    }
}
AspectJ 描述符描述
arg()限制连接点匹配参数为指定类型的执行方法
@args()限制连接点匹配参数由指定注解标注的执行方法
execution()用于匹配是连接点的执行方法
this()限制连接点匹配的 AOP 代理的 bean 引用为指定类型的类
target限制连接点匹配目标对象为指定类型的类
@target()限制连接点匹配特定的执行对象,这些对象对应的类要具有指定类型的注解
within()限制连接点匹配指定的类型
@within()限制连接点匹配指定注解所标注的类型
@annotation限定匹配带有指定注解的连接点

运行结果:

Before executing: addUser
Adding user: John

2. 基于 XML 配置

特点:

  • 通过 XML 文件定义切面、切入点和通知。
  • 更适合老项目或对注解支持不足的框架。
  • 配置稍显繁琐,但逻辑清晰。

实现步骤:

  1. 引入依赖:确保项目中包含 Spring AOP 的相关依赖。
  2. 开启 AOP 配置:在 XML 配置文件中添加 <aop:aspectj-autoproxy/>
  3. 定义切面类
    • 创建切面类,通知方法无需注解。
    • 在 XML 中通过 <aop:aspect> 配置切面及其通知。
  4. 业务类正常编写,无需修改

XML 配置示例:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 开启 AOP 支持 -->
    <aop:aspectj-autoproxy/>

    <!-- 定义切面类 -->
    <bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>

    <!-- 定义切面和通知 -->
    <aop:config>
        <aop:aspect ref="loggingAspect">
            <aop:pointcut id="userServiceMethods" expression="execution(* com.example.service.*.*(..))"/>
            <aop:before method="logBefore" pointcut-ref="userServiceMethods"/>
        </aop:aspect>
    </aop:config>
</beans>

通过自动扫描检测切面

你可以在Spring XML 配置中将切面类注册为常规的bean,或者通过类路径扫描自动检测它们 - 与任何其他Spring管理的bean相同。然而,只是注解了@Aspect 的类不会被当作bean 进行管理,你还需要在类上面添加 @Component 注解,把它当作一个组件交给 Spring 管理。

切面类示例:

public class LoggingAspect {
    public void logBefore() {
        System.out.println("Before executing method...");
    }
}

运行结果:

Before executing method...
Adding user: John

两种实现方式的对比

对比维度基于注解式配置基于 XML 配置
配置方式使用注解,代码与配置结合紧密使用 XML 文件,配置与代码分离
复杂性简单直观,开发效率高配置较多,开发效率稍低
灵活性灵活,适合现代开发模式灵活,适合老项目或遗留系统
维护性更易维护,代码逻辑清晰配置文件独立,便于集中管理
推荐使用场景新项目或注解支持良好的环境老项目或需要动态修改配置的场景

总结

  • 基于注解式配置 更加简洁高效,适合现代开发,是 Spring 官方推荐的 AOP 方式。
  • 基于 XML 配置 适合对注解支持不足的老项目,或者需要集中管理配置的场景。