本文內容
- 實例
- 引入
- 原始方法
- 裝飾者模式
- 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 動態代理和 cglib 代理
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 框架——AspectWerkz
利用 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 作為配置文件。