淺談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),將業務代碼和非業務代碼(好比權限檢查,日誌記錄,性能統計等)完全分離開。java

下圖是某應用裏方法的常規實現:權限檢查,日誌記錄和性能檢測的代碼一次又一次地侵入到本應只包含業務代碼的三個方法中:算法

下圖是應用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社區博客上有詳細敘述:

本文提到的Jerry開發的全部ABAP原型和工具,在這個連接裏有源代碼。

從此若是有人聊到關於ABAP可否進行面向切面編程的話題,您或許能夠提到Jerry這篇文章。感謝閱讀。

要獲取更多Jerry的原創文章,請關注公衆號"汪子熙":

ABAP專題

相關文章
相關標籤/搜索