万字详解Spring框架核心知识AOP

介绍

AOP - Aspect-oriented Programming,网上很多文章翻译为“面向方面编程”,总觉得不好理解,个人觉得翻译为“面向特性编程”更准确,因为在AOP中,Aspect可以是很多内容,如类型、方法等,无论是哪种,都是需要首先抽象出这类Aspect的特性,比如都位于哪个package、这些函数签名的共性部分等,然后面向这些具备相同特性的Aspect进行编程(增强)。

增强:对原有的业务逻辑进行修改,附加更多的逻辑处理,处理后的逻辑更完善,更强大

AOP 目前只支持对方法进行增强,不支持对字段进行增强

AOP 一般会和IoC配合使用,但是IoC并不依赖AOP

那么AOP到底是用来做什么的?用一个简单的例子介绍一下,比如有一个函数:

public void hiU(String name){ System.out.println(name+",come here."); } hiU("Betsy-VR"); // Betsy-VR,come here. 复制代码

作为礼仪之邦,这可不行,如何才能输出:Hi Betsy-VR,come here呢?

  • 直接修改函数源代码,添加Hi:如果这类函数很多或者函数是第三方闭源的,就行不通了

  • 直接定义新类,继承该类并重写该函数:这是OOP思路,如果这类函数很多就行不通了,也会导致类膨胀问题

  • 使用代理模式,编写代理类,内部转调该函数,和上面面临同样的问题,而且一旦该类要修改,代理类要同步修改

  • 使用AOP:这也是AOP两大主要作用之一,实际也是采用代理模式(默认使用JDK动态代理,是针对接口的代理,也可以配置为CGLIB),但是AOP自动生成代理类,解决了上述缺点,而且AOP是非侵入式的

AOP主要用来做什么?

  • Provide declarative enterprise services(提供声明式的企业服务,比如Spring中的事务处理@Transaction).

  • Let users implement custom aspects, complementing their use of OOP with AOP(使用户可以自定义增强逻辑,补充OOP的使用,比如上面的小栗子,就是这一条).

AOP的使用流程

寻找要增强的函数的特性 / join point

既然要添加增强逻辑,首先要知道对哪些函数进行增强,比如上面小栗子中的hiU,找出这些函数的共性,比如这些函数都位于org.betsy.common下,又或者它们签名中修饰符都是public void 等等

定义一个可以描述这些特性的表达式 / pointcut

找到了,就要把它们定义出来,比如使用SpEL:execution(public * *(..)),具体含义的先跳过,这样Spring框架会根据这个定义去匹配函数,然后生成代理类,并注入(织入)你要增强的业务逻辑

编写要增强的业务逻辑 / Advice

找到位置了,也要告诉框架你想添加什么内容吧,比如上述小栗子中的Hi

织入 / Aware

让框架把这些内容组织起来,构建代理类,使其生效

大概就是这么一个过程,每一个过程都有一个具体的名字/术语,下面逐一介绍

AOP术语/概念

  • Aspect:方面/特性,采用模块化的方式,把跨多个类的关注点模块化

  • Join point:连接点,很不好理解,官方解释说连接点总是方法的执行,个人觉得可以理解成被增强的那些方法,如上述的hiU,在具体点,连接点实际指的是hiU方法执行前、执行后、执行前和执行后、返回后、异常后这几个阶段

  • Pointcut:切入点,也很不好理解,官方解释说切入点是匹配连接点的谓词,个人觉得可以简单理解成描述那些连接点的一个表达式,如上述的public * *(..)

  • Advice:在连接点上要执行的业务逻辑,就是你想添加的内容

其中 Aspect、PointCut很关键,开发时经常打交道

包括哪些通知类型

我们要增强的内容,可以添加到连接点的哪个阶段?

  • Before advice:在方法(连接点)执行之前添加业务增强逻辑,除非增强的逻辑中出现异常,否则不会阻止函数继续执行

  • After(finally) advice:在方法(连接点)执行之后添加业务增强逻辑,无论函数正常结束还是异常,该增强逻辑都会执行

  • After returning advice:在方法(连接点)正常执行结束后添加业务增强逻辑,异常不会执行

  • After throwing advice:在方法(连接点)发生异常后添加业务增强逻辑,正常执行不会执行

  • Around advice:最强大的一种,在方法(连接点)执行之前和执行之后添加业务增强逻辑,另外还可以配置是继续执行方法体,还是直接返回增强业务的返回值,或者直接返回异常

启用AOP支持

要使用AOP技术,首先要启动对它的支持,这就需要用到注解@EnableAspectJAutoProxy,源码:

@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Import({AspectJAutoProxyRegistrar.class}) public @interface EnableAspectJAutoProxy { boolean proxyTargetClass() default false; boolean exposeProxy() default false; } 复制代码

该注解不能被扫描,所以需要配合 @configuration 或 @component使用,如下:

@Configuration @EnableAspectJAutoProxy public class AppConfig { } 复制代码

在使用SpringBoot作为开发框架时,不需要手动启动AOP,当添加AOP依赖后,框架已经自动启动了。

  1. 添加AOP依赖

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> 复制代码
  1. 框架自动配置

​内部已经有了配置:

​在类org.springframework.boot.autoconfigure.aop.AopAutoConfiguration中有自动配置的逻辑。

定义一个Aspect

当启用对AOP支持后,Spring框架会自动扫描带有@Aspect注解的类,并配置AOP。

import org.aspectj.lang.annotation.Aspect; @Aspect public class NotVeryUsefulAspect { } 复制代码

被标记为Aspect的类和普通的Bean一样,可以定义属性、方法等。

该类主要用来把跨类的切入点进行整合并模块化。

定义切点/Pointcut

上文中介绍过,Pointcut可以理解为方法的调用,而且AOP目前只支持方法级的切点,不支持属性级。

一个Pointcut包含两部分:方法签名和表达式。

  • 方法签名:一般会定义一个带有void(必须)返回值的方法来表示

  • 表达式:使用@Pointcut注解描述

比如:

@Pointcut("execution(* transfer(..))") // 表达式部分 
private void anyOldTransfer() {} // 签名部分 复制代码

上例中,execution是什么?

Pointcut中的指示器

  • execution:(主要)用于匹配关注的方法,也就是匹配连接点(方法)

  • within:限制匹配特定类型中的连接点(方法)

  • this:限制匹配代理类实现指定类的连接点(方法)

  • target:限制匹配目标类实现指定类的连接点(方法)

  • args:限制运行时传递的参数是指定类型的连接点(方法)

  • @target:匹配的连接点(方法)必须是带有指定类型注解的类型中的方法

  • @args:限制与连接点的匹配,连接点的实际参数必须有给定类型的注释

  • @annotation:限制与连接点的匹配,连接点的主体具有给定的注释

迷迷糊糊之this和target

很多地方都翻译为被代理类和目标类,呃~

  • this:指向由AOP生成的代理类,或者叫AOP类

  • target:指向AOP代理的目标类,也就是业务类本身,如UserOperService

想搞清楚this和target区别,要首先搞清楚spring中有两种代理类:JDK代理和CGLIB代理

  • JDK代理:针对接口的代理,生成的代理类会自动继承Proxy类,JAVA又是单继承,所以只能是针对接口的

  • CGLIB代理:针对类的代理,生成的代理类不会自动添加父类Proxy,而是生成子类,直接继承目标类

通过配置 proxyTargetClass=false 启用JDK代理,否则是CGLIB代理,默认是false

综上,无论采用哪种代理,target都是目标类本身,但是this会根据代理方式不同,运行时判断指向谁:

  • JDK代理时,this指向接口和代理类

  • CGLIB代理时,this指向接口和子类(目标类的子类)

举个例子(伪代码):

  • 定义@Component class A#hiU{sout("A");}

  • 定义@Component class B#hiU{};,不重写A#hiU()

  • 定义Pointcut:@Before("this(com.example.demoaop.aop.A)")--> sout("this);

  • 定义Pointcut:@After("target(com.example.demoaop.aop.A)")--> sout("target);

当调用 A#hiU() 时,target和this都命中,毫无疑问

当调用 B#hiU() 时,target和this都命中,可见匹配时向下兼容

把3、4中的com.example.demoaop.aop.A修改为com.example.demoaop.aop.B,再次调用:

当调用 A#hiU() 时,target和this都没有命中,可见匹配时不向上兼容

当调用 B#hiU() 时,target命中,但是this没有命中

不常用之args

这个是动态分析的,开销也比其它的大,使用比较少,但还是简单介绍一下。

args和execution的区别,比如:execution(* *(org.betsy.entity.A))和args(org.betsy.entity.A)都是针对参数的,有什么区别?

  • execution:指定的参数类型和方法签名中的参数类型一致时,会被命中

  • args:指定的参数类型和方法签名没关系,和调用时传递的参数类型有关

但是args有一个常用的作用,就是把原始方法的参数传递给advice,在下文介绍Advice参数时再介绍。

组合使用

可以使用||、&&、!,或者引用另一个表达式的名称来组合使用。

// 匹配所有公共的方法 @Pointcut("execution(public * *(..))") private void anyPublicOperation() {} 
// 匹配所有指定模块下的方法 @Pointcut("within(com.xyz.myapp.trading..*)") private void inTrading() {} 
// 匹配所有指定模块下,公共的方法 @Pointcut("anyPublicOperation() && inTrading()") private void tradingOperation() {} 复制代码

如果要定义比较复杂的表达式,最佳实践是定义小的表达式,然后组合成复杂的

表达式格式

开发中,使用最多的就是@execution定义表达式,格式如下:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)

  • modifiers-pattern:(可选)修饰符,如public private等

  • ret-type-pattern:(必须)返回类型,通常使用*表示匹配所有返回类型,否则只匹配返回指定类型的方法

  • declaring-type-pattern:(可选)方法所在类型的完整限定类型名,如com.betsy.service,会和name-pattern组合使用,并使用.连接

  • name-pattern:(必须)方法名,通常使用表示方法名的全部或部分,如set表示以set开头的方法

  • param-pattern:(必须)参数,比较复杂,下文单独介绍

  • throws-pattern:(可选)异常

关于param-pattern,通常用法有:

  • ()表示无参数

  • (..)表示任何数量的任何类型的参数

  • (.)表示一个任何类型的参数

  • (*,String)表示两个参数,第一个是任意类型,第二个是String类型 这些是常用的,有兴趣的朋友可以研究下更多的用法:Language Semantics

来看几个官方的例子

// 匹配所有是public的方法 execution(public * *(..)) 
// 匹配所有以set开头的方法 execution(* set*(..)) 
// 匹配com.xyz.service.AccountService下的所有方法 execution(* com.xyz.service.AccountService.*(..)) 
// 匹配com.xyz.service包下的所有方法 execution(* com.xyz.service.*.*(..)) within(com.xyz.service.*) 
// 匹配com.xyz.service包及子包下的方法 execution(* com.xyz.service..*.*(..)) within(com.xyz.service..*) 
// 如果代理类实现了AccountService,则匹配 
// this必须使用完整限定名,不能使用通配符* this(com.xyz.service.AccountService) 
// 如果目标类实现了AccountService接口,则匹配 
// target必须使用完整限定名,不能使用通配符 target(com.xyz.service.AccountService) 
// 如果运行时传递了一个可序列化参数,则匹配 args(java.io.Serializable) 
// 如果目标类上添加@Transactional注解,则匹配 @target(org.springframework.transaction.annotation.Transactional) 
// 如果目标类实现了Transactional,则匹配 @within(org.springframework.transaction.annotation.Transactional) 
// 如果方法上添加了@Transactional注解,则匹配 @annotation(org.springframework.transaction.annotation.Transactional) 
// 如果方法只有一个参数,并且运行时传值是Classified类型,则匹配 @args(com.xyz.security.Classified) 
// 如果BEAN名称是tradeService,则匹配 
// 可以使用通配符 * ,如 *Service bean(tradeService) 复制代码

最佳实践

采用AOP就会有性能损耗,静态pointcut在编译时处理,但是动态的Pointcut要在运行时动态判断,对性能影响就会比较大,虽然官方说会采用最佳优化方式处理排序问题,但如何缩小匹配的最小范围,依然是首先要考虑的问题。

Pointcut的匹配搜索指示器(designators)有3种:

  • Kinded:特征指示器,定义具备相同特性的Ponintcut,如execution, get, set, call, 和 handler

  • Scoping:范围指示器,如within 和 withincode

  • Contextual:上下文指示器,如this, target, 和 @annotation

在定义Pointcut时,最好同时具备Kinded和Scoping指示器,如果只采用Kinded或Contextual指示器,则会影响性能(包括时间和内存)

Scoping的性能最快,所以,如果可能,最可能使用这种指示器最佳

定义Advice

Advice包括两部分,一是类型、二是内容:

  • 类型:可以看看上一篇中的介绍 # 包括哪些通知类型

  • 内容:也就是你想干什么,增强的业务逻辑

关联Pointcut的两种方式

在定义Advice时,有两种方式可以关联Pointcut:

  • 直接把Pointcut表达式写在Advice类型中,如@Before("execution(* com.betsy.dao..(..))")

  • 首先定义Pointcut,然后在Advice类型中引用Pointcut,如@Before("com.betsy.CommonPointcuts.dataAccessOperation()")

接下来尽可能使用官方的实例进行介绍!

| Before Advice

在被匹配的方法执行之前,先执行doAccessCheck方法。

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; @Aspect public class BeforeExample { 
@Before("execution(* com.xyz.myapp.dao.*.*(..))") public void doAccessCheck() { // ... } } 复制代码

| After Advice

在方法执行完成后,执行doReleaseLock方法,所谓执行完成包括正常返回和异常返回,所以也叫Finally Advice ,类似try-catch-finally,一般用于释放资源等操作。

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.After; @Aspect public class AfterFinallyExample { @After("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") public void doReleaseLock() { // ... } } 复制代码

| After returning Advice

在方式正常执行完成后,执行doAccessCheck方法,只有正常返回后执行,异常后不执行。

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; 
@Aspect public class AfterReturningExample { @AfterReturning("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") public void doAccessCheck() { // ... } } 复制代码

如果要在Advice中获取原方法(join-point)的返回值,可以使用returning:

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterReturning; 
@Aspect public class AfterReturningExample { 
@AfterReturning( pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()", returning="retVal") public void doAccessCheck(Object retVal) { // ... } } 复制代码

returning必须和doAccessCheck参数名相同,另外,还限定了只匹配那些返回类型是Object的方法,这点很重要!

| After Throwing Advice

在方法异常后,执行doRecoveryActions方法,正常返回时不执行。

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; 
@Aspect public class AfterThrowingExample { @AfterThrowing("com.xyz.myapp.CommonPointcuts.dataAccessOperation()") public void doRecoveryActions() { // ... } } 复制代码

如果要在Advice中接收异常信息,并限定只有发生指定异常时才会执行Advice,可以使用throwing属性:

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.AfterThrowing; 
@Aspect public class AfterThrowingExample { 
@AfterThrowing( pointcut="com.xyz.myapp.CommonPointcuts.dataAccessOperation()", throwing="ex") public void doRecoveryActions(DataAccessException ex) { // ... } } 复制代码

| Around Advice

这个比较复杂,主要用于在原始方法(join-point)调用之前和之后添加增强逻辑(Advice)。

前面的几类都是在方法调用前或调用后被动执行,Around Advice除了可以添加增强逻辑之外,还可以控制要不要执行原始方法。

import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.ProceedingJoinPoint; @Aspect public class AroundExample { 
// 第一个参数是ProceedingJoinPoint,必须地 @Around("com.xyz.myapp.CommonPointcuts.businessService()") public Object doBasicProfiling(ProceedingJoinPoint pjp) throws Throwable { 
// 在方法执行之前,要执行的逻辑 System.out.println("方法要执行..."); 
//这里会调用原始方法,如果不写这句话,原始方法将不会被执行 Object retVal = pjp.proceed(); 
// 在方法执行之后,要执行的逻辑 System.out.println("方法执行完了!!!"); return retVal; } } 复制代码

画圈考点1:关于ProceedingJoinPoint#proceed()

上述实例中,有一个Object retVal = pjp.proceed();表示调用原始方法,这个proceed()有两个重载:

  • proceed():无参数调用,表示调用时传递什么参数,就是什么参数,Advice不干预

  • proceed(Object[]):有参数调用,将替换掉调用时传递的参数,以Object[]中的参数为准

举个栗子:

//原方法 public void hiU(String name){ System.out.println(name); } 
//调用 hiU("hi Betsy"); 
//使用无参数proceed Object retVal = pjp.proceed();
// 输出:hi Betsy 
//使用有参数proceed Object retVal = pjp.proceed(new Object[]{"hi Betsy-VR"});
// 输出:hi Betsy-VR 复制代码

画圈考点2:Around Advice返回值

在上例中不难理解,真正对原始方法的调用,是在doBasicProfiling内调用proceed触发的,那么是不是返回、返回什么数据,当然也是在doBasicProfiling中控制的,最简单的理解,就是doBasicProfiling对原方法进行了一层包装(代理),所以doBasicProfiling的返回值就是在调用原方法时可以接收到的返回值。

就好像,就好像,你委托张三向李四要微信,李四把微信返回给张三,张三要不要告诉你呢?就是这个返回值控制的,如果Adround Advice定义返回值是void,那你就拿不到李四的微信啦...

关于参数

前面介绍了Advice的5个类型,在定义Advice时,方法都没有参数(除了Around Advice之外),实际上在上述5个类型定义时,方法的第一个参数都可以是org.aspectj.lang.JoinPoint(Around Advice的参数ProceedingJoinPoint是JoinPoint的子类)。

JoinPoint

提供了很多有用的方法:

  • etArgs:获取原始方法的参数

  • getThis:获取代理对象

  • getTarget:获取目标对象

  • getSignature:获取原方式的描述

  • toString:打印原方法的信息

当然,除了Around Advice必须使用第一个参数之外,其他类型都是可选的。

args

除此之外,还允许把原方法的参数传递到Advice中来,这里要使用args(上文有介绍用它限定参数类型的作用)。

@Before("com.xyz.myapp.CommonPointcuts.dataAccessOperation() && args(account,..)") public void validateAccount(Account account) { // ... } 复制代码

这样就可以让advice接受到调用原方法时传递的account参数,同时,这也限定了只会匹配参数是Account类型的方法才会被增强。

如果要使用JoinPoint,则必须是第一个参数,Account是第二个参数

annocation

首先定义注解,然后把注解添加到方法上,最后在Advice中获取注解中的配置:

@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.METHOD) public 
@interface Auditable { String value(); } 
@Auditable(value="Betsy-VR") public void hiU(){} @Before("com.xyz.lib.Pointcuts.anyPublicMethod() && 
@annotation(auditable)") public void audit(Auditable auditable) { String code = auditable.value(); } 复制代码

执行顺序

如果调用方法时,命中了多个Advice,如两个@Before被命中,那调用顺序如何控制?

使用@Order注解或实现org.springframework.core.Ordered接口即可。

但是 Before和After 类型的顺序是相反的,示意图如下:

由上图可见:

  • 对于Before类型的Advice,Order的值越小优先级越高,越优先执行

  • 对于After类型的Advice,Order的值越小优先级越高,但越最后执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值