首頁  >  文章  >  Java  >  Java中靜態代理程式和動態代理程式的四種實作方法介紹

Java中靜態代理程式和動態代理程式的四種實作方法介紹

不言
不言轉載
2018-10-22 14:53:532798瀏覽

這篇文章帶給大家的內容是關於Java中靜態代理和動態代理的四種實作方法介紹,有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。

面試問題:Java裡的代理程式設計模式(Proxy Design Pattern)一共有幾種實作方式?這個題目很像孔乙己問「茴香豆的徠字有哪幾種寫法?」

所謂代理模式,是指客戶端(Client)並不會直接呼叫實際的物件(下圖右下角的RealSubject),而是透過呼叫代理(Proxy),來間接的呼叫實際的物件。

代理模式的使用場合,一般是由於客戶端不想直接存取實際對象,或存取實際的對象存在技術上的障礙,因而透過代理對像作為橋樑,來完成間接存取。

Java中靜態代理程式和動態代理程式的四種實作方法介紹

實作方式一:靜態代理

開發一個介面IDeveloper,該介麵包含一個方法writeCode ,寫程式碼。

public interface IDeveloper {

     public void writeCode();

}

建立一個Developer類,實作該介面。

public class Developer implements IDeveloper{
    private String name;
    public Developer(String name){
        this.name = name;
    }
    @Override
    public void writeCode() {
        System.out.println("Developer " + name + " writes code");
    }
}

測試程式碼:建立一個Developer實例,名叫Jerry,去寫程式碼!

public class DeveloperTest {
    public static void main(String[] args) {
        IDeveloper jerry = new Developer("Jerry");
        jerry.writeCode();
    }
}

現在問題來了。 Jerry的專案經理對Jerry光寫程式碼,而不維護任何的文件很不滿。假設哪天Jerry休假去了,其他的程式設計師來接替Jerry的工作,對著陌生的程式碼一臉問號。經全組討論決定,每位開發人員寫程式碼時,必須同步更新文件。

為了強迫每個程式設計師在開發時記著寫文檔,而又不影響大家寫程式碼這個動作本身, 我們不修改原來的Developer類,而是創建了一個新的類,同樣實現IDeveloper介面。這個新類別DeveloperProxy內部維護了一個成員變量,指向原始的IDeveloper實例:

public class DeveloperProxy implements IDeveloper{
    private IDeveloper developer;
    public DeveloperProxy(IDeveloper developer){
        this.developer = developer;
    }
    @Override
    public void writeCode() {
        System.out.println("Write documentation...");
        this.developer.writeCode();
    }
}

這個代理類別實現的writeCode方法裡,在調用實際程式設計師writeCode方法之前,加上一個寫文檔的調用,這樣就確保了程式設計師寫程式碼時都伴隨著文件更新。

測試程式碼:

Java中靜態代理程式和動態代理程式的四種實作方法介紹

#靜態代理程式方式的優點

1.易於理解和實現

2. 代理類別和真實類別的關係是編譯期靜態決定的,和下文馬上要介紹的動態代理比較起來,執行時沒有任何額外開銷。

靜態代理程式方式的缺點

每一個真實類別都需要一個建立新的代理類別。還是以上述文件更新為例,假設老闆對測試工程師也提出了新的要求,讓測試工程師每次測出bug時,也要及時更新對應的測試文件。那麼採用靜態代理的方式,測試工程師的實作類別ITester也得建立一個對應的ITesterProxy類別。

public interface ITester {
    public void doTesting();
}
Original tester implementation class:
public class Tester implements ITester {
    private String name;
    public Tester(String name){
        this.name = name;
    }
    @Override
    public void doTesting() {
        System.out.println("Tester " + name + " is testing code");
    }
}
public class TesterProxy implements ITester{
    private ITester tester;
    public TesterProxy(ITester tester){
        this.tester = tester;
    }
    @Override
    public void doTesting() {
        System.out.println("Tester is preparing test documentation...");
        tester.doTesting();
    }
}

正是因為有了靜態程式碼方式的這個缺點,才誕生了Java的動態代理實作方式。

Java動態代理實作方式一:InvocationHandler

InvocationHandler的原理我曾經專門寫文章介紹過:Java動態代理之InvocationHandler最簡單的入門教學

透過InvocationHandler, 我可以用一個EnginnerProxy代理類別來同時代理Developer和Tester的行為。

public class EnginnerProxy implements InvocationHandler {
    Object obj;
    public Object bind(Object obj)
    {
        this.obj = obj;
        return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
        .getClass().getInterfaces(), this);
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
    throws Throwable
    {
        System.out.println("Enginner writes document");
        Object res = method.invoke(obj, args);
        return res;
    }
}

真實類別的writeCode和doTesting方法在動態代理類別裡透過反射的方式執行。

測試輸出:

Java中靜態代理程式和動態代理程式的四種實作方法介紹

透過InvocationHandler實作動態代理的限制

假設有個產品經理類(ProductOwner) 沒有實作任何介面。

public class ProductOwner {
    private String name;
    public ProductOwner(String name){
        this.name = name;
    }
    public void defineBackLog(){
        System.out.println("PO: " + name + " defines Backlog.");
    }
}

我們仍然採取EnginnerProxy代理類別去代理它,編譯時不會出錯。運行時會發生什麼事?

ProductOwner po = new ProductOwner("Ross");

ProductOwner poProxy = (ProductOwner) new EnginnerProxy().bind(po);

poProxy.defineBackLog();

運行時報錯。所以限制就是:如果被代理的類別未實現任何接口,那麼不能採用透過InvocationHandler動態代理的方式去代理它的行為。

Java中靜態代理程式和動態代理程式的四種實作方法介紹

Java動態代理實作方式二:CGLIB

CGLIB是一個Java字節碼產生函式庫,提供了一個易用的API對Java字節碼進行建立和修改。關於這個開源庫的更多細節,請移步至CGLIB在github上的倉庫:https://github.com/cglib/cglib

我們現在嘗試用CGLIB來代理之前採用InvocationHandler沒有成功代理的ProductOwner類別(該類別未實作任何介面)。

現在我改為使用CGLIB API來建立代理類別:

public class EnginnerCGLibProxy {
    Object obj;
    public Object bind(final Object target)
    {
        this.obj = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(obj.getClass());
        enhancer.setCallback(new MethodInterceptor() {
            @Override
            public Object intercept(Object obj, Method method, Object[] args,
            MethodProxy proxy) throws Throwable
            {
                System.out.println("Enginner 2 writes document");
                Object res = method.invoke(target, args);
                return res;
            }
        }
        );
        return enhancer.create();
    }
}

測試程式碼:

ProductOwner ross = new ProductOwner("Ross");

ProductOwner rossProxy = (ProductOwner) new EnginnerCGLibProxy().bind(ross);

rossProxy.defineBackLog();

尽管ProductOwner未实现任何代码,但它也成功被代理了:

Java中靜態代理程式和動態代理程式的四種實作方法介紹

用CGLIB实现Java动态代理的局限性

如果我们了解了CGLIB创建代理类的原理,那么其局限性也就一目了然。我们现在做个实验,将ProductOwner类加上final修饰符,使其不可被继承:

Java中靜態代理程式和動態代理程式的四種實作方法介紹

再次执行测试代码,这次就报错了: Cannot subclass final class XXXX。

所以通过CGLIB成功创建的动态代理,实际是被代理类的一个子类。那么如果被代理类被标记成final,也就无法通过CGLIB去创建动态代理。

Java动态代理实现方式三:通过编译期提供的API动态创建代理类

假设我们确实需要给一个既是final,又未实现任何接口的ProductOwner类创建动态代码。除了InvocationHandler和CGLIB外,我们还有最后一招:

我直接把一个代理类的源代码用字符串拼出来,然后基于这个字符串调用JDK的Compiler(编译期)API,动态的创建一个新的.java文件,然后动态编译这个.java文件,这样也能得到一个新的代理类。

Java中靜態代理程式和動態代理程式的四種實作方法介紹

测试成功:

Java中靜態代理程式和動態代理程式的四種實作方法介紹

我拼好了代码类的源代码,动态创建了代理类的.java文件,能够在Eclipse里打开这个用代码创建的.java文件,

Java中靜態代理程式和動態代理程式的四種實作方法介紹

Java中靜態代理程式和動態代理程式的四種實作方法介紹

下图是如何动态创建ProductPwnerSCProxy.java文件:

Java中靜態代理程式和動態代理程式的四種實作方法介紹

下图是如何用JavaCompiler API动态编译前一步动态创建出的.java文件,生成.class文件:

Java中靜態代理程式和動態代理程式的四種實作方法介紹

下图是如何用类加载器加载编译好的.class文件到内存:

Java中靜態代理程式和動態代理程式的四種實作方法介紹

如果您想试试这篇文章介绍的这四种代理模式(Proxy Design Pattern), 请参考我的github仓库,全部代码都在上面。感谢阅读。

https://github.com/i042416/Ja...

以上是Java中靜態代理程式和動態代理程式的四種實作方法介紹的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除