Jerry以前一篇文章 SAP产品加强技术回顾,提到基于Java编程语言实现的SAP Commerce,借助Spring框架的支持,能使用面向切面编程的理念(Aspect Orient Programming,如下简称AOP),将业务代码和非业务代码(好比权限检查,日志记录,性能统计等)完全分离开。java
下图是某应用里方法的常规实现:权限检查,日志记录和性能检测的代码一次又一次地侵入到本应只包含业务代码的三个方法中:算法
下图是应用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社区博客上有详细叙述:
本文提到的Jerry开发的全部ABAP原型和工具,在这个连接里有源代码。
从此若是有人聊到关于ABAP可否进行面向切面编程的话题,您或许能够提到Jerry这篇文章。感谢阅读。
要获取更多Jerry的原创文章,请关注公众号"汪子熙":
ABAP专题