Spring AOP
文章目录
1. 简介
代理模式:属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。代理方式可以解决附加功能代码干扰核心代码和不方便统一维护的问题。
- 静态代理:定义代理类(一般会实现被代理类接口)及代理方法,类中组合被代理类对象并在代理类方法中调用被代理对象方法。使用时调用代理对象。
- 动态代理:动态生成代理对象,无需针对不同业务定义不同代理类,不写死代码
- JDK动态代理:代理的目标类必须实现接口,基于接口创建被代理对象
- cglib:通过继承被代理的目标类实现代理,所以无需实现接口
AOP(Aspect Oriented Programming):面向切面编程,是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。AOP技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。使用AOP,可以在不修改原来代码的基础上添加新功能。
AOP主要应用场景:
- 日志记录
- 事务处理
- 安全控制
- 性能监控
- 异常处理
- 缓存控制
- 动态代理
AOP术语名词
-
横切关注点:从每个方法中抽取出来的同一类非核心业务。
AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
-
通知(增强)方法:每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法
-
连接点 joinpoint:指那些被拦截到的点。在 Spring 中,指可以被动态代理拦截目标类的方法
-
切入点 pointcut:定位连接点的方式,或者可以理解成被选中的连接点。是一个表达式,比如
execution(* com.spring.service.impl.*.*(..))
。符合条件的每个方法都是一个具体的连接点。 -
切面 aspect:切入点和通知的结合。是一个类。
-
目标 target:被代理的目标对象。
-
代理 proxy:向目标对象应用通知之后创建的代理对象。
-
织入 weave:指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。
关系梳理:
- AOP一种区别于OOP的编程思维,用来完善和解决OOP的非核心代码冗余和不方便统一维护问题
- 代理技术(动态代理|静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐
- Spring AOP框架,基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架。SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现
底层技术组成:
- AspectJ:早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解
- 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口
- cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口
2. 基本使用
- 加入依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.6</version>
</dependency>
- 开启aspectj注解支持
- xml 方式:配置
<aop:aspectj-autoproxy />
- 配置类方式:配置
@EnableAspectJAutoProxy
注解
- xml 方式:配置
- 声明切面类,切入点,通知及通知方法
@Aspect // 表示是切面类
@Component
public class MyAdvice {
@Pointcut("execution(public void com.aop.controller.HelloController.hello())") // 切入点
public void pointcut() {}
@Before("pointcut()") // 也可使用 value = 切入点表达式 的方式
public void before(JoinPoint joinPoint) {
System.out.println("前置通知触发");
}
@AfterReturning("pointcut()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("后置通知触发");
}
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) {
Object result = null;
try {
System.out.println("环绕前置");
result = proceedingJoinPoint.proceed();
System.out.println("环绕后置");
} catch (Throwable t) {
System.out.println("环绕异常");
throw new RuntimeException(t);
}
return result;
}
}
3. 通知及获取通知细节信息
3.1 通知类型
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知(增强)方法。
- 前置通知
@Before
:在被代理的目标方法前执行 - 返回通知
@AfterReturning
:在被代理的目标方法成功结束后执行,异常终止不执行 - 异常通知
@AfterThrowing
:在被代理的目标方法异常结束后执行,异常终止才执行 - 后置通知
@After
:在被代理的目标方法最终结束后执行,无论是否异常 - 环绕通知
@Around
:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
3.2 获取通知细节信息
-
JoinPoint 接口 :需要获取方法签名、传入的实参等信息时,可以在通知方法声明 JoinPoint 类型的形参。
getSignature()
:获取方法签名对象,包含一个方法的全部声明信息,调用该对象getName()
方法即可获取方法名称getArgs()
:获取参数列表
环绕通知特别点:使用
ProceedingJoinPoint
接口作为形参,调用process()
方法表示执行被代理方法 -
方法返回值 :在
@AfterReturning
注解的方法中,获取目标方法的返回值returning
:通过注解的该属性,指定返回值在方法形参中的名称- 方法形参 :将上述名称添加到方法参数中,名字需与上面指定的对应,类型可用
Object
-
异常对象值 :在
@AfterThrowing
注解的方法中,获取目标方法的异常对象值throwing
:通过注解的该属性,指定异常值在方法形参中的名称- 方法形参 :将上述名称添加到方法参数中,名字需与上面指定的对应,类型可用
Throwable
4. 切入点表达式语法
AOP切点表达式(Pointcut Expression)是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。
以下按顺序指明位置对应功能
execution(表达式)
固定开头- 方法访问修饰符
- 方法返回值
- 特殊情况:当 2、3 都不需要考虑时,用
*
开头即可
- 特殊情况:当 2、3 都不需要考虑时,用
- 指定包的地址
- 固定包:直接写
com.aop.controller
- 单层:
com.aop.*
- 任意层:
com.aop..
含当前层 - 任何包:
*..
- 固定包:直接写
- 指定类名称
- 固定名称:
HelloController
- 任意类:
*
- 模糊匹配:
*Controller
- 任意包任意类:
*..*
- 固定名称:
- 指定方法名称:语法与类名称一致
- 方法参数
- 具体值:有参
(String,int)
,无参()
- 任意值:
(..)
任意参数,可有可无 - 部分具体:
(..String..)
有一个参数是 String 类型
- 具体值:有参
5. 切点表达式的提取(重用)
在第 2 节中即用了切点表达式的提取,使用 @Pointcut(切点表达式)
需要添加到一个无参数无返回值方法上即可,引用时写 全限定类名.方法名()
即可,相同类间可省略全限定类名。
@Pointcut("execution(public void com.aop.controller.HelloController.hello())") // 切入点
public void pointcut() {}
@Before("pointcut()") // 也可使用 value = 切入点表达式 的方式
public void before(JoinPoint joinPoint) {
System.out.println("前置通知触发");
}
【建议】:将切点表达式统一存储到一个类中进行集中管理和维护
6. 切面优先级设置
使用 @Order
注解,值越小,前置通知越先执行,后置通知越晚执行,默认值 Integer.MAX_VALUE
。
就像在前置通知的最后放了一面镜子表示被代理类的执行,先从队列最前面的前置通知执行,直到镜子执行完。镜子中镜像对应着后置通知顺序,在前置通知中顺序最靠前的将最后执行。
实际意义:当有多个切面嵌套执行时,要慎重考虑切面的执行顺序。
7. Spring AOP对获取Bean的影响
情景1:
- 声明一个接口
- 接口有一个实现类
- 创建一个切面类,对上面接口的实现类应用通知
测试:根据实现类类型获取bean,无法获取,且输出如下;如改成根据接口获取,将可以获取到,且获取到的对象是 jdk动态代理对象
Bean named 'helloController' is expected to be of type 'com.aop.controller.HelloController' but was actually of type 'jdk.proxy2.$Proxy34'
解释:应用了切面后,真正放在IOC容器中的是代理类的对象,目标类并没有放到 IoC容器中
情景2:
- 声明一个类
- 创建一个切面类,对上面接口的实现类应用通知
测试:根据实现类类型直接获取bean,可以获取到,获取到 cglib 代理对象,
class org.xzx.controller.HelloController$$SpringCGLIB$$0
解释:因为 cglib 代理实现基于继承,故可以满足 instance of HelloController
条件