Jerry之前一篇文章 SAP产品增强技术回顾,提到基于Java编程语言实现的SAP Commerce,借助Spring框架的支持,能使用面向切面编程的理念(Aspect Orient Programming,以下简称AOP),将业务代码和非业务代码(比如权限检查,日志记录,性能统计等)彻底分离开。
下图是某应用里方法的常规实现:权限检查,日志记录和性能检测的代码一次又一次地侵入到本应只包含业务代码的三个方法中:
下图是应用AOP之后的方法实现:三个方法体内只包含纯粹的业务代码,看起来清爽了很多。权限检查,日志记录和性能检测的代码,作为仍需关注的三个方面,以切面的方式编织到三个方法中。Weave,AOP里的术语,中文材料里经常译成“编织”,描述了被代理类的方法通过非源代码修改层面被增添以新逻辑的动作。
我们说面向对象编程(Object Oriented Programming,简称OOP)是一种理念,不同的编程语言可以有不同的实现。同理,AOP这种理念,不同的编程语言也存在不同的实现。
Java AOP的实现可以分为静态代理和动态代理两种。无论哪种代理方式,一言以蔽之,AOP的核心为,业务逻辑位于原始类中始终保持不变,而编织的非业务逻辑位于代理类中。运行时执行的代码,实际上被调用的是代理类,原始类的业务逻辑通过代理类被间接地调用。
代理模式的UML图:
业务逻辑在编译期间被编织进入代理类的方式,称为静态代理;业务逻辑在运行期间才进行编织的方式,称为动态代理。准确地说,编译期编织还可细分为编译时和编译后编织,而运行期间编织又可细分为载入时编织和运行时编织,但这种细分方式不影响本文接下来的阐述,所以后续仍只按照编译期和运行期两大类来介绍。
看一些具体的例子。
定义一个IDeveloper的接口,里面包含一个writeCode的方法。创建一个Developer类,实现该方法。
测试:创建一个名为Jerry的Developer实例,调用writeCode方法。
假设我想让Developer在写代码之前,先编写对应的文档,但我不想把写文档这个逻辑,侵入到writeCode方法里。这里“编写文档”,就相当于待编织的非业务逻辑,或者叫做待编织的切面逻辑。
使用静态代理的思路,另外新建一个代理类DeveloperProxy:
注意上图的writeCode方法,首先第8行完成文档编写的任务,然后代理类在第9行调用被代理类Developer的writeCode方法,完成写代码的实际业务逻辑。
测试代码:
Developer和DeveloperProxy都实现了同一个接口IDeveloper,对于消费者代码来说,它完全感知不到也不必要去感知这两个接口实现类的内部差异——这一切对消费者代码来说完全透明。消费者拿到的引入,指向的是类型为IDeveloper接口的变量,然后调用定义在接口上的writeCode方法即可。
从以上例子可以看出,静态代理工作的基石是接口,如果原始类由于某种原因,无法改造成为某个接口的实现类(比如原始类来自系统遗留代码,无法重构),则静态代理这条路行不通。
针对每个原始类,采用静态代理,都需要创建一个具有持久存储的代理类。这种方式便于理解,并且非业务逻辑(前例中的“写文档”行为)在编译期间植入静态代理类,实际运行时性能优于即将介绍的动态代理。
在Java里如果不想手动创建静态代理类,可以使用工具AspectJ来自动完成。由于本文的读者主要是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提供的链接里获得:
所谓动态代理,即AOP框架在编译期不会对原始类做任何处理,而是直到应用运行期间,在内存中临时为需要被代理的类生成一个AOP对象,该对象包含了原始类的全部方法,并且在被代理的方法处做了增强处理,编织入新的逻辑,并回调原始类的方法。
Spring AOP动态代理有两种实现方式:JDK动态代理和CGLIB动态代理。
JDK动态代理的原理是基于Java反射机制实现的方法拦截器机制。
我们在第一个例子的基础上,增添一个新的ITester接口,代表测试人员这个岗位:
现在的需求是给测试人员的doTesting方法内也植入编写文档的逻辑。如果采用静态代理的方式,我们得又创建一个TesterProxy的静态代理类。随着开发小组里人员岗位类型的增加,这些静态代理类的个数也随之增加。
那么用动态代理如何优雅地避免这个问题呢?
创建一个新的代理类,取名为EnginnerProxy,名字暗示了这个实现了JDK标准接口InnovationHandler的类,在运行时能统一代理一个软件开发团队里所有角色的工程师类的方法。
第七行的bind方法,接收一个被代理类的实例,在运行时动态为该实例创建一个临时的代理类实例。所谓临时,指该代理实例的生命周期只存在于当前会话中,应用运行结束后即销毁,不会像静态代理类那样会持久化存储。
运行时代理类的方法一旦执行,无论是Developer的writeCode, 还是Tester的doTesting方法,均会被EnginnerProxy的invoke方法拦截,在invoke方法内统一执行第17行的文档撰写逻辑,然后再调用18行包含了业务逻辑的原始类方法。
下图是测试代码及运行结果,现在无论是Developer还是Tester,在写代码和做测试之前,都会自动执行文档撰写的任务了:
显而易见,在需要代理多个类时,动态代理只需创建一个统一的代理类,而不必像静态代理那样,需要为每个包含业务逻辑的类单独创建代理类。而代理类“用后即焚”,也避免了在工程文件夹里生成太多代理类。
另一方面,因为动态代理在运行时通过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克服了JDK动态代理需要被代理类必须实现某个接口才能工作的限制,然而其本身也有局限性。CGLIB本质上是运行时用API操作Java类的字节码的方式,直接创建一个继承自被代理类的子类,然后将切面逻辑编织到这个子类方法中去。显而易见,如果被代理类被定义成无法继承,比如被Java和ABAP里的final关键字修饰,则CGLIB动态代理这种方式也无法工作。
做一个测试,我将ProductOwner类标志为final,即无法被继承,这时在运行之前的测试代码,会遇到异常和错误消息:Cannot subclass final class
因为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社区博客上有详细叙述:
Implement CGLIB in ABAP
Create dynamic proxy persistently in Java and ABAP
Various Proxy Design Pattern implementation variants in Java, ABAP and JavaScript
本文提到的Jerry开发的所有ABAP原型和工具,在这个链接里有源代码。
今后如果有人聊到关于ABAP能否进行面向切面编程的话题,您或许可以提到Jerry这篇文章。感谢阅读。
要获取更多Jerry的原创文章,请关注公众号"汪子熙":
ABAP专题
Jerry的ABAP, Java和JavaScript乱炖
ABAP开发人员未来应该学些什么
Jerry 2017年的五一小长假:8种经典排序算法的ABAP实现
Jerry的ABAP原创技术文章合集
300行ABAP代码实现一个最简单的区块链原型
使用Java+SAP云平台+SAP Cloud Connector调用ABAP On-Premise系统里的函数
在SAP云平台的CloudFoundry环境下消费ABAP On-Premise OData服务
ABAP vs Java, 蛙泳 vs 自由泳
聊聊C语言和ABAP
动手使用ABAP Channel开发一些小工具,提升日常工作效率
我用ABAP做过的那些无聊的事情
不喜欢SAP GUI?那试试用Eclipse进行ABAP开发吧
使用Visual Studio Code编写和激活ABAP代码
你的ABAP程序给佛祖开过光么?来试试Jerry这个小技巧
在SAP云平台ABAP编程环境上编写第一段ABAP程序
SAP官方发布的ABAP编程规范
ABAP Code Inspector那些隐藏的功能,您都知道吗?
还在用ABAP进行SAP产品的二次开发?来了解下这种全新的二次开发理念吧
ABAP Netweaver体内的那些寄生式编程语言
从SAP社区上的一篇博客开始,聊聊SAP产品命名背后的那份情怀
云端的ABAP Restful服务开发
如何在SAP云平台ABAP编程环境里把CDS view暴露成OData服务
使用abapGit在ABAP On-Premises系统和SAP云平台ABAP环境之间进行代码传输
30分钟用Restful ABAP Programming模型开发一个支持增删改查的Fiori应用
Jerry带您了解Restful ABAP Programming模型系列之二:Action和Validation的实现
Jerry带您了解Restful ABAP Programming模型系列之三:云端ABAP应用调试
SAP云平台上的ABAP编程环境里如何消费第三方服务
ABAP开发者上云的时候到了 - 现在大家可以免费使用SAP云平台ABAP环境的试用版了
学而不思则罔 - SAP云平台ABAP编程环境的由来和适用场景
SAP云平台里的三叉戟应用
如何基于Restful ABAP Programming模型开发并部署一个支持增删改查的Fiori应用
SAP 2019 TechEd Key Note解读:云时代下SAP从业人员如何做二次开发?
有哪些ABAP关键字和语法,到了ABAP云环境上就没办法用了?
ABAP开发环境终于支持以驼峰命名法自动格式化ABAP变量名了
利用ABAP 740的新关键字REDUCE完成一个实际工作任务
一段让人瑟瑟发抖的ABAP代码
昨日万圣节ABAP怪兽级代码谜团,公布答案啦
介绍一种在ABAP内核态进行内表高效拷贝的方法
使用SAP Cloud Application Programming模型开发OData的一个实际例子
当ABAP遇见普罗米修斯
使用ABAP绘制可伸缩矢量图
ABAP开发环境语法高亮的那些事儿
SAP错误消息调试之七种武器:让所有的错误消息都能被定位
使用ABAP操作Excel的几种方法
SAP GUI里的收藏夹事务码管理工具
SAP GUI和Windows注册表
有了Debug权限就能干坏事?小心了,你的一举一动尽在系统监控中
ABAP CCDEF, CCIMP, CCMAC, CCAU, CMXXX这些东东是什么鬼
实现ABAP条件断点的三种方式
使用SAT跟踪监控从浏览器打开的SAP应用的性能和调用栈
一个13年ABAP老兵的建议:了解这些基础知识,对ABAP开发有百利而无一害
SAP ABAP Netweaver容器化, 不可能完成的任务吗?
SAP产品增强技术回顾
SAP API开发方法大全