AOP的全名是Aspect-oriented Programming,Aspect是程式設計時一個新的中心,AOP並不取代OOP,兩者各有各的角色,將職責各自分配自Object與Aspect,會使得程式中各個元件的角色更為清楚。
這邊先不直接探討AOP,我們先從一個簡單常見的例子來看看一個議題,這個例子是記錄(log)動作,程式中很常需要為某些動作或事件作下記錄,以便在事後檢視或是作為除錯時的資訊,一個最簡單的例子如下:
代碼:
import java.util.logging.*;
public class HelloSpeaker {
private Logger logger = Logger.getLogger(this.getClass().getName());
public void hello(String name) {
logger.log(Level.INFO, "hello method starts....");
System.out.println("Hello, " + name);
logger.log(Level.INFO, "hello method ends....");
}
}
HelloSpeaker在執行hello()方法時,我們希望能記錄該方法已經執行及結束,最簡單的作法就是如上在執行的前後加上記錄動作,然而Logger介入了HelloSpeaker中,記錄這個動作並不屬於HelloSpeaker,這使得HelloSpeaker的職責加重。
想想如果程式中這種記錄的動作到處都有需求,上面這種寫法勢必造成我們必須複製記錄動作的程式碼,使得維護記錄動作的困難度加大。如果不只有記錄動作,有一些非物件本身職責的相關動作也混入了物件之中(例如權限檢查、事務管理等等),會使得物件的負擔更形加重,甚至混淆了物件的職責,物件本身的職責所佔的程式碼,或許遠小於這些與物件職責不相關動作的程式碼。
怎麼辦,用下面的方法或許好一些,我們先定義一個介面,然後實作該介面:
代碼:
public interface IHello {
public void hello(String name);
}
代碼:
public class HelloSpeaker implements IHello {
public void hello(String name) {
System.out.println("Hello, " + name);
}
}
接下來是重點了,我們實作一個代理物件HelloProxy:
代碼:
import java.util.logging.*;
public class HelloProxy implements IHello {
private Logger logger = Logger.getLogger(this.getClass().getName());
private IHello helloObject;
public HelloProxy(IHello helloObject) {
this.helloObject = helloObject;
}
public void hello(String name) {
logger.log(Level.INFO, "hello method starts....");
helloObject.hello(name);
logger.log(Level.INFO, "hello method ends....");
}
}
執行時可以如此:
代碼:
IHello helloProxy = new HelloProxy(new HelloSpeaker());
helloProxy.hello("Justin");
代理物件HelloProxy將代理真正的HelloSpeaker來執行hello(),並在其前後加上記錄的動作,這使得我們的HelloSpeaker在撰寫時不必介入記錄動作,HelloSpeaker可以專心於它的職責。
這是靜態代理的基本範例,然而如您所看到的,代理物件的一個介面只服務於一種類型的物件,而且如果要代理的方法很多,我們勢必要為每個方法進行代理,靜態代理在程式規模稍大時就必定無法勝任。
Java在JDK 1.3之後加入協助開發動態代理功能的類別,我們不必為特定物件與方法撰寫特定的代理,使用動態代理,可以使得一個handler服務於各個物件,首先,一個handler必須實現java.lang.reflect.InvocationHandler:
代碼:
import java.util.logging.*;
import java.lang.reflect.*;
public class LogHandler implements InvocationHandler {
private Logger logger = Logger.getLogger(this.getClass().getName());
private Object delegate;
public Object bind(Object delegate) {
this.delegate = delegate;
return Proxy.newProxyInstance(
delegate.getClass().getClassLoader(),
delegate.getClass().getInterfaces(),
this);
}
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
Object result = null;
try {
logger.log(Level.INFO, "method starts..." + method);
result = method.invoke(delegate, args);
logger.log(Level.INFO, "method ends..." + method);
} catch (Exception e){
logger.log(Level.INFO, e.toString());
}
return result;
}
}
InvocationHandler的invoke()方法會傳入被代理物件的方法名稱與執行參數實際上要執行的方法交由method.invoke(),並在其前後加上記錄動作,method.invoke()傳回的物件是實際方法執行過後的回傳結果。
動態代理必須宣告介面,實作該介面,例如:
代碼:
public interface IHello {
public void hello(String name);
}
代碼:
public class HelloSpeaker implements IHello {
public void hello(String name) {
System.out.println("Hello, " + name);
}
}
java.lang.reflect.Proxy的newProxyInstance()依要代理的物件、介面與handler產生一個代理物件,我們可以使用下面的方法來執行程式:
代碼:
LogHandler logHandler = new LogHandler();
IHello helloProxy = (IHello) logHandler.bind(new HelloSpeaker());
helloProxy.hello("Justin");
LogHandler不在服務於特定物件與介面,而HelloSpeaker也不用插入任何有關於記錄的動作,它不用意識到記錄動作的存在。
講了半天,我們回到AOP的議題上,這個例子與AOP有何關係?
如上面的例子中示範的,HelloSpeaker本來必須插入記錄動作,這使得HelloSpeaker的職責加重,並混淆其原來的角色,為此,我們使用代理將記錄的動作提取出來,以釐清記錄動作與HelloSpeaker的職責與角色。
在這裡,記錄這個動作是我們所關注的,AOP中的Aspect所指的就是像記錄這類的動作,我們將這些動作(或特定職責)視為關注的中心,將其設計為通用、不介入特定物件、職責清楚的元件,這就是所謂的「Aspect導向」(比較好的一個中文翻法是「切面導向」,下一個主題再說明),就如同「物件導向」一樣,每個物件僅代表一個實際的個體,將物件視為關注的中心,將其設計為通用、職責清楚的元件。
這邊先以代理機制作為一個開端初步探討AOP,如您所看到的,AOP與OOP並不抵觸,兩者的合作使得程式更為清晰,Object與Aspect職責不互相混淆,而且都更具重用性,下一個主題我們再來深入AOP的一些細節。