Spring中的AOP

AOP 是 Aspect Oriented Programming(面向切面编程)的首字母缩写,是一种编程范式,它的目的是通过分离横切关注点(cross-cutting concerns)来提升代码的模块化程度。

AOP中提到的关注点,其实就是一段特定的功能,有些关注点出现在多个模块中,就称为横切关注点

AOP解决了两个问题:

  • 代码混乱:核心的业务逻辑代码还必须兼顾其他功能,这就导致不同功能的代码交织在一起,可读性很差
  • 代码分散:同一个功能的代码分散在多个模块中,不易维护。

AOP中几个重要概念

概念 说明
切面(aspect) 按关注点进行模块分解时,横切关注点就表示为一个切面
连接点(join point) 程序执行的某一刻,在这个点上可以添加额外的动作
通知(advice) 切面在特定连接点上执行的动作
切入点(pointcut) 切入点是用来描述连接点的,它决定了当前代码与连接点是否匹配

aop的具体流程:通过切入点来匹配程序中的特定连接点,在这些连接点上执行通知,这种通知可以是在连接点前后执行,也可以是将连接点包围起来

Spring AOP的实现原理

在Spring中,AOP背后的实现原理是基于动态代理技术(Dynamic Proxy),代理模式是GoF提出的23种经典设计模式之一,我们可以对某个对象提供一个代理,控制对该对象的访问,代理可以在两个有调用关系的对象之间起到中介作用——代理封装了目标对象,调用者调用了代理的方法,代理再去调用实际的目标对象
Spring——AOP-小白菜博客
动态代理就是在运行时动态地为对象创建代理的技术,在Spring中,由AOP框架创建、用来实现切面的对象被称为AOP代理(AOP Proxy),一般采用JDK动态代理或者是CGLIB代理;二者区别如下:

必须实现接口 支持拦截public方法 支持拦截protected方法 拦截默认作用域方法
JDK动态代理
CGLIB代理

Spring容器在为Bean注入依赖时,会自动将被依赖Bean的AOP代理注入进来,这就让我们感觉是在使用原始的Bean,实则不然。

被切面拦截的对象称为目标对象(Target Object)或通知对象(Advised Object),因为Spring用了动态代理,所以目标对象就是要被代理的对象。

以JDK代理为例,假设我们想在say()方法前后增加两条日志,可以采用下面的代码

点击查看代码
/**
 * TODO 要被动态代理的Hello接口及其实现片段
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/3/12 10:45
 */
interface Hello {
    void say() throws NoSuchMethodException;
}

public class SpringHello implements Hello {
    @Override
    public void say() throws NoSuchMethodException {
        System.out.println(this.getClass().getName() + ": " + this.getClass().getMethod("say") + "Hello SpringAOP!");
    }
}

随后,我们再实现一个InvocationHandler,于是对代理对象的调用都会转为调用invoke方法,传入的参数中就会包含了所调用的方法和实际的参数

点击查看代码
/**
 * TODO 在Hello.say()前后打印日志的InvocationHandler
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/3/12 10:47
 */
public class LogHandler implements InvocationHandler {
    private Hello source;

    public LogHandler(Hello source) {
        this.source = source;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Ready to say something");
        try{
            return method.invoke(source,args);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("Already say something");
        }
        return method.invoke(source,args);
    }
}

最后,再通过Proxy.newProxyInstance()为Hello实现类的Bean实例创建使用LogHandler的代理

点击查看代码
/**
 * TODO 使用Proxy.newProxyInstance()获取代理的目标对象
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/3/12 10:50
 */
public class AopApplication {
    public static void main(String[] args) {
        SpringHello original = new SpringHello();
        Hello target =(Hello) Proxy.newProxyInstance(Hello.class.getClassLoader(),original.getClass().getInterfaces(),new LogHandler(original));
        try {
            target.say();
        } catch (NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }
}

运行结果如下:

Spring——AOP-小白菜博客
Spring AOP的实现方式:

  • JDK动态代理:AopProxyFactory会创建JdkDynamicAopProxy
  • CGLIB代理:创建ObjenesisCglibApoProxy

基于@AspectJ的配置

Spring Framework同时支持@AspectJ注解和XML Schema两种方式来使用AOP。

基于注解的方式

首先,需要引入org.springframework:spring-aspects依赖,以便使用AspectJ相关的注解和功能。

 	<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>6.0.3</version>
	</dependency>

要开启@AspectJ支持,可以在Java配置类上增加@EnableAspectJAutoProxy注解

/**
 * TODO 基于注解的方式
 *
 * @author ss_419
 * @version 1.0
 * @date 2023/3/12 12:10
 */
@Configuration
@EnableAspectJAutoProxy
public class Config {
}

@EnableAspectJAutoProxy有两个属性,两者默认值都是false

  • proxyTargetClass用于选择是否开启基于类的代理(是否使用CGLIB来做代理)
  • exposeProxy用于选择是否将代理对象暴露到AopContext中
<?xml version="1.0" encoding="UTF-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/beans/spring-aop.xsd">
    <aop:aspectj-autoproxy/>
</beans>

注解方式的切入点声明由两部分组成——切入点表达式和切入点方法签名。
前者用来描述要匹配的连接点,后者可以用来引入切入点,方便切入点的复用。

@Pointcut注解中使用的就是AspectJ5的表达式,其中一些常用的PCD(pointcut designator,切入点标识符)

PCD 说明
execution 最常用的一个PCD,用来匹配特定方法的执行
within 匹配特定范围内的类型,可以用通配符来匹配某个Java包内的所有类
this Spring AOP代理对象这个Bean本身要匹配某个给定的类型
target 目标对象要匹配某个给定的类型,比this更常用一些
args 传入的方法参数要匹配给定的类型,它也可以用于绑定请求参数
bean Spring AOP 特有的一个PCD,匹配Bean的ID或名称,可以用通配符

execution常见表达式:
每个部分都可以使用*通配符

类名中使用.表示包中的所有类,..表示当前包与子包中的所有类

参数情况:

  • ()表示方法无参数
  • (..)表示有任意个参数
  • (*) 表示有一个任意类型的参数
  • (String表示有一个String类型参数
  • (String,String)代表有两个String类型的参数

前置通知:@Before()
后置通知:@AfterReturning()
环绕通知:@Around()