AOP 本身与 Spring IOC 容器并不直接相关,是 Spring 中相对独立的模块。
只是通过 BeanPostProcessor 作为工具放入 IOC 容器。在需要的时候可以对 Spring Bean 进行代理增强。
因此本篇内容并不会去赘述 AOP 是如何与 IoC 结合的。
什么是 AOP#
Aspect Orientation Programing,即面向切面编程,是一种编程范式 / 思想,将部分通用逻辑进行提取,能方便的织入业务逻辑中,但又不会污染业务逻辑的内容,很好的做到了关注点分离。
从实践上来看,AOP 的核心思想就是 Proxy Pattern,通过代理模式对原有对象进行增强。在方法执行的各个阶段提供 hook,注入一些通用的功能,如日志、审计、事务等。
为什么要用 AOP#
如下,这是在没有 AOP 的情况下,对于一些通用且必须的代码,在每个子逻辑中都需要重复调用,且与业务代码耦合。
![[attachments/5. AOP 2024-03-14 14.45.17.excalidraw]]
但从不同功能的颜色块即可发现,通过 AOP 可以实现业务逻辑和通用逻辑的解耦,在代码规模较大时有极为明显的好处。
AOP 的几个要素#
假设要实现一个通用的日志服务,在特定的方法调用的各个时机,使用日志进行记录。
那么理想状态下,日志相关的逻辑都应该放在日志类中,只需简单的声明即可在业务逻辑执行的各个时机执行特定逻辑,在 AOP 中,这样的日志类这称为 Aspect 切面。
日志类中可能有多个不同的待接入的逻辑方法。Spring AOP 将其称为 Advice。(中文常见到翻译为通知,无论从哪个方面来理解,都是十分低能翻译)。
将日志类的逻辑附加到某个 Service 上,对于 Spring AOP 而言,可以对该 Service 中某个方法的执行进行 Hook,这样的方法称为 JoinPoint 连接点。而这个被修改的 Service 被称为 TargetObject 目标对象。
显然,Advice 与 JoinPoint 实际上是多对多的关系。因此中间需要一个中介表示,将哪些 Advice 应用到哪些 JoinPoint 上。这个中介表示,也即谓词,称为 PointCut 切入点。实际上是一种表达式,这个表达式的功能就是,对目标进行筛选。
Advice 接入 JoinPoint 到方法的时机,有以下几种:
- After
- Before
- Around
- AfterReturning
- AfterThrowing
我们可以理解为:
target <-> pointcut expression <-> advice
do something common
after/before/...
some business
AOP 的 API#
根据上面的描述,我可以设计出这样的 API
// 提取通用逻辑
interface Advice {
fun before(target: Any, method: Method, args: Array<out Any?>)
fun after(target: Any, method: Method, args: Array<out Any?>)
//... 各种其他需要注入的通用逻辑
}
// 筛选器,
interface Pointcut {
//里面可以有各种筛选器,如类筛选器, 方法筛选器
// val classFilter: ClassFilter
// 实践上可以用特定表达式来表示上面的各种筛选器
// val expression: PointCutExpression
// 这里仅用 match 方法进行对外暴露
fun match(clazz:Class<*>, method:Method):Boolean
}
// 进一步,pointcut 和 advice 可以一起成对出现
interface PointcutAdvisor {
val pointCut: Pointcut
val advice: Advice
}
// 所有的通用逻辑,每个通用逻辑都带有自己的筛选器
val advisors: List<PointcutAdvisor>
// 被代理的目标对象
interface Proxy(val target: Any) {
// 在被代理对象执行调用时
fun invoke(target:Any, method:Method):Any? {
//筛选对目标应用的通用逻辑
val rest = advisors.filter{ it.pointCut.match(target.class, method) }
// 应用余下的符合要求的逻辑
return applyRestAdviceDuringInvokeMethod(method, rest)
}
}
这个 API 设计相比 Spring 而言简化了一些东西,但是核心的东西是类似的。
AOP 的使用#
![[attachments/5. AOP 2024-03-14 16.19.35.excalidraw]]
回到 Spring,从上文图不难看出,使用 AOP 只需将通用逻辑作为切面实现,随后通过一种方式将业务逻辑或通用逻辑组织在一起即可,这样的行为就可称为编织 / 织入。
从使用上来说,类似于 Bean 声明,编织 AOP 的方式有两个,一是通过 XML 的形式,另外一种则是注解的方式。
XML#
这种方式与上图类似,在第三个文件中进行组织,将切面与业务逻辑进行组合。
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="logAspect">
<!-- 配置切入点 -->
<aop:pointcut
id="pointCutMethod"
expression="execution(* io.ktlab.example.*.*(..))"
/>
<!-- 前置通知 -->
<aop:before
method="logBefore"
expression="execution(* io.ktlab.example.*.*(..))"
/>
<!-- 后置通知 -->
<aop:after
method="logAfter"
pointcut-ref="pointCutMethod"
/>
</aop:aspect>
</aop:config>
注解#
通常这种方式将 Aspect、PointCut、Advice 打包放入同一个类中,集中处理通用逻辑并指示要切入的位置和时机。
这种方式下,PointCut 和 Aspect 都可以在 Aspect 中声明。
@Component
@Aspect
class LogAspect {
// 切入点,选择要接入
@Pointcut("execution(* io.ktlab.example.*.*(..))")
fun pointCutMethod(){}
//
@Before("execution(* io.ktlab.example.*.*(..))")
fun logBefore(jp: JoinPoint) {
println("executing ${jp.signature.name}")
}
@After("execution(* io.ktlab.example.*.*(..))")
fun logAfter(jp: JoinPoint) {
println("${jp.signature.name} finished")
}
}
AOP PointCut 表达式#
通过特定的 PointCut 表达式,可以指出切面所应用到的范围。
designators-pattern(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?)
- designators-pattern,切入点指示符,如
execution、within等。 - modifiers-pattern,可选。指示目标方法的修饰符,如
public - ret-type-pattern,返回类型,指示目标方法的范围类型,如
* - declaring-type-pattern,可选。指示目标方法全限定类型名
- name-pattern,方法名称,指示要匹配的方法名称,如
* - param-pattern,可选。指示参数的匹配,由
()包裹,如()匹配无参数的方法(..)匹配任意参数的方法
具体的表达式规则可以参考 Spring 文档的 Declare a PointCut,以及 AspectJ 文档 The AspectJ Developer's Notebook
Spring AOP / AspectJ#
- Spring AOP 与 AspectJ 都是一种提供 AOP 功能的技术,其实现方式略有不同。Spring AOP 使用动态代理技术,不会做编译时修改,主要是运行时生成动态代理类。而 AspectJ 主要是基于编译修时的,因此功能会更强大。
- Spring AOP 只能对 Bean 中的函数进行 AOP 代理,而 AspectJ 则可以对构造函数、字段等均可设置 AOP 代理。
- Spring AOP 性能因为其动态代理的方式相比于 AspectJ 性能较弱,但是在一般的企业应用开发中,是足以胜任的。
- 切入方式上,Spring AOP 会基于目标对象生成动态代理类,而 AspectJ 则是编译时直接将代码植入目标类