jfinal AOP 实现原理和整体架构

jfinal AOP 实现原理和整体架构


语雀链接:https://www.yuque.com/miaoshuo/ko4syg/ny150b

(博客排版有问题,代码&表格请见原文)


序言

jfinal 是一个极简的 webmvc 框架,并且用极少的代码实现了 AOP,本文介绍 jfinal 中 AOP 实现原理和整体架构。


一、概念理解&使用方式


AOP(Aspect-oriented programming,面向切面编程),是通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。


传统AOP实现需要引入大量繁杂而多余的概念,例如:Aspect、Advice、Joinpoint、Poincut、Introduction、Weaving、Around等等,并且需要引入IOC容器并配合大量的XML或者annotation来进行组件装配。

传统AOP不但学习成本极高,开发效率极低,开发体验极差,而且还影响系统性能,尤其是在开发阶段造成项目启动缓慢,极大影响开发效率。

JFinal采用极速化的AOP设计,专注AOP最核心的目标,将概念减少到极致,仅有三个概念:Interceptor、Before、Clear,并且无需引入IOC也无需使用啰嗦的XML。


jfinal 实现的 AOP 只有三个概念,理解和使用相对简单。

概念

概述

Interceptor

对方法进行拦截,并提供机会在方法的前后添加切面代码,实现 AOP 的核心目标

Before

用来对拦截器进行配置,该注解可配置Class、Method级别的拦截器

Clear

    拦截器从上到下依次分为Global、Routes、Class、Method四个层次,Clear用于清除自身所处层次以上层的拦截器。


使用方式


使用 jfinal 的 AOP只需要三步:

  1. 在方法上使用 @Before(OtherInterceptor.class)

  2. 在属性上使用 @Inject

  3. 使用 Aop.get() 获取增强实例


代码如下:

//主 Service@Slf4jpublic class Service {    @Inject    private OtherService otherService;    @Before(ServiceInterceptor.class)    public void doIt() {        log.info("doIt");    }    public static class ServiceInterceptor implements Interceptor {        @Override        public void intercept(Invocation inv) {            log.info("before ServiceInterceptor");            inv.invoke();            log.info("after ServiceInterceptor");        }    }}


//被注入的属性@Slf4jpublic class OtherService {    @Before(OtherInterceptor.class)    public void doOther() {        log.info("doOther");    }    public static class OtherInterceptor implements Interceptor {        @Override        public void intercept(Invocation inv) {            log.info("before otherInterceptor");            inv.invoke();            log.info("after otherInterceptor");        }    }}


//获取增强实例public class AopDemo {    public static void main(String[] args) {        ProxyManager.me().setPrintGeneratedClassToConsole(true);        Service service= Aop.get(Service.class);        service.doIt();    }}



二、实现原理


jfinal 原生实现


原生实现原理如下,见 ProxyFactory :

  1. 解析被增强类的 class,根据模板生成被增强类的 java 代码

  2. 编译该 java 代码

  3. 加载增强后的class 文件

  4. 反射生成该 class 的实例

  5. 根据 @Inject 注解做依赖注入



其中最重要的步骤就是更新模板生成被增强类的 java 代码。其生成的代码如下:


package com.example.demo.jfinal;import com.jfinal.aop.Invocation;public class Service$$EnhancerByJFinal extends Service {    @Override    public void doIt() {        Invocation inv = new Invocation(this, 1L,            args -> {                Service$$EnhancerByJFinal.super.doIt(                );                return null;            }        );        inv.invoke();    }}


可以看到,代码中构建了 Invocation 并调用 invoke,其实就相当于把所有的执行逻辑放到 Invocation ,自动生成的增强类只是一层皮。接下来我们看下 Invocation 是如何实现执行逻辑的。



cglib 实现


cglib 实现原理 很简单,调用 cglib 即可


    public <T> T get(Class<T> target) {        return (T)net.sf.cglib.proxy.Enhancer.create(target, new CglibCallback());    }

CglibProxyFactory 就一行代码,就是调用 cglib 对 target 进行增强。切入逻辑在 CglibCallback 中

/** * CglibCallback. */class CglibCallback implements MethodInterceptor {    public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {    //1. 构造拦截器链        MethodKey key = IntersCache.getMethodKey(targetClass, method);        Interceptor[] inters = IntersCache.get(key);        if (inters == null) {            inters = interMan.buildServiceMethodInterceptor(targetClass, method);            IntersCache.put(key, inters);        }            //2. 封装 Invocation 并执行        Invocation invocation = new Invocation(target, method, inters,            x -> {                return methodProxy.invokeSuper(target, x);            }        , args);        invocation.invoke();        return invocation.getReturnValue();    }}


可以看出 cglib 实现的增强逻辑和原生实现的增强逻辑一致。都是构建 Invocation 并执行,只不过原生实现中通过 ProxyMethod 获取拦截器链,cglib 实现中使用InterceptorManager 构建拦截器链。


Invocation 实现织入逻辑


初始化

//构造方法  public Invocation(Object target, Long proxyMethodKey, Callback callback) {        this(target, proxyMethodKey, callback, NULL_ARGS);    }    public Invocation(Object target, Long proxyMethodKey, Callback callback, Object... args) {        this.action = null;        this.target = target;                ProxyMethod proxyMethod = ProxyMethodCache.get(proxyMethodKey);        this.method = proxyMethod.getMethod();        this.inters = proxyMethod.getInterceptors();                this.callback = callback;        this.args = args;    }


构造方法如上,通过ProxyMethod 获取了拦截器列表,其思路是根据类和方法上的 @Before 构建拦截器。



执行逻辑

        public void invoke() {        if (index < inters.length) {            inters[index++].intercept(this);        }        else if (index++ == inters.length) {    // index++ ensure invoke action only one time            try {                // Invoke the action                if (action != null) {                    returnValue = action.getMethod().invoke(target, args);                }                // Invoke the callback                else {                    returnValue = callback.call(args);                }            }            catch (InvocationTargetException e) {                Throwable t = e.getTargetException();                if (t == null) {t = e;}                throw t instanceof RuntimeException ? (RuntimeException)t : new RuntimeException(t);            }            catch (RuntimeException e) {                throw e;            }            catch (Throwable t) {                throw new RuntimeException(t);            }        }    }


执行逻辑也很简单,有拦截器则先执行拦截器,拦截器执行完毕则执行 callback





三、时序图




四、架构


整体架构


整体分为两层,Aop 和 Proxy 。Proxy 用于获取增强后的class 对象,包括生成增强的 java 代码、编译、加载 class;Aop 使用proxy增强后的代码并组装拦截器链以实现拦截逻辑

image.png


UML 类图

Proxy


Proxy 类图如下:


含义

类名

备注

对外接口

Proxy

提供 get 接口,根据 class 获取增强后实例

对外接口内部实现

ProxyFactory

虽然命名为 Factory ,但并不是工厂模式,而是 Proxy 的实现类

配置类

ProxyManager

Proxy模块对外的配置类

功能实现类

  1. ProxyGenerator

  2. ProxyCompiler

  3. ProxyClassLoader

代码生成器、代码编译器和class 加载器

实体

  1. ProxyClass

  2. ProxyMethod

  3. ProxyMethodCache

  1. ProxyClass,保存被代理类的元信息

  2. ProxyMethod,保存被代理的方法和方法上的拦截器

  3. ProxyMethodCache,缓存 ProxyMethod,供 Invocation 根据 proxyMethodKey 获取ProxyMethod。 (感觉这里有点多余,创建 Invocation 时直接传入 ProxyMethod 即可)

image.png


Aop


Aop 类图如下

含义

类名

备注

对外接口

Aop

  1. get 获取增强后的实例

  2. inject 注入依赖

对外接口内部实现

AopFactory

Aop 接口的实现类

配置类

AopManager

Aop 模块对外的配置类

功能实现类

  1. Proxy 模块

使用 proxy 模块的能力完成切面能力

实体

  1. Invocation

  2. Interceptor

  3. InterceptorManager

  1. Invocation,封装一次调用,对模板执行拦截器逻辑

  2. Interceptor,拦截器接口

  3. InterceptorManager,构建 target 的拦截器列表

image.png


五、总结


  1. jfinal 的 Aop 实现本质上是构建 Invocation。原生实现通过生成 java 代码并编译的方式生成增强类,cglib 实现通过 cglib 增强目标类。(生成 java 代码并编译生成增强的类思路牛逼!!!)

  2. 按 对外接口、对外配置类、接口实现类、功能实现类、实体 对jfinal 的类进行分类非常清晰。


参考

  1. jfinal Aop 官方文档

评论区

JFinal

2020-09-24 22:10

博主的分析十分深入,值得学习

jfinal AOP 与 spring AOP 最大不同在于 jfinal 是极简设计,用户只需要学习三个核心概念:Interceptor、Before、Clear,这让学习成本低到极致

而 spring AOP 的概念一大堆,例如:Aspect、Advice、Joinpoint、Poincut、Introduction、Weaving、Around等等,并且需要引入IOC容器并配合大量的XML或者annotation来进行组件装配。概念太多让学习成本急剧上升,开发时头脑的负荷加重,认知成本很高,相应的开发效率也会降低


除了极简设计、学习成本低以外,jfinal AOP 的另一特色是采用动态编译方式实现 proxy, 在 java 界绝无仅有

同时还支持 cglib 进行增强模式实现,目的是为了支持 JRE 环境没有动态编译支持的场景

李通

2020-09-25 18:13

@pfjia 代码结构有写乱 时序图是用什么软件画的,整体架构是用什么软件画的整体架构,uml类图是用什么软件画的

李通

2020-09-25 18:23

@jfinal jfinal AOP 的另一特色是采用动态编译方式实现 proxy 这个可以详细介绍一下吗?

JFinal

2020-09-26 01:03

@李通 这部分代码很少,主要流程就是:
1:ProxyGenerator 生成被代理类的子类,覆盖子类的被代理方法,生成的模板如下:
https://gitee.com/jfinal/jfinal/blob/master/src/main/java/com/jfinal/proxy/proxy_class_template.jf

看懂生成的 java 文件就能明白为啥可以实现 AOP 功能

2:生成 .java 文件以后,再使用 ProxyCompiler 编译成 .class 文件

3:使用 ProxyClassLoader 加载编译好的 .class 文件。使用 ProxyClassLoader 加载的类时,会用上其 AOP 相关功能

最好的办法是通过单步调试源码了解运行原理,代码不多,只有 600 多行,一会就看完了

李通

2020-09-26 09:27

@JFinal 先生成java文件,再动态编译,是个好想法

JFinal

2020-09-26 10:39

@李通 常规方法是通过 asm 或 cglib 做节码增强,其中 asm 方案可读性很差,而 cglib 增加了一个第三方依赖

jfinal 在 4.0 版本时去除了唯一的第三方依赖 cglib,采用了动态编译方案。该方案我从没在别的地方看到过,属于 jfinal 独创

不仅如此,该方案还与控制层的 AOP 使用了同一个 Interceptor + Invocation 结构,代码其实很优雅,注意看一下控制层的 AOP 没采用动态编译也没采用 cglib,而是采用的 actionKey 与 Action 映射的方式实现的

李通

2020-09-26 15:08

@JFinal 确实是jfinal独创,常规的字节码增强 有jdk动态代理,asm,cglib,aspect,spring aop,这些技术都需要再class上面增增改改,而你的方案(我称为jfinal动态编译方案)采用生成java文件,然后动态编译成class,再设计上就绕开了对class的修改,有下面这些优点

1.理论上可以做任何事情,因为是java文件,没有其他字节码增强技术的限制
2.方便调试,因为是生成java文件,可以直接阅读java文件定位问题,甚至debugjava文件,其他字节码增强技术只能dump出class文件,在调试上因为是增强后的代理类,需要通过一些手段才可以进入方法,大大提到了学习成功
3.代码优雅,可读性强,可维护性强

先生成java文件,再动态编译的方案解决了我之前困扰我多年的一个问题,当年我为了解决这个问题学习了大量的字节码增强技术,如果早些看到你的这个方案,或许会节省我大量的时间

JFinal

2020-09-26 15:30

@李通 JDK 动态代理必须要求被代理的目标实现接口,这个就极大限制了可代理的范围,所在 JDK 代理其实并没有多少意义

你说的很对,生成 java 代码可读性很好,出问题容易排错,而如果用 asm 操作字节码,出问题就不容易排查,字节码的可读性跟汇编语言是同一层次的