Spring AOP 系列的第一篇
先介绍一下 AOP 相关的一些概念。
出现的契机
在现实中、我们经常需要记录重要操作的流水以及打印相关的日志到日志文件
// 微信公众号:CoderLi
public class BizService01 {
public void dealBiz(BizDto bizDto) {
// 脱敏打印 + 统计上报到运营系统
record(bizDto);
// 业务操作
}
private void record(BizDto bizDto){
// .....
}
}
当存在 n 多个这样的 Service 的时候、我们进一步的操作可能是:将其抽取到一个公共的地方进行记录维护
// 微信公众号:CoderLi
public class BizService01 {
public void dealBiz(BizDto bizDto) {
// 脱敏打印 + 统计上报到运营系统
RecordUtils.record(bizDto);
// 业务操作
}
}
再进一步、我们可能使用模板方法来设计。子类继承该基础服务
// 微信公众号:CoderLi
public abstract class BaseBizService {
public void dealBiz(BizDto bizDto){
// 脱敏打印 + 统计上报到运营系统
if (isRecord()) {
RecordUtils.record(bizDto);
}
readDealBiz(bizDto);
}
protected abstract void readDealBiz(BizDto bizDto);
protected boolean isRecord(){
return true;
}
}
这样子貌似是能解决统一处理非核心业务、但是如果我们还需要进行其他的一些非核心业务处理的时候、比如说、权限检验、性能监控等、并且这些非核心业务他们的顺序、在每一个业务场景中、顺序可能不一样、是否执行这些非核心业务也是不确定的、那么单纯靠模板方法来解决的话、会显得非常费力、甚至可以说、这种变化的流程不适合模板方法了已经。
这个时候 AOP 出现了。而第一个比较流行的 Java AOP 框架就是 AspectJ 了。
AspectJ 使用
使用 Idea 来演示一个 Hello World 程序。确保已经安装并且启用了 AspectJ 插件
使用 Ajc 编译器、并配置 AspectJTools jar 的路径
项目 pom 依赖
org.aspectj
aspectjrt
1.9.7
public class Main {
// 微信公众号:CoderLi
void sayHi(){
System.out.println("hello world");
}
public static void main(String[] args) {
Main main = new Main();
main.sayHi();
}
}
// 切面。微信公众号:CoderLi
public aspect MainAspect {
// 切点
pointcut sayHiPointcut():call(* Main.sayHi());
// 前置通知
before():sayHiPointcut(){
System.out.println("say Hi Before");
}
// 后置通知
after ():sayHiPointcut(){
System.out.println("say Hi After");
}
}
我们使用了关键字 aspect 创建了一个切面。在这个切面里面、我们使用了 pointcut 定义了一个切点。而所谓的切点就是切面需要应用的方法、而这些方法我们称之为目标方法。在这个切面里面我们还定义了两个通知、一个是目标方法执行前的通知、一个是执行后的通知。
切面就是切点和通知的组合体。
我们看下 Main class 反编译的内容
public static void main(String[] args) {
Main main = new Main();
Main var10000 = main;
try {
// 前置通知
MainAspect.aspectOf().ajc$before$com_demo_aspectj_MainAspect$1$acd0869f();
var10000.sayHi();// 目标方法
} catch (Throwable var3) {
// 后置通知
MainAspect.aspectOf().ajc$after$com_demo_aspectj_MainAspect$2$acd0869f();
throw var3;
}
// 后置通知
MainAspect.aspectOf().ajc$after$com_demo_aspectj_MainAspect$2$acd0869f();
}
后置通知在 catch 代码块中、确保异常的时候也能被通知到。
通知的类型:
- 前置通知
- 后置通知
- 异常通知
- 后置返回通知
- 环绕通知
相关概念
- 连接点:程序执行的某个特定位置,比如某个方法调用前、调用后,方法抛出异常后,对类成员的访问以及异常处理程序块的执行等。一个类或一段程序代码拥有一些具有边界性质的特定点,这些代码中的特定点就是连接点。它自身还可以嵌套其他的 Joinpoint。AOP 中的 Joinpoint 可以有多种类型:构造方法调用,字段的设置和获取,方法的调用,方法的执行,异常的处理执行,类的初始化。Spring 仅支持方法执行类型的 Joinpoint。
- 切点:如果连接点相当于数据中的记录,那么切点相当于查询条件,一个切点可以匹配多个连接点。所以切点表示一组 Joinpoint ,这些 Jointpoint 或是通过逻辑关系组合起来,或是通过通配、正则表达式等方式集中起来,它定义了相应的 Advice 将要发生的地方。
- 通知:是织入到目标类连接点上的一段程序代码。Spring 提供的通知接口都是带方位名的,如:BeforeAdvice、AfterReturningAdvice、ThrowsAdvice 等。我们通过 AOP 将横切关注功能加到原有的业务逻辑上,这是对原有业务逻辑的一种增强,可以是前置、后置、返回后、抛出异常时等。其实 Advice 翻译成“增强”更合理,更能准确表达其本质。既然大部分文献都是称为通知,我们这里也称为通知。
- 织入: 织入是将Advice通知添加到目标类具体连接点上的过程。编译器织入、动态织入。
- Aspect 切面:Pointcut(切点)和Advice(通知)组成的
Introduction:引介是一种特殊的通知,它为类添加一些属性和方法。这样,即使一个业务类原本没有实现某个接口,通过引介功能,可以动态的为该业务类添加接口的实现逻辑,让业务类成为这个接口的实现类。
织入
前面我们也看到编译之后的 Main class 类、可以看到它是实际性的改变了我们原有的 class 文件的。这称之为静态织入。
主要是 ajc 编译器在编译期将 aspect 类编译成 class 字节码之后、然后织入到目标类中。
还有一种是更加常见的织入方式--动态织入。动态织入是在运行期间将要增强的代码织入到目标类中、这样往往是通过动态代理完成的。比如说 Java JDK 的动态代理或者 CGLib 的动态代理。Spring AOP 就是采用动态织入的。
Pointcut 表达式
切点的表达式。Spring Aop只支持其中的9种,外加Spring Aop自己扩充的一种一共是10种类型的表达式
execution:一般用于指定方法的执行,用的最多。
within:指定某些类型的全部方法执行,也可用来指定一个包。
this:Spring Aop是基于代理的,生成的bean也是一个代理对象,this就是这个代理对象,当这个对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
target:当被代理的对象可以转换为指定的类型时,对应的切入点就是它了,Spring Aop将生效。
args:当执行的方法的参数是指定类型时生效。
@target:当代理的目标对象上拥有指定的注解时生效。
@args:当执行的方法参数类型上拥有指定的注解时生效。
@within:与@target类似,看官方文档和网上的说法都是@within只需要目标对象的类或者父类上有指定的注解,则@within会生效,而@target则是必须是目标对象的类上有指定的注解。而根据笔者的测试这两者都是只要目标类或父类上有指定的注解即可。
@annotation:当执行的方法上拥有指定的注解时生效。
bean:当调用的方法是指定的bean的方法时生效
这个后续的文章再详细介绍