2015-02-26 16:07

[轉載] Java 動態代理

轉載自:Java Essence: 動態代理

來看一個最簡單的例子,當您需要在執行某些方法時留下日誌訊息,直覺的,您可能會如下撰寫:
  1. package cc.openhome; 
  2.  
  3. import java.util.logging.*; 
  4.  
  5. public class HelloSpeaker { 
  6.    private Logger logger = 
  7.            Logger.getLogger(this.getClass().getName()); 
  8.  
  9.    public void hello(String name) { 
  10.        // 方法執行開始時留下日誌 
  11.        logger.log(Level.INFO, "hello method starts...."); 
  12.        // 程式主要功能 
  13.        System.out.println("Hello, " + name); 
  14.        // 方法執行完畢前留下日誌 
  15.        logger.log(Level.INFO, "hello method ends...."); 
  16.    } 
  17. } 


HelloSpeaker 類別中,當執行 hello() 方法時,你希望該方法執行開始與執行完畢時都能留下日誌,最簡單的作法就是如以上的程式設計,在 方法執行的前後加上日誌動作,然而記錄的這幾行程式碼橫切入(Cross-cutting)HelloSpeaker 類別中,對於 HelloSpeaker 來說,日誌的這幾個動作並不屬於 HelloSpeaker 商務邏輯(顯示"Hello"等文字),這使得 HelloSpeaker 增加了額外的職責。

想想如果程式中這種日誌的動作到處都有需求,以上的寫法勢必造成你必須到處撰寫這些日誌動作的程式碼,這將使得維護日誌程式碼的困難度加大。如果需要的服 務(Service)不只有日誌動作,有一些非物件本身職責的相關動作也混入了物件之中(例如權限檢查、交易管理等等),會使得物件的負擔更形加重,甚至 混淆了物件本身該負有的職責,物件本身的職責所佔的程式碼,或許還小於這些與物件職責不相關的動作或服務的程式碼。

另一方面,使用以上的寫法,若你有一日不再需要日誌(或權限檢查、交易管理等)的服務,那麼你將需要修改所有留下日誌動作的程式碼,你無法簡單的就將這些相關服務從即有的程式中移去。

可以使用代理(Proxy)機制來解決這個問題,在這邊討論兩種代理方式:
  • 靜態代理(Static proxy)
  • 動態代理(Dynamic proxy)

在靜態代理的實現中,代理物件與被代理的物件都必須實現同一個介面,在代理物件中可以實現記錄等相關服務,並在需要的時候再呼叫被代理的物件,如此被代理物件當中就可以僅保留商務相關職責。

舉個實際的例子來說,首先定義一個 IHello 介面:
IHello.java
  1. package cc.openhome; 
  2.  
  3. public interface IHello { 
  4.    public void hello(String name); 
  5. } 


然後讓實現商務邏輯的 HelloSpeaker 類別要實現 IHello 介面,例如:
HelloSpeaker.java
  1. package cc.openhome; 
  2.  
  3. public class HelloSpeaker implements IHello { 
  4.    public void hello(String name) { 
  5.        System.out.println("Hello, " + name); 
  6.    } 
  7. } 


可以看到,在 HelloSpeaker 類別中現在沒有任何日誌的程式碼插入其中,日誌服務的實現將被放至代理物件之中,代理物件同樣也要實現 IHello 介面,例如:
HelloProxy.java
  1. package cc.openhome; 
  2.  
  3. import java.util.logging.*; 
  4.  
  5. public class HelloProxy implements IHello { 
  6.    private Logger logger = 
  7.            Logger.getLogger(this.getClass().getName()); 
  8.  
  9.    private IHello helloObject; 
  10.  
  11.    public HelloProxy(IHello helloObject) { 
  12.        this.helloObject = helloObject; 
  13.    } 
  14.  
  15.    public void hello(String name) { 
  16.        // 日誌服務 
  17.        log("hello method starts...."); 
  18.  
  19.        // 執行商務邏輯 
  20.        helloObject.hello(name); 
  21.  
  22.        // 日誌服務 
  23.        log("hello method ends...."); 
  24.    } 
  25.  
  26.    private void log(String msg) { 
  27.        logger.log(Level.INFO, msg); 
  28.    } 
  29. } 


HelloProxy 類別的 hello() 方法中,真正實現商務邏輯前後可以安排記錄服務,可以實際撰寫一個測試程式來看看如何使用代理物件。
ProxyDemo.java
  1. package cc.openhome; 
  2.  
  3. public class ProxyDemo { 
  4.    public static void main(String[] args) { 
  5.        IHello proxy = 
  6.            new HelloProxy(new HelloSpeaker()); 
  7.        proxy.hello("Justin"); 
  8.    } 
  9. } 


程式中呼叫執行的是代理物件,建構代理物件時必須給它一個被代理物件,記得在操作取回的代理物件時,必須轉換操作介面為 IHello 介面。

代理物件 HelloProxy 將代理真正的 HelloSpeaker 來執行 hello(),並在其前後加上日誌的動作,這使得我們的 HelloSpeaker 在撰寫時不必介入日誌動作,HelloSpeaker 可以專心於它的職責。

在 JDK 1.3 之後加入了可協助開發動態代理功能的 API 等相關類別,您不必為特定物件與方法撰寫特定的代理物件,使用動態代理,可以使得一個處理者 (Handler)服務於各個物件,首先,一個處理者的類別設計必須實作 java.lang.reflect.InvocationHandler 介面, 以實例來進行說明,例如設計一個 LogHandler 類別:
LogHandler.java
  1. package cc.openhome; 
  2.  
  3. import java.util.logging.*; 
  4. import java.lang.reflect.*; 
  5.  
  6. public class LogHandler implements InvocationHandler { 
  7.    private Logger logger = 
  8.            Logger.getLogger(this.getClass().getName()); 
  9.  
  10.    private Object delegate; 
  11.  
  12.    public Object bind(Object delegate) { 
  13.        this.delegate = delegate; 
  14.        return Proxy.newProxyInstance( 
  15.            delegate.getClass().getClassLoader(), 
  16.            delegate.getClass().getInterfaces(), 
  17.            this 
  18.        ); 
  19.    } 
  20.  
  21.    public Object invoke(Object proxy, Method method, Object[] args)  
  22.        throws Throwable  
  23.    { 
  24.        Object result = null; 
  25.  
  26.        try { 
  27.            log("method starts..." + method); 
  28.  
  29.            result = method.invoke(delegate, args); 
  30.  
  31.            logger.log(Level.INFO, "method ends..." + method); 
  32.        } catch (Exception e){ 
  33.            log(e.toString()); 
  34.        } 
  35.  
  36.        return result; 
  37.    } 
  38.  
  39.    private void log(String message) { 
  40.        logger.log(Level.INFO, message); 
  41.    } 
  42. } 


主要的概念是使用 Proxy.newProxyInstance() 靜態方法建立一個代理物件(底層會使用 Native 的方式生成代理物件的 Class 實例),建立代理物件時必須告知所要代理的介面,之後您可以操作所 建立的代理物件,在每次操作時會呼叫 InvocationHandlerinvoke() 方法,invoke() 方法會傳入被代理物件的方法名稱與執行 參數,實際上要執行的方法交由 method.invoke(),您在 method.invoke() 前後加上記錄動作,method.invoke() 傳 回的物件是實際方法執行過後的回傳結果。

接下來撰寫一個測試的程式,您要使用 LogHandlerbind() 方法來綁定被代理物件,如下所示:
ProxyDemo.java
  1. package cc.openhome; 
  2.  
  3. public class ProxyDemo { 
  4.    public static void main(String[] args) { 
  5.        LogHandler logHandler  = new LogHandler(); 
  6.  
  7.        IHello helloProxy = 
  8.            (IHello) logHandler.bind(new HelloSpeaker()); 
  9.  
  10.        helloProxy.hello("Justin"); 
  11.    } 
  12. } 

0 回應: