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框架创建、用来实现切面的对象被称为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!");
}
}
点击查看代码
/**
* 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);
}
}
点击查看代码
/**
* 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的实现方式:
- 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()