天天看點

Java 面向切面程式設計(Aspect Oriented Programming,AOP) 下載下傳 demo

本文内容

  • 執行個體
    • 引入
    • 原始方法
    • 裝飾者模式
  • JDK 動态代理和 cglib 代理
  • 直接使用 AOP 架構——AspectWerkz

最近跳槽了,新公司使用了 AOP 相關的技術,于是查點資料,複習一下。之前,多少知道點,但沒怎麼在實際項目中使用過~

下載下傳 demo

package com.cap.aop;      
public interface ICalculator {      
public double add(double num1, double num2) throws Exception;      
public double sub(double num1, double num2) throws Exception;      
public double div(double num1, double num2) throws Exception;      
public double mul(double num1, double num2) throws Exception;      
}      
package com.cap.aop;      
/**      
* 加減乘除      
* */      
public class Calculator implements ICalculator {      
@Override      
public double add(double num1, double num2) {      
double result = num1 + num2;      
return result;      
}      
@Override      
public double sub(double num1, double num2) {      
double result = num1 - num2;      
return result;      
}      
@Override      
public double div(double num1, double num2) {      
double result = num1 / num2;      
return result;      
}      
@Override      
public double mul(double num1, double num2) {      
double result = num1 * num2;      
return result;      
}      
}      

定義 ICalculator 接口和 Calculator 類,并且 Calculator 也繼承 ICalculator。

若要為這個類添加“日志”功能該如何做?日志在實際項目中很有必要,比如資料庫日志,業務日志等等,通過日志就能知道資料庫和業務存在的問題,這要比調試程式容易多了,此外還有性能統計,安全控制,事務處理,異常處理等等都是類似問題。

我們最可能想到的是,在類的每個方法内都寫日志相關的代碼,或是在該類的基類中寫,在其子類中繼承。

package com.cap.aop;      
/**      
* 加減乘除,原始方式      
* */      
public class CalculatorOriginalWay implements ICalculator {      
@Override      
public double add(double num1, double num2) {      
System.out.println("the method [add()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = num1 + num2;      
System.out.println("the method [add()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double sub(double num1, double num2) {      
System.out.println("the method [sub()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = num1 - num2;      
System.out.println("the method [sub()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double div(double num1, double num2) {      
System.out.println("the method [div()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = num1 / num2;      
System.out.println("the method [div()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double mul(double num1, double num2) {      
System.out.println("the method [mul()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = num1 * num2;      
System.out.println("the method [mul()]" + "end with result (" + result      
+ ")");      
return result;      
}      
}      

這樣做的缺點顯而易見,重複代碼太多,耦合也不好。要是該類隻針對正數運算呢,還要對變量做檢查,代碼如下所示:

package com.cap.aop;      
/**      
* 加減乘除,隻對正數      
* */      
public class CalculatorForPositiveNumber implements ICalculator {      
@Override      
public double add(double num1, double num2) throws Exception {      
this.argsValidatior(num1);      
this.argsValidatior(num2);      
System.out.println("the method [add()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = num1 + num2;      
System.out.println("the method [add()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double sub(double num1, double num2) throws Exception {      
this.argsValidatior(num1);      
this.argsValidatior(num2);      
System.out.println("the method [sub()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = num1 - num2;      
System.out.println("the method [sub()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double div(double num1, double num2) throws Exception {      
this.argsValidatior(num1);      
this.argsValidatior(num2);      
System.out.println("the method [div()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = num1 / num2;      
System.out.println("the method [div()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double mul(double num1, double num2) throws Exception {      
this.argsValidatior(num1);      
this.argsValidatior(num2);      
System.out.println("the method [mul()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = num1 * num2;      
System.out.println("the method [mul()]" + "end with result (" + result      
+ ")");      
return result;      
}      
private void argsValidatior(double arg) throws Exception {      
if (arg < 0)      
throw new Exception("參數不能為負數");      
}      
}      

這也僅僅是一個類而已,實際項目中那麼多類,要是都這麼幹,顯然不現實,那麼如何改進?——設計模式“裝飾者模式”,在不必改變原類檔案和繼承的情況下,動态地擴充一個對象的功能。

裝飾者方法

package com.cap.aop;      
/**      
* 加減乘除,裝飾者模式      
*/      
public class CalculatorDecorator implements ICalculator {      
private ICalculator cal;      
public CalculatorDecorator(ICalculator iCalculator) {      
cal = iCalculator;      
}      
@Override      
public double add(double num1, double num2) throws Exception {      
System.out.println("the method [add()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = cal.add(num1, num2);      
System.out.println("the method [add()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double sub(double num1, double num2) throws Exception {      
System.out.println("the method [sub()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = cal.sub(num1, num2);      
System.out.println("the method [sub()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double div(double num1, double num2) throws Exception {      
System.out.println("the method [div()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = cal.div(num1, num2);      
System.out.println("the method [div()]" + "end with result (" + result      
+ ")");      
return result;      
}      
@Override      
public double mul(double num1, double num2) throws Exception {      
System.out.println("the method [mul()]" + "begin with args (" + num1      
+ "," + num2 + ")");      
double result = cal.mul(num1, num2);      
System.out.println("the method [mul()]" + "end with result (" + result      
+ ")");      
return result;      
}      
}      

這個方法比上面的強點,用原來的類 Calculator 建立一個新類 CalculatorDecorator,也繼承 ICalculator 接口,這樣,在對原來的類不做任何修改的情況下,在新類中添加日志功能。但該方法也有弱點,需要為每個類都應用“裝飾者模式”,工作量也不小。如何解決?——代理。

JDK 從 1.3 版本開始,引入了動态代理。JDK 動态代理非常簡單,但動态代理的對象必須是一個或多個接口。

若想代理類,就需要使用 cglib 包。cglib 是一個強大的、高性能的代碼生成包,cglib 包的底層是通過使用一個小而快的位元組碼處理架構 ASM,來轉換位元組碼并生成新的類,cglib 被許多 AOP 架構使用,例如 Spring 的 AOP;Hibernate 使用 cglib 來代理單端 single-ended(多對一和一對一)關聯;EasyMock 通過使用模仿(moke)對象來測試 java 包……它們都通過 cglib 來為那些沒有接口的類建立模仿(moke)對象。

JDK 動态代理

package com.cap.aop;      
import java.lang.reflect.InvocationHandler;      
import java.lang.reflect.Method;      
import java.lang.reflect.Proxy;      
import java.util.Arrays;      
/**      
* 加減乘除,JDK 代理<br/>      
* 隻能代理接口,不能代理類      
*       
* */      
public class CalculatorInvocationHandler implements InvocationHandler {      
// 動态代理隻有在運作時才知道代理誰,是以定義為Object類型      
private Object target = null;      
/**      
* 通過構造函數傳入原對象      
*       
* @param target      
*            要代理的對象      
*/      
public CalculatorInvocationHandler(Object target) {      
this.target = target;      
}      
/**      
* InvocationHandler 接口的 invoke 方法,調用代理的方法      
*       
* @param proxy在其上調用方法的代理執行個體      
* @param method攔截的方法      
* @param args攔截的參數      
* */      
@Override      
public Object invoke(Object proxy, Method method, Object[] args)      
throws Throwable {      
System.out.println("JDK proxy...");      
// 日志開始      
System.out.println("the method [" + method.getName() + "]"      
+ "begin with args (" + Arrays.toString(args) + ")");      
Object result = method.invoke(this.target, args);      
// 日志結束      
System.out.println("the method [" + method.getName() + "]"      
+ "end with result (" + result + ")");      
return result;      
}      
/**      
* 擷取代理類      
*/      
public Object getProxy() {      
return Proxy.newProxyInstance(target.getClass().getClassLoader(),      
target.getClass().getInterfaces(),      
new CalculatorInvocationHandler(target));      
}      
}      

cglib 代理

package com.cap.aop;      
import java.lang.reflect.Method;      
import java.util.Arrays;      
import net.sf.cglib.proxy.Enhancer;      
import net.sf.cglib.proxy.MethodInterceptor;      
import net.sf.cglib.proxy.MethodProxy;      
/**      
* 加減乘除,cglib 代理<br/>      
* 能代理接口和類,不能代理final類      
*/      
public class CalculatorInterceptor implements MethodInterceptor {      
private Object target;      
public CalculatorInterceptor(Object target) {      
this.target = target;      
}      
@Override      
public Object intercept(Object proxy, Method method, Object[] args,      
MethodProxy invocation) throws Throwable {      
System.out.println("cglib proxy...");      
// 日志開始      
System.out.println("the method [" + method.getName() + "]"      
+ "begin with args (" + Arrays.toString(args) + ")");      
Object result = invocation.invoke(target, args);      
// 日志結束      
System.out.println("the method [" + method.getName() + "]"      
+ "end with result (" + result + ")");      
return result;      
}      
public Object proxy() {      
return Enhancer.create(target.getClass(), new CalculatorInterceptor(      
target));      
}      
}      

測試主程式,代碼如下所示:

package com.cap.aop;      
public class Client {      
public static void main(String[] args) throws Exception {      
ICalculator calculatorProxy = (ICalculator) new CalculatorInvocationHandler(      
new Calculator()).getProxy();      
calculatorProxy.add(10, 10);      
Calculator calculator = (Calculator) new CalculatorInterceptor(      
new Calculator()).proxy();      
calculator.add(20, 20);      
}      
}      

運作結果:

JDK proxy...      
the method [add]begin with args ([10.0, 10.0])      
the method [add]end with result (20.0)      
cglib proxy...      
the method [add]begin with args ([20.0, 20.0])      
the method [add]end with result (40.0)      

利用 AOP 架構,你隻需要利用注釋和 Aspect 就可以完成上面的所有工作。

AOP,Aspect Oriented Programming,稱為“面向切面程式設計”,AOP 是 OOD/OOP 和 GoF 的延續,GoF 孜孜不倦的追求是調用者和被調用者之間的解耦,提高代碼的靈活性和可擴充性,AOP 的目标也是一樣。

AOP 是一種通過預編譯和運作時動态代理實作在不修改源代碼的情況下給程式動态統一添加功能的技術。利用 AOP 可以對業務邏輯的各個部分進行隔離,進而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發效率。在 Spring 中,通過分離應用的業務邏輯與系統級服務,應用對象隻完成業務邏輯,不負責(甚至是意識)其它的系統級關注點,例如日志、事務、審計等等。

本文最開始的“日志功能”,就是一個切面。

AOP 主要應用在日志記錄,性能統計,安全控制,事務處理,異常處理等等,将它們從業務邏輯代碼中分離,将它們獨立到非業務邏輯的方法中,進而在改變這些行為時不影響業務邏輯的代碼。

AOP 與 OOP/OOD

AOP(面向切面程式設計)與 OOP(面向對象程式設計)字面上雖然類似,但卻是面向不同領域的兩種設計思想。

OOP 針對業務中的實體及其屬性和行為進行抽象封裝,以便劃分邏輯單元。而 AOP 則是針對業務中的“切面”進行提取,它面對的是處理過程中的某個步驟或階段,以獲得各部分之間低耦合性的隔離效果。是以,這兩種設計思想有着本質的差異。

簡單來說,對“雇員”這個業務實體進行封裝,是 OOP 的任務,我們可以建立一個“Employee”類,并将“雇員”相關的屬性和行為封裝其中。而用 AOP 對“雇員”進行封裝将無從談起;權限檢查也是如此,它是 AOP 的領域。

換而言之,OOD/OOP 面向名詞領域,AOP 面向動詞領域。

很多人在初次接觸 AOP 的時候可能會說,AOP 能做到的,一個定義良好的 OOP 接口也能,這個觀點是值得商榷。AOP 和定義良好的 OOP 可以說都是用來解決并且實作需求中的橫切問題。但對于 OOP 中的接口來說,它仍然需要我們在相應的子產品中去調用該接口中相關的方法,這是 OOP 所無法回避的,并且一旦接口不得不進行修改的時候,所有事情會變得一團糟;AOP 則不會,你隻需要修改相應的 Aspect,再重新 weave(編織)即可。 AOP 也絕對不會取代 OOP。核心的需求仍然會由 OOP 來加以實作,而 AOP 将會和 OOP 整合起來,揚長避短。

AOP 涉及的概念

下面概念在 AspectWerkz 的注釋中都有所展現。

  • Aspect: Aspect 聲明類似于 Java 中的類聲明,在 Aspect 中會包含着一些 Pointcut 以及相應的 Advice。
  • Joint point:表示在程式中明确定義的點,典型的包括方法調用,對類成員的通路以及異常處理程式塊的執行等等,它自身還可以嵌套其它 joint point。
  • Pointcut:表示一組 joint point,這些 joint point 或是通過邏輯關系組合起來,或是通過通配、正規表達式等方式集中起來,它定義了相應的 Advice 将要發生的地方。
  • Advice:Advice 定義了在 pointcut 裡面定義的程式點具體要做的操作,它通過 before、after 和 around 來差別是在每個 joint point 之前、之後還是代替執行的代碼。

Java 的 AOP 架構

  • AspectWerkz 是簡單、動态、輕量級、強大的 AOP 架構,更容易的內建 AOP 的項目中。
  • JBoss AOP 是 JBoss 4.0 帶了一個 AOP 架構,但也能夠在你的應用中單獨的運作它。
  • Nanning。Nanning Aspects is a simple yet scaleable aspect-oriented framework for Java. Nanning is also nice "little" town in Guanxi province in southern China. It's about 1-2 million inhabitants which barely qualifies it as a town by chinese standards. It is not to be confused with the much larger Nanking/Nanjing.
  • JAC,Java Aspect Components 是一個應用伺服器。它為 Java 2 平台、J2EE和基于 Web 的分布式應用,提供開放式資源的又一個選擇。JAC 包括統一模型語言(UML)IDE,該 IDE 子產品化應用商業邏輯,并自動生成和編譯純商業邏輯 Java 類。這些類,在 JAC 容器内執行,可從一組技術和/或商業的橫切關系 (crosscutting concerns),如資料持久性、認證、配置檔案管理、通路權限檢測、示範和負載平衡中無縫地受益。基于 AOP 的 JAC 将這些關系(concerns)從應用程式的核心商業邏輯中分離出來。
  • DynamicAspects。This project is no longer maintained! DynamicAspects enables you to do aspect-oriented programming in pure Java. Using the "instrumentation" and "agent" features introduced with Sun JDK 1.5, aspects can be installed and deinstalled during runtime!
  • CAESAR。CaesarJ is a new Java based programming language, which facilitates better modularity and development of reusable components. The components are collaborations of classes, but they can modularize crosscutting features or non-functional concerns. Caesar language features help to implement, abstract and integrate such components. Caesar can be used in combination with plain Java. Tool support is available in the form of an Eclipse plugin.
  • PROSE 是一個動态編排(weaving)工具,允許在運作時插入或抽取 aspects。PROSE aspects 是規則的 Java 對象能夠被發送到或從網絡上的計算機接收。簽名可被用于保證它們的完整性。一旦一個 aspect 插入到 JVM 中,任何事件的發生将影響在相應 aspect advice 執行的結果。假如一個 aspect 從 JVM 中撤消,aspect 代碼将被丢棄并且相應的攔截也将不會再發生。
  • FastAOP。FastAOP is an very high performant AOP (Aspect Oriented Programming) framework for java. The framework was initially

    developped to support performance profiling and monitoring for large J2EE applications with nearly no runntime overhad.

.Net 的 AOP 架構

  • Encase 是 C# 編寫開發的為 .NET 平台提供的 AOP 架構。Encase 獨特的提供了把方面(aspects)部署到運作時代碼,其它 AOP 架構依賴配置檔案的方式。這種部署方面(aspects)的方法幫助缺少經驗的開發人員提高開發效率。
  • NKalore 是這款程式設計語言,擴充了 C#,允許在 .NET 平台使用 AOP。NKalore 的文法簡單、直覺,編譯器是基于 Mono C#編譯器(MCS)。NKalore 目前隻能在指令行或 #Develop 内部使用。NKalore 相容公共語言規範(Common Language Specification,CLS),可以在任何 .NET 開發環境中使用,包括微軟的 Visual Studio .NET。
  • PostSharp 讀取 .NET 位元組子產品,轉換成對象模型。讓插件分析和轉換這個模型并寫回到MSIL。PostSharp 使開發程式分析應用程式容易得像分析代碼規則和設計模式,它使程式開發的思想變革為面向方面軟體開發(AOSD/AOD)思想。
  • AspectDNG 的目标是為 .NET 開發人員提供簡單而功能強大的 AOP-GAOP 實作。它效仿 java 下的開源工具 AspectJ 和 Spoon,成熟程度也很接近它們。
  • RAIL,Runtime Assembly Instrumentation Library,開源項目可以在 C# 程式集加載和運作前進行處理控制調整和重新建構。C#在 CLR 中,我們已經能夠動态加載程式集并且獲得程式集中的類和方法,RAIL(Runtime Assembly Instrumentation Library)的出現填補了CLR處理過程中的一些空白。
  • SetPoint 是一款 .NET 架構下的全功能 AOP 引擎,它着重為稱為語義切點(semantic pointcuts)的定義依賴 RDF/OWL 的使用,它的功能為一個 IL-level,highly dynamic weaver&LENDL,,一個引人注目的定義語言 DotNetAOP 為 CLR language 提供 AOP 架構基礎屬性。
  • NAop 是一個 .NET 下的 AOP 架構。
  • AspectSharp 是 .NET 下的免費 AOP架構,它以 Dynamic Proxies 和 XML 作為配置檔案。