浅谈 Java 和 SAP ABAP 的静态代理和动态代理,以及 ABAP 面向切面编程的尝试
文章目录
Java 的静态代理
静态代理的优缺点
ABAP 的静态代理
Spring AOP 的动态代理
JDK 动态代理的优缺点
CGLIB 动态代理的优缺点
ABAP CGLIB 的模拟实现
ABAP Pre 和 Post Exit
Jerry 之前一篇文章 SAP产品增强技术回顾,提到基于 Java 编程语言实现的 SAP Commerce,借助 Spring 框架的支持,能使用面向切面编程的理念(Aspect Orient Programming,以下简称 AOP),将业务代码和非业务代码(比如权限检查,日志记录,性能统计等)彻底分离开。
下图是某应用里方法的常规实现:权限检查,日志记录和性能检测的代码一次又一次地侵入到本应只包含业务代码的三个方法中:
下图是应用 AOP 之后的方法实现:三个方法体内只包含纯粹的业务代码,看起来清爽了很多。权限检查,日志记录和性能检测的代码,作为仍需关注的三个方面,以切面的方式编织到三个方法中。Weave,AOP 里的术语,中文材料里经常译成“编织”,描述了被代理类的方法通过非源代码修改层面被增添以新逻辑的动作。
我们说面向对象编程(Object Oriented Programming,简称 OOP)是一种理念,不同的编程语言可以有不同的实现。同理,AOP 这种理念,不同的编程语言也存在不同的实现。
Java AOP 的实现可以分为静态代理和动态代理两种。无论哪种代理方式,一言以蔽之,AOP 的核心为,业务逻辑位于原始类中始终保持不变,而编织的非业务逻辑位于代理类中。运行时执行的代码,实际上被调用的是代理类,原始类的业务逻辑通过代理类被间接地调用。
代理模式的 UML 图:
业务逻辑在编译期间被编织进入代理类的方式,称为静态代理;业务逻辑在运行期间才进行编织的方式,称为动态代理。准确地说,编译期编织还可细分为编译时和编译后编织,而运行期间编织又可细分为载入时编织和运行时编织,但这种细分方式不影响本文接下来的阐述,所以后续仍只按照编译期和运行期两大类来介绍。
看一些具体的例子。
Java 静态代理
定义一个 IDeveloper 的接口,里面包含一个 writeCode 的方法。创建一个 Developer 类,实现该方法。
测试:创建一个名为 Jerry 的 Developer 实例,调用 writeCode 方法。
假设我想让 Developer 在写代码之前,先编写对应的文档,但我不想把写文档这个逻辑,侵入到 writeCode 方法里。这里“编写文档”,就相当于待编织的非业务逻辑,或者叫做待编织的切面逻辑。
使用静态代理的思路,另外新建一个代理类 DeveloperProxy:
注意上图的 writeCode 方法,首先第 8 行完成文档编写的任务,然后代理类在第 9 行调用被代理类 Developer 的 writeCode 方法,完成写代码的实际业务逻辑。
测试代码:
Developer 和 DeveloperProxy 都实现了同一个接口 IDeveloper,对于消费者代码来说,它完全感知不到也不必要去感知这两个接口实现类的内部差异——这一切对消费者代码来说完全透明。消费者拿到的引入,指向的是类型为 IDeveloper 接口的变量,然后调用定义在接口上的 writeCode 方法即可。
静态代理的优缺点
从以上例子可以看出,静态代理工作的基石是接口,如果原始类由于某种原因,无法改造成为某个接口的实现类(比如原始类来自系统遗留代码,无法重构),则静态代理这条路行不通。
针对每个原始类,采用静态代理,都需要创建一个具有持久存储的代理类。这种方式便于理解,并且非业务逻辑(前例中的“写文档”行为)在编译期间植入静态代理类,实际运行时性能优于即将介绍的动态代理。
在 Java 里如果不想手动创建静态代理类,可以使用工具 AspectJ 来自动完成。由于本文的读者主要是 ABAP 开发人员,这里略过其使用方式。
ABAP 静态代理类的自动创建
我仿照 Java AspectJ 的思路,用 ABAP 写了一个类似的原型。下面是使用方法。
首先我创建一个类 CL_HELLOWORLD:
我想自动为该类创建一个静态代理,在代理类的 PRINT 方法里,除了调用这个原始类的 PRINT 方法外,再做一些额外的逻辑,比如打印一些输出。
调用下图的 GET_PROXY 方法,将自动为 CL_HELLOWORLD 创建一个静态代理类,将第 7 行和第 8 行指定的额外逻辑编织到静态代理类的 PRINT 方法里:
测试:调用静态代理类的 PRINT 方法,得到下图的输出,能观察到编织到静态代理类的两行 WRITE 语句,分别在原始类 PRINT 方法之前和之后被调用了:
SE24 可以观察到,通过我写的工具自动创建的 ABAP 静态类,及编织到代理类方法 PRINT 里的额外逻辑:
这个工具的核心是调用 ABAP Class API 生成新的 ABAP 类,源代码可以在文末 Jerry 提供的链接里获得:
Spring AOP 的动态代理
所谓动态代理,即 AOP 框架在编译期不会对原始类做任何处理,而是直到应用运行期间,在内存中临时为需要被代理的类生成一个 AOP 对象,该对象包含了原始类的全部方法,并且在被代理的方法处做了增强处理,编织入新的逻辑,并回调原始类的方法。
Spring AOP 动态代理有两种实现方式:JDK 动态代理和 CGLIB 动态代理。
JDK 动态代理
JDK 动态代理的原理是基于 Java 反射机制实现的方法拦截器机制。
我们在第一个例子的基础上,增添一个新的 ITester 接口,代表测试人员这个岗位:
现在的需求是给测试人员的 doTesting 方法内也植入编写文档的逻辑。如果采用静态代理的方式,我们得又创建一个 TesterProxy 的静态代理类。随着开发小组里人员岗位类型的增加,这些静态代理类的个数也随之增加。
那么用动态代理如何优雅地避免这个问题呢?
创建一个新的代理类,取名为 EnginnerProxy,名字暗示了这个实现了 JDK 标准接口 InnovationHandler 的类,在运行时能统一代理一个软件开发团队里所有角色的工程师类的方法。
第七行的 bind 方法,接收一个被代理类的实例,在运行时动态为该实例创建一个临时的代理类实例。所谓临时,指该代理实例的生命周期只存在于当前会话中,应用运行结束后即销毁,不会像静态代理类那样会持久化存储。
运行时代理类的方法一旦执行,无论是 Developer 的 writeCode, 还是 Tester 的 doTesting 方法,均会被 EnginnerProxy 的 invoke 方法拦截,在 invoke 方法内统一执行第 17 行的文档撰写逻辑,然后再调用 18 行包含了业务逻辑的原始类方法。
下图是测试代码及运行结果,现在无论是 Developer 还是 Tester,在写代码和做测试之前,都会自动执行文档撰写的任务了:
基于 JDK 动态代理的优缺点
显而易见,在需要代理多个类时,动态代理只需创建一个统一的代理类,而不必像静态代理那样,需要为每个包含业务逻辑的类单独创建代理类。而代理类“用后即焚”,也避免了在工程文件夹里生成太多代理类。
另一方面,因为动态代理在运行时通过 Java 反射机制实现,运行时的性能劣于在编译期间进行代理逻辑编织的静态代理。此外,JDK 动态代理工作的前提条件同静态代理一样,也需要被代理的类实现某个接口。
看个反例,假设产品经理类 ProductOwner 未实现任何接口:
使用 JDK 动态代理,在运行时会抛 ClassCastException 异常:
正因为 JDK 动态代理的这种局限性,存在另一种动态代理的实现方式:基于 CGLIB 的动态代理。
CGLIB(Code Generation Library)是一个 Java 字节码生成库,可以在运行时对 Java 类的字节码进行处理和增强,底层基于字节码处理框架 ASM 实现。
基于 CGLIB 的动态代理可以绕过 JDK 动态代理的限制,即使一个需要被代理的类没有实现任何接口,也能使用 CGLIB 动态代理。
注意这次使用 CGLIB 创建的统一代理类,导入的开发包来自 net.sf.cglib.proxy, 而非 JDK 动态代理解决方案中的 java.lang.reflect:
消费代码的风格同 JDK 动态代理类似:
CGLIB 动态代理的优缺点
CGLIB 克服了 JDK 动态代理需要被代理类必须实现某个接口才能工作的限制,然而其本身也有局限性。CGLIB 本质上是运行时用 API 操作 Java 类的字节码的方式,直接创建一个继承自被代理类的子类,然后将切面逻辑编织到这个子类方法中去。显而易见,如果被代理类被定义成无法继承,比如被 Java 和 ABAP 里的 final 关键字修饰,则 CGLIB 动态代理这种方式也无法工作。
做一个测试,我将 ProductOwner 类标志为 final,即无法被继承,这时在运行之前的测试代码,会遇到异常和错误消息:Cannot subclass final class
ABAP 动态代理
因为 ABAP 无法在语言层面精确做到像 Java JDK InnovationHandler 那样能够用一个代理类统一拦截多个被代理类方法执行的效果,因此 Jerry 选择对另一种动态代理,即 CGLIB 代理方式,用 ABAP 进行模拟。
首先创建一个需要被代理的类,业务逻辑写在 GREET 方法里。
接着使用 Jerry 自己实现的 ABAP CGLIB 工具类,通过其方法 GET_PPROXY 得到这个类的代理类,并调用代理类的 GREET 方法:
上图第 8 行和第 9 行是包含了两个切面逻辑的类,我期望其方法分别在被代理类的 GREET 调用之前和调用之后被执行。
ABAP CGLIB 的核心在 GET_PROXY 方法里的 generate_proxy 方法内:
这里使用了 ABAP 动态生成类的关键字 GENERATE SUBROUTINE POOL, 根据内表 mt_source 里包含的预先拼凑好的源代码,生成新的临时类。这个类不会在 SE24 或者 SE80 里存储,仅仅存活在当前应用的会话里。
第 17 行动态生成新的代理类之后,第 21 行生成一个该代理类的实例,然后在第 23 和 26 行分别植入切面逻辑。
最后调用这个代理类实例的 GREET 方法,打印输出如下:
其中 Hello World 是原始被代理类即 ZCL_JAVA_CGLIB 的 GREET 方法的输出,而它的前后两行为调用 ABAP CGLIB 生成代理类时传入的切面逻辑。
到目前为止,尽管我们意识到静态代理和动态代理都各自存在一些缺陷,但从这些缺陷出现的原因,也再次提醒我们,在编写新的代码时,要尽量面向接口编程,尽量避免直接面向实现编程,从而降低程序的耦合性,提高应用的可维护性,可复用性和可扩展性。
以上介绍的 ABAP CGLIB 工具只是 Jerry 开发的一个原型,在 ABAP 里如果仅仅想将切面逻辑(比如权限检查,日志记录,性能分析)彻底地同业务逻辑隔离开,可以使用 ABAP Netweaver 提供的对类方法增强的标准方式:Pre-Exit 和 Post-Exit.
选中要增强的类,点击 Enhance 菜单:
这种增强和被代理的类是分开存储的:
创建新的 Pre-Exit:
点击 Pre-Exit 的面板,就可以进去编写代码了:
在运行时,被代理类 ZCL_JAVA_CGLIB 的 GREET 方法执行之前,Pre-Exit 里的代码会自动触发:
Jerry 之前在 SAP Business By Design 这个产品工作的时候,在不修改产品标准代码的前提下,用这种 Exit 技术实现了很多的客户需求。典型的客户需求是,在 SAP 标准 UI 增添扩展字段,其值通过后台复杂的逻辑计算出来。于是我们首先把后台 API 的 Response 结构体做增强,新建一个扩展字段;然后给后台 API 取数方法创建一个 Post-Exit,将扩展字段的填充逻辑实现在 Exit 里。
采用 Pre 和 Post-Exit,虽然使用方式上和 Java Spring AOP 基于注解(Annotation)的工作方式相比有所差异,但从效果上看,也能实现 Spring AOP 将业务逻辑和非业务逻辑严格分开的需求。
本文介绍的 Java 和 ABAP 的静态和动态代理,以及 ABAP 模拟 Java CGLIB 的实现,在 Jerry 发布的 SAP 社区博客上有详细叙述:
感谢阅读。
版权声明: 本文为 InfoQ 作者【Jerry Wang】的原创文章。
原文链接:【http://xie.infoq.cn/article/02b2b4260d8c4e41c87ae7dfb】。文章转载请联系作者。
评论