介绍
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依赖后,框架已经自动启动了。
-
添加AOP依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> 复制代码
-
框架自动配置
内部已经有了配置:
在类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的值越小优先级越高,但越最后执行