天天看點

淺談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和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

下圖是應用AOP之後的方法實作:三個方法體内隻包含純粹的業務代碼,看起來清爽了很多。權限檢查,日志記錄和性能檢測的代碼,作為仍需關注的三個方面,以切面的方式編織到三個方法中。Weave,AOP裡的術語,中文材料裡經常譯成“編織”,描述了被代理類的方法通過非源代碼修改層面被增添以新邏輯的動作。

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

我們說面向對象程式設計(Object Oriented Programming,簡稱OOP)是一種理念,不同的程式設計語言可以有不同的實作。同理,AOP這種理念,不同的程式設計語言也存在不同的實作。

Java AOP的實作可以分為靜态代理和動态代理兩種。無論哪種代理方式,一言以蔽之,AOP的核心為,業務邏輯位于原始類中始終保持不變,而編織的非業務邏輯位于代理類中。運作時執行的代碼,實際上被調用的是代理類,原始類的業務邏輯通過代理類被間接地調用。

代理模式的UML圖:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

業務邏輯在編譯期間被編織進入代理類的方式,稱為靜态代理;業務邏輯在運作期間才進行編織的方式,稱為動态代理。準确地說,編譯期編織還可細分為編譯時和編譯後編織,而運作期間編織又可細分為載入時編織和運作時編織,但這種細分方式不影響本文接下來的闡述,是以後續仍隻按照編譯期和運作期兩大類來介紹。

看一些具體的例子。

Java靜态代理

定義一個IDeveloper的接口,裡面包含一個writeCode的方法。建立一個Developer類,實作該方法。

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

測試:建立一個名為Jerry的Developer執行個體,調用writeCode方法。

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

假設我想讓Developer在寫代碼之前,先編寫對應的文檔,但我不想把寫文檔這個邏輯,侵入到writeCode方法裡。這裡“編寫文檔”,就相當于待編織的非業務邏輯,或者叫做待編織的切面邏輯。

使用靜态代理的思路,另外建立一個代理類DeveloperProxy:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

注意上圖的writeCode方法,首先第8行完成文檔編寫的任務,然後代理類在第9行調用被代理類Developer的writeCode方法,完成寫代碼的實際業務邏輯。

測試代碼:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

Developer和DeveloperProxy都實作了同一個接口IDeveloper,對于消費者代碼來說,它完全感覺不到也不必要去感覺這兩個接口實作類的内部差異——這一切對消費者代碼來說完全透明。消費者拿到的引入,指向的是類型為IDeveloper接口的變量,然後調用定義在接口上的writeCode方法即可。

從以上例子可以看出,靜态代理工作的基石是接口,如果原始類由于某種原因,無法改造成為某個接口的實作類(比如原始類來自系統遺留代碼,無法重構),則靜态代理這條路行不通。

針對每個原始類,采用靜态代理,都需要建立一個具有持久存儲的代理類。這種方式便于了解,并且非業務邏輯(前例中的“寫文檔”行為)在編譯期間植入靜态代理類,實際運作時性能優于即将介紹的動态代理。

在Java裡如果不想手動建立靜态代理類,可以使用工具AspectJ來自動完成。由于本文的讀者主要是ABAP開發人員,這裡略過其使用方式。

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

ABAP靜态代理類的自動建立

我仿照Java AspectJ的思路,用ABAP寫了一個類似的原型。下面是使用方法。

首先我建立一個類CL_HELLOWORLD:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

我想自動為該類建立一個靜态代理,在代理類的PRINT方法裡,除了調用這個原始類的PRINT方法外,再做一些額外的邏輯,比如列印一些輸出。

調用下圖的GET_PROXY方法,将自動為CL_HELLOWORLD建立一個靜态代理類,将第7行和第8行指定的額外邏輯編織到靜态代理類的PRINT方法裡:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

測試:調用靜态代理類的PRINT方法,得到下圖的輸出,能觀察到編織到靜态代理類的兩行WRITE語句,分别在原始類PRINT方法之前和之後被調用了:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

SE24可以觀察到,通過我寫的工具自動建立的ABAP靜态類,及編織到代理類方法PRINT裡的額外邏輯:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

這個工具的核心是調用ABAP Class API生成新的ABAP類,源代碼可以在文末Jerry提供的連結裡獲得:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

所謂動态代理,即AOP架構在編譯期不會對原始類做任何處理,而是直到應用運作期間,在記憶體中臨時為需要被代理的類生成一個AOP對象,該對象包含了原始類的全部方法,并且在被代理的方法處做了增強處理,編織入新的邏輯,并回調原始類的方法。

Spring AOP動态代理有兩種實作方式:JDK動态代理和CGLIB動态代理。

JDK動态代理

JDK動态代理的原理是基于Java反射機制實作的方法攔截器機制。

我們在第一個例子的基礎上,增添一個新的ITester接口,代表測試人員這個崗位:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

現在的需求是給測試人員的doTesting方法内也植入編寫文檔的邏輯。如果采用靜态代理的方式,我們得又建立一個TesterProxy的靜态代理類。随着開發小組裡人員崗位類型的增加,這些靜态代理類的個數也随之增加。

那麼用動态代理如何優雅地避免這個問題呢?

建立一個新的代理類,取名為EnginnerProxy,名字暗示了這個實作了JDK标準接口InnovationHandler的類,在運作時能統一代理一個軟體開發團隊裡所有角色的工程師類的方法。

第七行的bind方法,接收一個被代理類的執行個體,在運作時動态為該執行個體建立一個臨時的代理類執行個體。所謂臨時,指該代理執行個體的生命周期隻存在于目前會話中,應用運作結束後即銷毀,不會像靜态代理類那樣會持久化存儲。

運作時代理類的方法一旦執行,無論是Developer的writeCode, 還是Tester的doTesting方法,均會被EnginnerProxy的invoke方法攔截,在invoke方法内統一執行第17行的文檔撰寫邏輯,然後再調用18行包含了業務邏輯的原始類方法。

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

下圖是測試代碼及運作結果,現在無論是Developer還是Tester,在寫代碼和做測試之前,都會自動執行文檔撰寫的任務了:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

基于JDK動态代理的優缺點

顯而易見,在需要代理多個類時,動态代理隻需建立一個統一的代理類,而不必像靜态代理那樣,需要為每個包含業務邏輯的類單獨建立代理類。而代理類“用後即焚”,也避免了在工程檔案夾裡生成太多代理類。

另一方面,因為動态代理在運作時通過Java反射機制實作,運作時的性能劣于在編譯期間進行代理邏輯編織的靜态代理。此外,JDK動态代理工作的前提條件同靜态代理一樣,也需要被代理的類實作某個接口。

看個反例,假設産品經理類ProductOwner未實作任何接口:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

使用JDK動态代理,在運作時會抛ClassCastException異常:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

正因為JDK動态代理的這種局限性,存在另一種動态代理的實作方式:基于CGLIB的動态代理。

CGLIB(Code Generation Library)是一個Java位元組碼生成庫,可以在運作時對Java類的位元組碼進行處理和增強,底層基于位元組碼處理架構ASM實作。

基于CGLIB的動态代理可以繞過JDK動态代理的限制,即使一個需要被代理的類沒有實作任何接口,也能使用CGLIB動态代理。

注意這次使用CGLIB建立的統一代理類,導入的開發包來自net.sf.cglib.proxy, 而非JDK動态代了解決方案中的java.lang.reflect:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

消費代碼的風格同JDK動态代理類似:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

CGLIB克服了JDK動态代理需要被代理類必須實作某個接口才能工作的限制,然而其本身也有局限性。CGLIB本質上是運作時用API操作Java類的位元組碼的方式,直接建立一個繼承自被代理類的子類,然後将切面邏輯編織到這個子類方法中去。顯而易見,如果被代理類被定義成無法繼承,比如被Java和ABAP裡的final關鍵字修飾,則CGLIB動态代理這種方式也無法工作。

做一個測試,我将ProductOwner類标志為final,即無法被繼承,這時在運作之前的測試代碼,會遇到異常和錯誤消息:Cannot subclass final class

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

ABAP動态代理

因為ABAP無法在語言層面精确做到像Java JDK InnovationHandler那樣能夠用一個代理類統一攔截多個被代理類方法執行的效果,是以Jerry選擇對另一種動态代理,即CGLIB代理方式,用ABAP進行模拟。

首先建立一個需要被代理的類,業務邏輯寫在GREET方法裡。

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

接着使用Jerry自己實作的ABAP CGLIB工具類,通過其方法GET_PPROXY得到這個類的代理類,并調用代理類的GREET方法:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

上圖第8行和第9行是包含了兩個切面邏輯的類,我期望其方法分别在被代理類的GREET調用之前和調用之後被執行。

ABAP CGLIB的核心在GET_PROXY方法裡的generate_proxy方法内:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

這裡使用了ABAP動态生成類的關鍵字GENERATE SUBROUTINE POOL, 根據内表mt_source裡包含的預先拼湊好的源代碼,生成新的臨時類。這個類不會在SE24或者SE80裡存儲,僅僅存活在目前應用的會話裡。

第17行動态生成新的代理類之後,第21行生成一個該代理類的執行個體,然後在第23和26行分别植入切面邏輯。

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

最後調用這個代理類執行個體的GREET方法,列印輸出如下:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

其中Hello World是原始被代理類即ZCL_JAVA_CGLIB的GREET方法的輸出,而它的前後兩行為調用ABAP CGLIB生成代理類時傳入的切面邏輯。

到目前為止,盡管我們意識到靜态代理和動态代理都各自存在一些缺陷,但從這些缺陷出現的原因,也再次提醒我們,在編寫新的代碼時,要盡量面向接口程式設計,盡量避免直接面向實作程式設計,進而降低程式的耦合性,提高應用的可維護性,可複用性和可擴充性。

以上介紹的ABAP CGLIB工具隻是Jerry開發的一個原型,在ABAP裡如果僅僅想将切面邏輯(比如權限檢查,日志記錄,性能分析)徹底地同業務邏輯隔離開,可以使用ABAP Netweaver提供的對類方法增強的标準方式:Pre-Exit和Post-Exit.

選中要增強的類,點選Enhance菜單:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

這種增強和被代理的類是分開存儲的:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

建立新的Pre-Exit:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

點選Pre-Exit的面闆,就可以進去編寫代碼了:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

在運作時,被代理類ZCL_JAVA_CGLIB的GREET方法執行之前,Pre-Exit裡的代碼會自動觸發:

淺談Java和SAP ABAP的靜态代理和動态代理,以及ABAP面向切面程式設計的嘗試

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這篇文章。感謝閱讀。