1. 程式人生 > >面向切面編程

面向切面編程

沒有 ida rgs 容器 簡潔 action src 處理 業務邏輯

在傳統的編寫業務邏輯處理代碼時,我們通常會習慣性地做幾件事情:日誌記錄、事務控制及權限控制等,然後才是編寫核心的業務邏輯處理代碼。當代碼編寫完成回頭再看時,不禁發現,揚揚灑灑上百行代碼中,真正用於核心業務邏輯處理才那麽幾行,如圖6-4所示。方法復方法,類復類,就這樣子帶著無可奈何遺憾地度過了多少個春秋。這倒也罷,倘若到了項目的尾聲,突然決定在權限控制上需要進行大的變動時,成千上萬個方法又得一一"登門拜訪",痛苦"雪上加霜"。

技術分享

如果能把圖6-4中眾多方法中的所有共有代碼全部抽取出來,放置到某個地方集中管理,然後在具體運行時,再由容器動態織入這些共有代碼的話,最起碼可以解決兩個問題:

Java EE程序員在編寫具體的業務邏輯處理方法時,只需關心核心的業務邏輯處理,既提高了工作效率,又使代碼變更簡潔優雅。

在日後的維護中由於業務邏輯代碼與共有代碼分開存放,而且共有代碼是集中存放的,因此使維護工作變得簡單輕松。

面向切面編程AOP技術就是為解決這個問題而誕生的,切面就是橫切面,如圖6-5所示,代表的是一個普遍存在的共有功能,例如,日誌切面、權限切面及事務切面等。

技術分享

下面我們以用戶管理業務邏輯組件UserService的AOP實現過程(見圖6-6)為例,深度剖析一下AOP技術的實現原理。AOP技術是建立在Java語言的反射機制與動態代理機制之上的。業務邏輯組件在運行過程中,AOP容器會動態創建一個代理對象供使用者調用,該代理對象已經按Java EE程序員的意圖將切面成功切入到目標方法的連接點上,從而使切面的功能與業務邏輯的功能同時得以執行。從原理上講,調用者直接調用的其實是AOP容器動態生成的代理對象,再由代理對象調用目標對象完成原始的業務邏輯處理,而代理對象則已經將切面與業務邏輯方法進行了合成。

技術分享

現將圖6-6中涉及到的一些概念解釋如下。

切面(Aspect):其實就是共有功能的實現。如日誌切面、權限切面、事務切面等。在實際應用中通常是一個存放共有功能實現的普通Java類,之所以能被AOP容器識別成切面,是在配置中指定的。

通知(Advice):是切面的具體實現。以目標方法為參照點,根據放置的地方不同,可分為前置通知(Before)、後置通知(AfterReturning)、異常通知(AfterThrowing)、最終通知(After)與環繞通知(Around)5種。在實際應用中通常是切面類中的一個方法,具體屬於哪類通知,同樣是在配置中指定的。

連接點(Joinpoint):就是程序在運行過程中能夠插入切面的地點。例如,方法調用、異常拋出或字段修改等,但Spring只支持方法級的連接點。

切入點(Pointcut):用於定義通知應該切入到哪些連接點上。不同的通知通常需要切入到不同的連接點上,這種精準的匹配是由切入點的正則表達式來定義的。

目標對象(Target):就是那些即將切入切面的對象,也就是那些被通知的對象。這些對象中已經只剩下幹幹凈凈的核心業務邏輯代碼了,所有的共有功能代碼等待AOP容器的切入。

代理對象(Proxy):將通知應用到目標對象之後被動態創建的對象。可以簡單地理解為,代理對象的功能等於目標對象的核心業務邏輯功能加上共有功能。代理對象對於使用者而言是透明的,是程序運行過程中的產物。

織入(Weaving):將切面應用到目標對象從而創建一個新的代理對象的過程。這個過程可以發生在編譯期、類裝載期及運行期,當然不同的發生點有著不同的前提條件。譬如發生在編譯期的話,就要求有一個支持這種AOP實現的特殊編譯器;發生在類裝載期,就要求有一個支持AOP實現的特殊類裝載器;只有發生在運行期,則可直接通過Java語言的反射機制與動態代理機制來動態實現。

 AOP(Aspect-Oriented Programming,面向切面的編程),它是可以通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的一種技術。它是一種新的方法論,它是對傳統OOP編程的一種補充。

  OOP是關註將需求功能劃分為不同的並且相對獨立,封裝良好的類,並讓它們有著屬於自己的行為,依靠繼承和多態等來定義彼此的關系;AOP是希望能夠將通用需求功能從不相關的類當中分離出來,能夠使得很多類共享一個行為,一旦發生變化,不必修改很多類,而只需要修改這個行為即可。

  AOP是使用切面(aspect)將橫切關註點模塊化,OOP是使用類將狀態和行為模塊化。在OOP的世界中,程序都是通過類和接口組織的,使用它們實現程序的核心業務邏輯是十分合適。但是對於實現橫切關註點(跨越應用程序多個模塊的功能需求)則十分吃力,比如日誌記錄,驗證。

技術分享
/*計算器接口*/
public interface Calculator
{
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;
}
技術分享 技術分享
/*計算器接口的實現類*/
public class ArithmeticCalculator implements Calculator
{
@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;
}

/*示意代碼 暫時不考慮除數0的情況*/
@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;
}
}
技術分享

大多數應用程序都有一個通用的需求,即在程序運行期間追蹤正在發生的活動。為了給計算機添加日誌功能,ArithmeticCalculator類改變如下:

技術分享
/*計算器接口的實現類,添加記錄日誌功能*/
public class ArithmeticCalculator implements Calculator
{
@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;
}

/*示意代碼 暫時不考慮除數0的情況*/
@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;
}
}
技術分享

若ArithmeticCalculator規定只能計算正數時,又需要添加參數驗證方法:

技術分享
/*計算器接口的實現類,添加記錄日誌功能*/
public class ArithmeticCalculator implements Calculator
{
@Override
public double add(double num1, double num2) throws Exception
{
this.argsValidatior(num1);
this.argsValidatior(num2);

/*同上*/
}

@Override
public double sub(double num1, double num2) throws Exception
{
this.argsValidatior(num1);
this.argsValidatior(num2);

/*同上*/
}

/*示意代碼 暫時不考慮除數0的情況*/
@Override
public double div(double num1, double num2) throws Exception
{
this.argsValidatior(num1);
this.argsValidatior(num2);

/*同上*/
}

@Override
public double mul(double num1, double num2) throws Exception
{
this.argsValidatior(num1);
this.argsValidatior(num2);

/*同上*/
}

private void argsValidatior(double arg)throws Exception
{
if(arg < 0)
throw new Exception("參數不能為負數");
}
}
技術分享

  上面的程序一個很直觀的特點就是,好多重復的代碼,並且當加入越來越多的非業務需求(例如日誌記錄和參數驗證),原有的計算器方法變得膨脹冗長。這裏有一件非常痛苦的事情,無法使用原有的編程方式將他們模塊化,從核心業務中提取出來。例如日誌記錄和參數驗證,AOP裏將他們稱為橫切關註點(crosscutting concern),它們屬於系統範圍的需求通常需要跨越多個模塊。
  在使用傳統的面向對象的編程方式無法理想化的模塊化橫切關註點,程序員不能不做的就是將這些橫切關註點放置在每一個模塊裏與核心邏輯交織在一起,這將會導致橫切關註點在每一個模塊裏到處存在。使用非模塊化的手段實現橫切關註將會導致,代碼混亂,代碼分散,代碼重復。你想想看如果日誌記錄需要換一種顯示方式,那你要改多少代碼,一旦漏掉一處(概率很高),將會導致日誌記錄不一致。這樣的代碼很維護。種種原因表明,模塊只需要關註自己原本的功能需求,需要一種方式來將橫切關註點沖模塊中提取出來。

  忍無可忍的大牛們提出了AOP,它是一個概念,一個規範,本身並沒有設定具體語言的實現,也正是這個特性讓它變的非常流行,現在已經有許多開源的AOP實現框架了。本次不是介紹這些框架的,我們將不使用這些框架,而是使用底層編碼的方式實現最基本的AOP解決上面例子出現的問題。AOP實際是GoF設計模式的延續,設計模式孜孜不倦追求的是調用者和被調用者之間的解耦,AOP可以說也是這種目標的一種實現。AOP可以使用"代理模式"來實現。

技術分享

  代理模式的原理是使用一個代理將對象包裝起來,然後用該代理對象取代原始的對象,任何對原始對象的調用首先要經過代理。代理對象負責決定是否以及何時將方法調用信息轉發到原始對象上。與此同時,圍繞著每個方法的調用,代理對象也可以執行一些額外的工作。可以看出代理模式非常適合實現橫切關註點。

  由於本人只了解Java,所以姑且認為代理模式有兩種實現方式,一種是靜態代理、另一種是動態代理。他們的區別在於編譯時知不知道代理的對象是誰。在模塊比較多的系統中,靜態代理是不合適也非常低效的,因為靜態代理需要專門為每一個接口設計一個代理類,系統比較大成百上千的接口是很正常的,靜態代理模式太消耗人力了。動態代理是JDK所支持的代理模式,它可以非常好的實現橫切關註點。

技術分享
/*使用動態代理需要實現InvocationHandler接口*/
public class ArithmeticCalculatorInvocationHandler implements InvocationHandler
{
/*要代理的對象,動態代理只有在運行時才知道代理誰,所以定義為Object類型,可以代理任意對象*/
private Object target = null;

/*通過構造函數傳入原對象*/
public ArithmeticCalculatorInvocationHandler(Object target)
{
this.target = target;
}

/*InvocationHandler接口的方法,proxy表示代理,method表示原對象被調用的方法,args表示方法的參數*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
/*原對象方法調用前處理日誌信息*/
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(this.target.getClass().getClassLoader(), this.getClass().getInterfaces(), this);
}
}
技術分享

場景類調用:

技術分享
public class Client
{
public static void main(String[] args) throws Exception
{
/*獲得代理*/
Calculator arithmeticCalculatorProxy = (Calculator)new ArithmeticCalculatorInvocationHandler(
new ArithmeticCalculator()).getProxy();

/*調用add方法*/
arithmeticCalculatorProxy.add(10, 10);
}
}
技術分享

控制臺的輸出:

the method [add]begin with args ([10.0, 10.0])
the method [add]end with result (20.0)

可以看到使用動態代理實現了橫切關註點。

技術分享

若需要添加參數驗證功能,只需要再創建一個參數驗證代理即可:

技術分享
public class ArithmeticCalculatorArgsInvocationHandler implements
InvocationHandler
{
/*要代理的對象,動態代理只有在運行時才知道代理誰,所以定義為Object類型,可以代理任意對象*/
private Object target = null;

/*通過構造函數傳入原對象*/
public ArithmeticCalculatorArgsInvocationHandler(Object target)
{
this.target = target;
}

/*InvocationHandler接口的方法,proxy表示代理,method表示原對象被調用的方法,args表示方法的參數*/
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
System.out.println("begin valid method ["+method.getName()+"] with args "+Arrays.toString(args));

for(Object arg : args)
{
this.argValidtor((Double)arg);
}

Object result = method.invoke(this.target, args);

return result;
}

/*獲取代理類*/
public Object getProxy()
{
return Proxy.newProxyInstance(this.target.getClass().getClassLoader(), this.target.getClass().getInterfaces(), this);
}

private void argValidtor(double arg) throws Exception
{
if(arg < 0)
throw new Exception("參數不能為負數!");
}
}
技術分享

場景類調用:

技術分享
public class Client
{
public static void main(String[] args) throws Exception
{
/*獲得代理*/
Calculator arithmeticCalculatorProxy = (Calculator)new ArithmeticCalculatorInvocationHandler(
new ArithmeticCalculator()).getProxy();

Calculator argValidatorProxy = (Calculator)new ArithmeticCalculatorArgsInvocationHandler(arithmeticCalculatorProxy).getProxy();

/*調用add方法*/
argValidatorProxy.add(10, 10);
}
}
技術分享

控制臺輸出:

begin valid method [add] with args [10.0, 10.0]
the method [add]begin with args ([10.0, 10.0])
the method [add]end with result (20.0)

輸入一個負數數據:

技術分享
public class Client
{
public static void main(String[] args) throws Exception
{
/*獲得代理*/
Calculator arithmeticCalculatorProxy = (Calculator)new ArithmeticCalculatorInvocationHandler(
new ArithmeticCalculator()).getProxy();

Calculator argValidatorProxy = (Calculator)new ArithmeticCalculatorArgsInvocationHandler(arithmeticCalculatorProxy).getProxy();

/*調用add方法*/
argValidatorProxy.add(-10, 10);
}
}
技術分享

控制臺輸出:

技術分享
begin valid method [add] with args [-10.0, 10.0]
Exception in thread "main" java.lang.Exception: 參數不能為負數!
at com.beliefbetrayal.aop.ArithmeticCalculatorArgsInvocationHandler.argValidtor(ArithmeticCalculatorArgsInvocationHandler.java:46)
at com.beliefbetrayal.aop.ArithmeticCalculatorArgsInvocationHandler.invoke(ArithmeticCalculatorArgsInvocationHandler.java:29)
at $Proxy0.add(Unknown Source)
at com.beliefbetrayal.aop.Client.main(Client.java:14)
技術分享

技術分享


  不知道你有沒有使用過Struts2,這個結構和Struts2的攔截器非常相似,一個個Action對象好比我們的原對象業務核心,一個個攔截器好比是這裏的代理,通用的功能實現成攔截器,讓Action可以共用,Struts2的攔截器也是AOP的優秀實現。

面向切面編程