本文将从另外一个角度讲解 AOP,从宏观的实现原理和设计本质入手。大部分讲 AOP 的博文都是一上来就罗列语法,而后敲个应用 demo就完了 。但学习不能知其然,不知其因此然。java
对 AOP 我提出了几点思考:AspectJ 为何会大热?AspectJ 是怎样工做的?和 Spring AOP 有什么区别?什么场景下适用?咱们能不能本身实现一个 AOP 方法?android
在熟悉原理前,若是想先掌握 AOP 的使用方法能够看:设计模式
敲一个小 Demo 来引入主题,假设我想不依赖任何 AOP 方法,在特定方法的执行先后加上日志打印。bash
定义一个目标类接口闭包
把 before() 和 after() 方法写死在 execute() 方法体中,很是不优雅,咱们改进一下。架构
可是存在一个问题,随着打印日志的需求增多,Proxy 类愈来愈多,咱们能不能保持只有一个代理呢?这时候咱们就须要用到 JDK 动态代理了。框架
新建动态代理类函数
客户端调用工具
这又引出一个问题,日志打印和业务逻辑耦合在一块儿,咱们但愿把前置和后置抽离出来,做为单独的加强类。post
新建加强类接口和实现类
用反射代替写死方法,解耦代理和操做者
客户端调用
可是用了反射性能太差了,并且动态代理用起来也不方便,有没有更好的办法?
咱们的诉求很简单:1. 性能高;2. 松耦合;3. 步骤方便;4. 灵活性高。
那主流的 AOP 框架是怎么解决这个问题的呢?咱们赶忙来看看!
不一样的 AOP 方法原理略微有些不一样,咱们先看下 AOP 实现方式有哪些:
AOP方式 | 机制 | 说明 |
---|---|---|
静态织入 | 静态代理 | 直接修改原类,好比编译期生成代理类的 APT |
静态织入 | 自定义类加载器 | 使用类加载器启动自定义的类加载器,并加一个类加载监听器,监听器发现目标类被加载时就织入切入逻辑,以 Javassist 为表明 |
动态织入 | 动态代理 | 字节码加载后,为接口动态生成代理类,将切面植入到代理类中,以 JDK Proxy 为表明 |
动态织入 | 动态字节码生成 | 字节码加载后,经过字节码技术为一个类建立子类,并在子类中采用方法拦截的技术拦截全部父类方法的调用织入逻辑。属于子类代理,以 CGLIB 为表明 |
全部 AOP 方法本质就是:拦截、代理、反射(动态状况下),实现原理能够看做是代理 / 装饰设计模式的泛化,为何这么说?咱们来详细分析一下。
静态织入原理就是静态代理,咱们以 AspectJ 为例。
前面说到 Demo 存在的种种问题,AspectJ 是怎么解决的呢?AspectJ 提供了两套强大的机制:
AspectJ 中的切面,就解决了这个问题。
@Before("execution(* android.view.View.OnClickListener.onClick(..))")
复制代码
咱们能够经过切面,将加强类与拦截匹配条件(切点)组合在一块儿,从而生成代理。这把是否要使用切面的决定权利还给了切面,咱们在写切面时就能够决定哪些类的哪些方法会被代理,从而逻辑上不须要侵入业务代码。
而普通的代理模式并无作到切面与业务代码的解耦,虽然将切面的逻辑独立进了代理类,可是决定是否使用切面的权利仍然在业务代码中。这才致使了 Demo 中种种的麻烦。
AspectJ 提供了两套对切面的描述方法:
@Aspect
public class AnnoAspect {
@Pointcut("execution(...)")
public void jointPoint() {
}
@Before("jointPoint()")
public void before() {
//...
}
@After("jointPoint()")
public void after() {
//...
}
}
复制代码
public aspect AnnoAspect {
pointcut XX():
execution(...);
before(): XX() {
//...
}
after(): XX() {
//...
}
}
复制代码
那么切面语法让切面从逻辑上与业务代码解耦,可是我要怎么找到特定的业务代码织入切面呢?
两种解决思路:一种就是提供注册机制,经过额外的配置文件指明哪些类受到切面的影响,不过这仍是须要干涉对象建立的过程;另一种解决思路就是在编译期或类加载期先扫描切面,并将切面代码经过某种形式插入到业务代码中。
那 AspectJ 织入方式有两种:一种是 ajc 编译,能够在编译期将切面织入到业务代码中。另外一种就是 aspectjweaver.jar 的 agent 代理,提供了一个 Java agent 用于在类加载期间织入切面。
@Before
机制国际惯例写个 Demo
反编译后(请点开大图查看)
发现 AspectJ 会把调用切面的方法插入到切入点中,且封装了切入点所在的方法名、所在类、入参名、入参值、返回值等等信息,传递给切面,这样就创建了切面和业务代码的关联。
咱们跟进 LogAspect.aspectOf().aroundJoinPoint(localJoinPoint);
一探究竟。
咱们发现了什么?其实 Before 和 After 的插入就是在匹配到的 JoinPoint 调用先后插入 Advise 方法,以此来达到拦截目标 JoinPoint 的做用。 以下图所示:
@Around
机制打开编译后的 class 文件(请点开大图查看)
咱们发现和 Before、After 织入不同了!前者的织入只是在匹配的 JoinPoint 先后插入 Advise 方法,仅仅是插入。而 Around 拆分了业务代码和 Advise 方法,把业务代码迁移到新函数中,经过一个单独的闭包拆分来执行,至关于对目标 JoinPoint 进行了一个代理,因此 Around 状况下咱们除了编写切面逻辑,还须要手动调用 joinPoint.proceed() 来调用闭包执行原方法。
咱们看下 proceed() 都作了些什么
那这个 arc 是什么?何时拿到的呢?
继续回溯
在 AroundClosure 闭包中,会把运行时对象和当前链接点 joinPoint 对象传入,调用 linkClosureAndJoinPoint() 绑定两端,这样在 Around 中就能够经过 ProceedingJoinPoint.proceed() 调用 AroundClosure,进而调用到目标方法了。
那么一图总结 Around 机制:
咱们从 AspectJ 编译后的 class 文件能够明显看出执行的逻辑,proceed 方法就是回调执行被代理类中的方法。
因此 AspectJ 作的事情以下:
首先从文件列表里取出全部的文件名,读取文件,进行分析;
扫描含有 aspect 的切面文件;
根据切面中定义规则,拦截匹配的 JoinPoint ;
继续读取切面定义的规则,根据 around 或 before ,采用不一样策略织入切面。
@Before
@After
机制与 @Around
机制区别分析完 class 你会发现,AspectJ 实际上就是用一种特定语言编写切面,经过本身的语法编译工具 ajc 编译器来编译,生成一个新的代理类,该代理类加强了业务类。
AspectJ 就是一个代码生成工具;
编写一段通用的代码,而后根据 AspectJ 语法定义一套代码生成规则,AspectJ 就会帮你把这段代码插入到对应的位置去。
AspectJ 语法就是用来定义代码生成规则的语法。
扩展编译器,引入特定的语法来建立 Advise,从而在编译期间就织入了Advise 的代码。
若是使用过 Java Compiler Compiler (JavaCC),你会发现二者的代码生成规则的理念惊人类似。JavaCC 容许你在语法定义规则文件中,加入你本身的 Java 代码,用来处理读入的各类语法元素。
动态织入原理就是动态代理。
Spring AOP 利用截取的方式,对被代理类进行装饰,以取代原有对象行为的执行,不会生成新类。
可能有的小伙伴会困惑了,Spring AOP 使用了 AspectJ,怎么是动态代理呢?
那是由于 Spring 只是使用了与 AspectJ 同样的注解,没有使用 AspectJ 的编译器,转向采用动态代理技术的实现原理来构建 Spring AOP 的内部机制(动态织入),这是与 AspectJ(静态织入)最根本的区别。
Spring 底层的动态代理分为两种 JDK 动态代理和 CGLib:
JDK 动态代理用于对接口的代理,动态产生一个实现指定接口的类,注意动态代理有个约束:目标对象必定是要有接口的,没有接口就不能实现动态代理,只能为接口建立动态代理实例,而不能对类建立动态代理。
CGLIB 用于对类的代理,把被代理对象类的 class 文件加载进来,修改其字节码生成一个继承了被代理类的子类。使用 cglib 就是为了弥补动态代理的不足。
咱们前面的 Demo 第三种方式使用了动态代理,咱们不由有了疑问,动态代理类及其对象实例是如何生成的?调用动态代理对象方法为何能够调用到目标对象方法?
咱们经过 Proxy.newProxyInstance
能够动态生成指定接口的代理类的实例。咱们来看下newProxyInstance
内部实现机制。
代理对象会实现接口的全部方法,实现的方法交由咱们自定义的 handler 来处理。
咱们看下 getProxyClass0
方法,只凭一个类加载器、一个接口,是怎么建立代理类的?
注意一下:Android 中动态代理类是直接生成,而 Java 是生成代理类的字节码,再根据字节码生成代理类。
那么客户端就能够 getProxy()
拿到生成的代理类 com.sun.proxy.$Proxy0
这个代理类继承自 Proxy
并实现了咱们被代理类的全部接口,在各个接口方法的内部,经过反射调用了 InvocationHandlerImpl
的 invoke
方法。
总结下步骤:
不知不觉咱们复习了一下代理模式,设计模式必须依赖大量的业务场景,脱离业务去看设计模式是没有意义的。
由于脱离了应用场景,即便理解了模式的内容和结构,也学不会在合适的时候应用。
首先你要勇于追求优雅的代码,就像咱们开头的打印日志的需求,不断提出问题,不断追求更好的解决方案,在新的方案上挖掘新的问题……若是你彻底不追求设计,那天然是不会想到去研究设计模式的。
本篇完成耗时 26 个番茄钟(650 分钟)
我是 FeelsChaotic,一个写得了代码 p 得了图,剪得了视频画得了画的程序媛,致力于追求代码优雅、架构设计和 T 型成长。
欢迎关注 FeelsChaotic 的简书和掘金,若是个人文章对你哪怕有一点点帮助,欢迎 ❤️!你的鼓励是我写做的最大动力!
最最重要的,请给出你的建议或意见,有错误请多多指正!