AOP

1.6K
0
0
最后修改于

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,切入点指示符,如 executionwithin 等。
  • modifiers-pattern,可选。指示目标方法的修饰符,如public
  • ret-type-pattern,返回类型,指示目标方法的范围类型,如 *
  • declaring-type-pattern,可选。指示目标方法全限定类型名
  • name-pattern,方法名称,指示要匹配的方法名称,如*
  • param-pattern,可选。指示参数的匹配,由() 包裹,如
Spring AOP / AspectJ#
  • Spring AOP 与 AspectJ 都是一种提供 AOP 功能的技术,其实现方式略有不同。Spring AOP 使用动态代理技术,不会做编译时修改,主要是运行时生成动态代理类。而 AspectJ 主要是基于编译修时的,因此功能会更强大。
  • Spring AOP 只能对 Bean 中的函数进行 AOP 代理,而 AspectJ 则可以对构造函数、字段等均可设置 AOP 代理。
  • Spring AOP 性能因为其动态代理的方式相比于 AspectJ 性能较弱,但是在一般的企业应用开发中,是足以胜任的。
  • 切入方式上,Spring AOP 会基于目标对象生成动态代理类,而 AspectJ 则是编译时直接将代码植入目标类
AOP