Spring AOP 概念 & 应用 ——AOP 源码解析(一)
一、概念
参考:
1.1 什么是AOP?
AOP(Aspect Oriented Programming),即面向切面编程,通俗点说,是把代码逻辑的一些公共部分(比如参数校验、事务等)抽象出来,形成功能模块,然后把程序在某个位置(切点)“切开”,把这些抽象好的功能模块,放进去(织入)。这样代码运行时,到达切点后,就可以执行我们抽象出来的通用代码了。
其实AOP与通用功能模块化相比,主要是多了一步把功能“织入”到目标代码这一步,让代码复用更加加单,有时候你甚至不知道用到了AOP。
1.2 术语
简单介绍一下AOP的术语:
- Pointcut(切点): 定义了要在哪里把代码切开,比如在某个包下的某个公共方法执行前切入,具象化一点,就是Spring中定义切点的表达式,比如:
"execution (* com.hellohu.spring.aop.*.*(..))"
。 - Advice(通知):就是具体要执行的动作,比如参数校验、异常处理等抽象出来的功能模块。
- Aspect(切面):Pointcut(切点)+ Advice(通知)。
- Join Point(连接点) :程序中运行的一个点,与切点的关系,可以类比为对象与类的关系,也就是连接点是程序具体运行时的一个点。具象化一点,就是Spring中的JoinPoint接口,也就是我们定义通知时第一个入参,会提供方法签名、参数等信息。
- Introduction(引入):在一个类上引入额外的方法&实现,Spring AOP可以给被切的对象添加额外的接口和对应的实现。
- Target object:被一个或多个切面“织入”的对象,由于Spring使用动态代理,因此在Spring AOP中,始终是代理对象。
- Weaving(织入):把程序在“Pointcut”处切开,然后把“Advice”放进去的过程。可以在编译时织入(使用aspectJ编译器),可以在类加载的时织入,也可以在运行时织入。Spring AOP使用的是运行时织入。
以上这些概念都比较抽象,其实用起来还是非常简单的,不用过于纠结。
Spring AOP支持多种通知类型:
- Before advice(前置通知): 在切点之前运行,一般不会影响后续运行,除非抛出异常。
- After returning advice(返回通知):在切点正常返回后执行。
- After throwing advice(异常通知): 切点抛出异常后执行。
- After (finally) advice(后置通知): 切点执行完成之后执行,无论是否抛出异常。
- Around advice(环绕通知): 可以环绕切点执行,可以选择继续执行切点(甚至可以多次执行),也可以直接返回自定义的结果或者直接抛出异常。
二、使用
我们先看下Spring AOP该如何使用。
2.1 开启AOP支持
引入AOP依赖,至于版本,其实挑一个你喜欢的就好。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.5</version>
</dependency>
开启AOP的支持,可以使用注解的方式开启,也可以使用XML引入。我们这里仅演示使用注解开启的方法,注解使用@EnableAspectJAutoProxy
开启即可。
@Configuration
@EnableAspectJAutoProxy
public class HelloConfig {
}
2.2 定义切面
使用注解定义切面非常简单,只要使用@Aspect注解即可:
@Aspect
@Component
public class LogAspect {}
切面 = 切点+通知,有了切面类,接下来就是要定义切点和通知了。
2.2.1 定义切点
Spring AOP使用一个类似正则的表达式,来表示切点,确切的说,是复用(或者说是实现)了ASpectJ的提供的部分注解。ASpectJ定义的切点有很多种,不过Spring AOP,仅支持在方法上定义切点,其他ASpectJ中定义的在属性、构造器上的切点,是不支持的。接下来我们定义一下切点:
@Pointcut("execution (* com.hellohu.spring.aop.*.*(..))")
public void pointCut() {
}
以上代表切所有的com.hellohu.spring.aop包下类的所有公共方法。切点的表达式有很多种,这里解释下比较常用的execution表达式:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)
- modifier:修饰符,public, private等,不写表示匹配任意修饰符(?代表可选)
- ret-type:返回类型,“*”表示匹配任意类型
- declaring-type:类路径,包括包名,不写匹配任意类型,“..”表示匹配所有子包和类,比如
com.hellohu.spring.aop..
表示匹配com.hellohu.spring.aop
包下所有类。 - name-pattern:方法名称,使用“*”表示通配符
- param-pattern:参数类型和数量,*代表任意类型的一个参数,“..”则是任意类型任意数量
- () 匹配没有参数的方法
- (..) 匹配有任意数量参数的方法
- (*) 匹配有一个任意类型参数的方法
- (*,String) 匹配有两个参数的方法,并且第一个为任意类型,第二个为 String 类型
- throws-pattern:抛出异常类型,不写是匹配任意类型
以上就是execution表达式的说明,其他的还有args、this等表达式,这里不再赘述,可以参考第15章-Spring AOP切点表达式(Pointcut)详解-CSDN博客。
另外,表达式直接,也可以使用"与或非"(&& || !)连接,比如
@Pointcut("execution (* com.hellohu.spring.aop.*.*(..)) && !bean(notCut*)")
以上表示匹配com.hellohu.spring.aop包下所有类的所有public方法,但是排除名字以notCut开头的Bean。
2.2.2 定义通知
我们简单定义一个日志通知,把所有的通知类型都简单的实现一下,正好顺便可以观察一下通知的执行顺序。
package com.hellohu.spring.aop;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogAspect {
@Pointcut("execution (* com.hellohu.spring.aop.*.*(..))")
public void pointCut() {
}
@Before(value = "pointCut()")
public void before(JoinPoint joinPoint) {
//记录参数
System.out.println("before advice: " + genSignatureStr(joinPoint.getSignature()) + ", args:[" + StringUtils.join(joinPoint.getArgs(), ",") + "]");
}
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(JoinPoint joinPoint, Object result) {
//记录返回值
String resultString = result == null ? "void" : result.toString();
System.out.println("return advice: " + genSignatureStr(joinPoint.getSignature()) + ", return :" + resultString);
}
@After(value = "pointCut()")
public void after(JoinPoint joinPoint) {
//什么也没记录
System.out.println("after/finally advice: " + genSignatureStr(joinPoint.getSignature()));
}
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(JoinPoint joinPoint, Exception e) {
//记录运行时的异常
System.out.println("after throwing advice: " + genSignatureStr(joinPoint.getSignature()) + " exception:" + e.getMessage());
}
@Around(value = "pointCut()")
public Object around(JoinPoint joinPoint) throws Throwable {
//记录运行耗时
System.out.println("around advice begin");
long now = System.currentTimeMillis();
try {
return ((ProceedingJoinPoint) joinPoint).proceed();
} finally {
long cost = System.currentTimeMillis() - now;
System.out.println("around advice end " + genSignatureStr(joinPoint.getSignature()) + " cost " + cost + "ms");
}
}
private String genSignatureStr(Signature signature) {
return signature.getDeclaringType().getName() + "." + signature.getName();
}
}
2.2.3 测试执行
我们先定义一个被“切”的类
package com.hellohu.spring.aop;
import java.util.Objects;
import org.springframework.stereotype.Repository;
@Repository
public class AopDaoImpl{
public int update(String s) {
Objects.requireNonNull(s);
System.out.println("update " + s);
return 1;
}
}
测试代码,第一次调用update会正常执行,第二次会抛出空指针异常。
@Test
public void testAop() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(HelloConfig.class)) {
AopDao aopDao = ctx.getBean(AopDao.class);
aopDao.update("hello world!!");
aopDao.update(null);
}
}
第一次aopDao.update("hello world!!");
执行结果:
around advice begin
before advice: com.hellohu.spring.aop.AopDao.update, args:[hello world!!]
update hello world!!
return advice: com.hellohu.spring.aop.AopDao.update, return :1
after/finally advice: com.hellohu.spring.aop.AopDao.update
around advice end com.hellohu.spring.aop.AopDao.update cost 4ms
第二次aopDao.update(null);
执行结果:
around advice begin
before advice: com.hellohu.spring.aop.AopDao.update, args:[]
after throwing advice: com.hellohu.spring.aop.AopDao.update exception:null
after/finally advice: com.hellohu.spring.aop.AopDao.update
around advice end com.hellohu.spring.aop.AopDao.update cost 0ms
。。。省略异常日志。。。
从执行效果上来看,五种通知执行的先后顺序为:
- 环绕通知。
- 前置通知。
- 返回/异常通知二选一,正常返回执行返回通知,否则执行异常通知。
- 后置通知,该通知无论是否异常,都会执行,类似于finally的效果。
2.2.4 补充说明
- 切点的表达式,也可以直接写在各种注解的value属性上,比
@Around(value = "execution (* com.hellohu.spring.aop.*.*(..))")
,效果是一样的,只不过定义在方法上,更方便复用。上面所有的通知,统一使用"pointCut()"即可。甚至不在一个切面类中,也可以复用,只不过需要指定类的全路径了,比如我在另一个切面复用这个表达式:@Around(value = "com.hellohu.spring.aop.LogAspect.pointCut()")
。 - 这个例子中,仅定义了一个切面类,那如果多个切面类,执行的先后顺序是怎样的呢,可以通过@Order注解来指定切面的优先级。
三、执行原理
后续文章,会通过解析Spring AOP的源码,对AOP的原理一探究竟。不过在探究原理前,我们可以大致猜下Spring AOP是如何实现的。
- 引入,通过
@EnableAspectJAutoProxy
注解开启AOP功能,那必然是通过该注解引入了某个BeanPostProcessor
,在bean创建完成后,对bean做了动态代理。 - 关于动态代理,相信你已经熟知了,无非是基于JDK还是CGLIB,不过无论使用哪种动态代理,如何把各种切面组织在一起,各种通知执行的顺序等等,应该是统一写在一起来维护的。
- 具体通知的实现,各种通知应该是不同的,不出意外,应该是典型的模板+策略模式。
后续会逐步解析Spring AOP的源码,自下而上的分析,也就是说,按照上面猜测的步骤,3/2/1的方式去分析。
系列所有文章
转载自:https://juejin.cn/post/7366086988350504971