反射實現 AOP 動態代理模式例項說明(Spring AOP 的實現 原理)
說明以下,spring aop的實現原理不是用java的動態代理。是用代理模式和CGLib (Code GenerationLibrary), 不過現在不用CGLib(Code Generation Library),直接用ASM框架來操作位元組碼了。
好長時間沒有用過Spring了. 突然拿起書.我都發現自己對AOP都不熟悉了.
其實AOP的意思就是面向切面程式設計.
OO注重的是我們解決問題的方法(封裝成Method),而AOP注重的是許多解決解決問題的方法中的共同點,是對OO思想的一種補充!
還是拿人家經常舉的一個例子講解一下吧:
比如說,我們現在要開發的一個應用裡面有很多的業務方法,但是,我們現在要對這個方法的執行做全面監控,或部分監控.也許我們就會在要一些方法前去加上一條日誌記錄,
我們寫個例子看看我們最簡單的解決方案
我們先寫一個介面IHello.java程式碼如下:
package sinosoft.dj.aop.staticaop;
public interface IHello {
/** *//**
* 假設這是一個業務方法
* @param name
*/
void sayHello(String name);
}
裡面有個方法,用於輸入"Hello" 加傳進來的姓名;我們去寫個類實現IHello介面
package sinosoft.dj.aop.staticaop; public class Hello implements IHello { public void sayHello(String name) { System.out.println("Hello " + name); } }
現在我們要為這個業務方法加上日誌記錄的業務,我們在不改變原始碼的情況下,我們會去怎麼做呢?也許,你會去寫一個類去實現IHello介面,並依賴Hello這個類.程式碼如下:
1package sinosoft.dj.aop.staticaop;
2
3public class HelloProxy implements IHello {
4 private IHello hello;
5
6 public HelloProxy(IHello hello) {
7 this.hello = hello;
8 }
9
10 public void sayHello(String name) {
11 Logger.logging(Level.DEBUGE, "sayHello method start.");
12 hello.sayHello(name);
13 Logger.logging(Level.INFO, "sayHello method end!");
14
15 }
16
17}
18
其中.Logger類和Level列舉程式碼如下:
Logger.java
1package sinosoft.dj.aop.staticaop;
2
3import java.util.Date;
4
5public class Logger{
6 /** *//**
7 * 根據等級記錄日誌
8 * @param level
9 * @param context
10 */
11 public static void logging(Level level, String context) {
12 if (level.equals(Level.INFO)) {
13 System.out.println(new Date().toLocaleString() + " " + context);
14 }
15 if (level.equals(Level.DEBUGE)) {
16 System.err.println(new Date() + " " + context);
17 }
18 }
19
20}
21Level.java
1package sinosoft.dj.aop.staticaop;
2
3public enum Level {
4 INFO,DEBUGE;
5}
6那我們去寫個測試類看看,程式碼如下:
Test.java
1package sinosoft.dj.aop.staticaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = new HelloProxy(new Hello());
6 hello.sayHello("Doublej");
7 }
8}
9執行以上程式碼我們可以得到下面結果:
Tue Mar 04 20:57:12 CST 2008 sayHello method start.
Hello Doublej
2008-3-4 20:57:12 sayHello method end!
從上面的程式碼我們可以看出,hello物件是被HelloProxy這個所謂的代理態所建立的.這樣,如果我們以後要把日誌記錄的功能去掉.那我們只要把得到hello物件的程式碼改成以下:
1package sinosoft.dj.aop.staticaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = new Hello();
6 hello.sayHello("Doublej");
7 }
8}
9
上面程式碼,可以說是AOP最簡單的實現!
但是我們會發現一個問題,如果我們像Hello這樣的類很多,那麼,我們是不是要去寫很多個HelloProxy這樣的類呢.沒錯,是的.其實也是一種很麻煩的事.在jdk1.3以後.jdk跟我們提供了一個API java.lang.reflect.InvocationHandler的類. 這個類可以讓我們在JVM呼叫某個類的方法時動態的為些方法做些什麼事.讓我們把以上的程式碼改一下來看看效果.
同樣,我們寫一個IHello的介面和一個Hello的實現類.在介面中.我們定義兩個方法;程式碼如下 :
IHello.java
1package sinosoft.dj.aop.proxyaop;
2
3public interface IHello {
4 /** *//**
5 * 業務處理A方法
6 * @param name
7 */
8 void sayHello(String name);
9 /** *//**
10 * 業務處理B方法
11 * @param name
12 */
13 void sayGoogBye(String name);
14}
15
Hello.java
1package sinosoft.dj.aop.proxyaop;
2
3public class Hello implements IHello {
4
5 public void sayHello(String name) {
6 System.out.println("Hello " + name);
7 }
8 public void sayGoogBye(String name) {
9 System.out.println(name+" GoodBye!");
10 }
11}
12
我們一樣的去寫一個代理類.只不過.讓這個類去實現java.lang.reflect.InvocationHandler介面,程式碼如下:
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.InvocationHandler;
4import java.lang.reflect.Method;
5import java.lang.reflect.Proxy;
6
7public class DynaProxyHello implements InvocationHandler {
8
9 /** *//**
10 * 要處理的物件(也就是我們要在方法的前後加上業務邏輯的物件,如例子中的Hello)
11 */
12 private Object delegate;
13
14 /** *//**
15 * 動態生成方法被處理過後的物件 (寫法固定)
16 *
17 * @param delegate
18 * @param proxy
19 * @return
20 */
21 public Object bind(Object delegate) {
22 this.delegate = delegate;
23 return Proxy.newProxyInstance(
24 this.delegate.getClass().getClassLoader(), this.delegate
25 .getClass().getInterfaces(), this);
26 }
27 /** *//**
28 * 要處理的物件中的每個方法會被此方法送去JVM呼叫,也就是說,要處理的物件的方法只能通過此方法呼叫
29 * 此方法是動態的,不是手動呼叫的
30 */
31 public Object invoke(Object proxy, Method method, Object[] args)
32 throws Throwable {
33 Object result = null;
34 try {
35 //執行原來的方法之前記錄日誌
36 Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
37
38 //JVM通過這條語句執行原來的方法(反射機制)
39 result = method.invoke(this.delegate, args);
40 //執行原來的方法之後記錄日誌
41 Logger.logging(Level.INFO, method.getName() + " Method Start!");
42 } catch (Exception e) {
43 e.printStackTrace();
44 }
45 //返回方法返回值給呼叫者
46 return result;
47 }
48
49}
50
上面類中出現的Logger類和Level列舉還是和上一上例子的實現是一樣的.這裡就不貼出程式碼了.
讓我們寫一個Test類去測試一下.程式碼如下:
Test.java
1package sinosoft.dj.aop.proxyaop;
2
3public class Test {
4 public static void main(String[] args) {
5 IHello hello = (IHello)new DynaProxyHello().bind(new Hello());
6 hello.sayGoogBye("Double J");
7 hello.sayHello("Double J");
8
9 }
10}
11
執行輸出的結果如下:
Tue Mar 04 21:24:03 CST 2008 sayGoogBye Method end .
Double J GoodBye!
2008-3-4 21:24:03 sayGoogBye Method Start!
Tue Mar 04 21:24:03 CST 2008 sayHello Method end .
Hello Double J
2008-3-4 21:24:03 sayHello Method Start!
由於執行緒的關係,第二個方法的開始出現在第一個方法的結束之前.這不是我們所關注的!
從上面的例子我們看出.只要你是採用面向介面程式設計,那麼,你的任何物件的方法執行之前要加上記錄日誌的操作都是可以的.他(DynaPoxyHello)自動去代理執行被代理物件(Hello)中的每一個方法,一個java.lang.reflect.InvocationHandler介面就把我們的代理物件和被代理物件解藕了.但是,我們又發現還有一個問題,這個DynaPoxyHello物件只能跟我們去在方法前後加上日誌記錄的操作.我們能不能把DynaPoxyHello物件和日誌操作物件(Logger)解藕呢?
結果是肯定的.讓我們來分析一下我們的需求.
我們要在被代理物件的方法前面或者後面去加上日誌操作程式碼(或者是其它操作的程式碼),
那麼,我們可以抽象出一個介面,這個接口裡就只有兩個方法,一個是在被代理物件要執行方法之前執行的方法,我們取名為start,第二個方法就是在被代理物件執行方法之後執行的方法,我們取名為end .介面定義如下 :
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.Method;
4
5public interface IOperation {
6 /** *//**
7 * 方法執行之前的操作
8 * @param method
9 */
10 void start(Method method);
11 /** *//**
12 * 方法執行之後的操作
13 * @param method
14 */
15 void end(Method method);
16}
17
我們去寫一個實現上面介面的類.我們把作他真正的操作者,如下面是日誌操作者的一個類:
LoggerOperation.java
package sinosoft.dj.aop.proxyaop;
import java.lang.reflect.Method;
public class LoggerOperation implements IOperation {
public void end(Method method) {
Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
}
public void start(Method method) {
Logger.logging(Level.INFO, method.getName() + " Method Start!");
}
}
然後我們要改一下代理物件DynaProxyHello中的程式碼.如下:
1package sinosoft.dj.aop.proxyaop;
2
3import java.lang.reflect.InvocationHandler;
4import java.lang.reflect.Method;
5import java.lang.reflect.Proxy;
6
7public class DynaProxyHello implements InvocationHandler {
8 /** *//**
9 * 操作者
10 */
11 private Object proxy;
12 /** *//**
13 * 要處理的物件(也就是我們要在方法的前後加上業務邏輯的物件,如例子中的Hello)
14 */
15 private Object delegate;
16
17 /** *//**
18 * 動態生成方法被處理過後的物件 (寫法固定)
19 *
20 * @param delegate
21 * @param proxy
22 * @return
23 */
24 public Object bind(Object delegate,Object proxy) {
25
26 this.proxy = proxy;
27 this.delegate = delegate;
28 return Proxy.newProxyInstance(
29 this.delegate.getClass().getClassLoader(), this.delegate
30 .getClass().getInterfaces(), this);
31 }
32 /** *//**
33 * 要處理的物件中的每個方法會被此方法送去JVM呼叫,也就是說,要處理的物件的方法只能通過此方法呼叫
34 * 此方法是動態的,不是手動呼叫的
35 */
36 public Object invoke(Object proxy, Method method, Object[] args)
37 throws Throwable {
38 Object result = null;
39 try {
40 //反射得到操作者的例項
41 Class clazz = this.proxy.getClass();
42 //反射得到操作者的Start方法
43 Method start = clazz.getDeclaredMethod("start",
44 new Class[] { Method.class });
45 //反射執行start方法
46 start.invoke(this.proxy, new Object[] { method });
47 //執行要處理物件的原本方法
48 result = method.invoke(this.delegate, args);
49// 反射得到操作者的end方法
50 Method end = clazz.getDeclaredMethod("end",
51 new Class[] { Method.class });
52// 反射執行end方法
53 end.invoke(this.proxy, new Object[] { method });
54
55 } catch (Exception e) {
56 e.printStackTrace();
57 }
58 return result;
59 }
60
61}
62
然後我們把Test.java中的程式碼改一下.測試一下:
package sinosoft.dj.aop.proxyaop;
public class Test {
public static void main(String[] args) {
IHello hello = (IHello)new DynaProxyHello().bind(new Hello(),new LoggerOperation());
hello.sayGoogBye("Double J");
hello.sayHello("Double J");
}
}
結果還是一樣的吧.
如果你想在每個方法之前加上日誌記錄,而不在方法後加上日誌記錄.你就把LoggerOperation類改成如下:
package sinosoft.dj.aop.proxyaop;
import java.lang.reflect.Method;
public class LoggerOperation implements IOperation {
public void end(Method method) {
//Logger.logging(Level.DEBUGE, method.getName() + " Method end .");
}
public void start(Method method) {
Logger.logging(Level.INFO, method.getName() + " Method Start!");
}
}
執行一下.你就會發現,每個方法之後沒有記錄日誌了. 這樣,我們就把代理者和操作者解藕了!
下面留一個問題給大家,如果我們不想讓所有方法都被日誌記錄,我們應該怎麼去解藕呢.?
我的想法是在代理物件的public Object invoke(Object proxy, Method method, Object[] args)方法裡面加上個if(),對傳進來的method的名字進行判斷,判斷的條件存在XML裡面.這樣我們就可以配置檔案時行解藕了.如果有興趣的朋友可以把操作者,被代理者,都通過配置檔案進行配置 ,那麼就可以寫一個簡單的SpringAOP框架了.
Spring AOP的一些概念
切面(Aspect): 一個關注點的模組化,這個關注點可能會橫切多個物件。事務管理是J2EE應用中一個關於橫切關注點的很好的例子。在Spring AOP中,切面可以使用通用類(基於模式的風格) 或者在普通類中以@Aspect 註解(@AspectJ風格)來實現。
連線點(Joinpoint): 在程式執行過程中某個特定的點,比如某方法呼叫的時候或者處理異常的時候。 在Spring AOP中,一個連線點 總是 代表一個方法的執行。通過宣告一個org.aspectj.lang.JoinPoint型別的引數可以使通知(Advice)的主體部分獲得連線點資訊。
通知(Advice):在切面的某個特定的連線點(Joinpoint)上執行的動作。通知有各種型別,其中包括“around”、“before”和“after”等通知。通知的型別將在後面部分進行討論。許多AOP框架,包括Spring,都是以攔截器做通知模型, 並維護一個以連線點為中心的攔截器鏈。
切入點(Pointcut):匹配連線點(Joinpoint)的斷言。通知和一個切入點表示式關聯,並在滿足這個切入點的連線點上執行(例如,當執行某個特定名稱的方法時)。切入點表示式如何和連線點匹配是AOP的核心:Spring預設使用AspectJ切入點語法。
引入(Introduction): (也被稱為內部型別宣告(inter-type declaration))。宣告額外的方法或者某個型別的欄位。 Spring允許引入新的介面(以及一個對應的實現)到任何被代理的物件。例如,你可以使用一個引入來使bean實現 IsModified 介面,以便簡化快取機制。
目標物件(Target Object): 被一個或者多個切面(aspect)所通知(advise)的物件。也有人把它叫做被通知(advised) 物件。 既然Spring AOP是通過執行時代理實現的,這個物件永遠是一個 被代理(proxied) 物件。
AOP代理(AOP Proxy): AOP框架建立的物件,用來實現切面契約(aspect contract)(包括通知方法執行等功能)。 在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。 注意:Spring 2.0最新引入的基於模式(schema-based)風格和@AspectJ註解風格的切面宣告,對於使用這些風格的使用者來說,代理的建立是透明的。
織入(Weaving): 把切面(aspect)連線到其它的應用程式型別或者物件上,並建立一個被通知(advised)的物件。 這些可以在編譯時(例如使用AspectJ編譯器),類載入時和執行時完成。 Spring和其他純Java AOP框架一樣,在執行時完成織入。
通知的型別:
前置通知(Before advice):在某連線點(join point)之前執行的通知,但這個通知不能阻止連線點前的執行(除非它丟擲一個異常)。
返回後通知(After returning advice):在某連線點(join point)正常完成後執行的通知:例如,一個方法沒有丟擲任何異常,正常返回。
丟擲異常後通知(After throwing advice): 在方法丟擲異常退出時執行的通知。
後通知(After (finally)advice):當某連線點退出的時候執行的通知(不論是正常返回還是異常退出)。
環繞通知(Around Advice):包圍一個連線點(join point)的通知,如方法呼叫。這是最強大的一種通知型別。 環繞通知可以在方法呼叫前後完成自定義的行為。它也會選擇是否繼續執行連線點或直接返回它們自己的返回值或丟擲異常來結束執行。