面試問題:Java裏的代理設計模式(Proxy Design Pattern)一共有幾種實現方式?這個題目很像孔乙己問「茴香豆的茴字有哪幾種寫法?」java
所謂代理模式,是指客戶端(Client)並不直接調用實際的對象(下圖右下角的RealSubject),而是經過調用代理(Proxy),來間接的調用實際的對象。git
代理模式的使用場合,通常是因爲客戶端不想直接訪問實際對象,或者訪問實際的對象存在技術上的障礙,於是經過代理對象做爲橋樑,來完成間接訪問。程序員
開發一個接口IDeveloper,該接口包含一個方法writeCode,寫代碼。github
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的工做,對着陌生的代碼一臉問號。經全組討論決定,每一個開發人員寫代碼時,必須同步更新文檔。ide
爲了強迫每一個程序員在開發時記着寫文檔,而又不影響你們寫代碼這個動做自己, 咱們不修改原來的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方法以前,加上一個寫文檔的調用,這樣就確保了程序員寫代碼時都伴隨着文檔更新。this
測試代碼:spa
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的動態代理實現方式。
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方法在動態代理類裏經過反射的方式進行執行。
測試輸出:
經過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動態代理的方式去代理它的行爲。
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未實現任何代碼,但它也成功被代理了:
若是咱們瞭解了CGLIB建立代理類的原理,那麼其侷限性也就一目瞭然。咱們如今作個實驗,將ProductOwner類加上final修飾符,使其不可被繼承:
再次執行測試代碼,此次就報錯了: Cannot subclass final class XXXX。
因此經過CGLIB成功建立的動態代理,實際是被代理類的一個子類。那麼若是被代理類被標記成final,也就沒法經過CGLIB去建立動態代理。
Java動態代理實現方式三:經過編譯期提供的API動態建立代理類
假設咱們確實須要給一個既是final,又未實現任何接口的ProductOwner類建立動態代碼。除了InvocationHandler和CGLIB外,咱們還有最後一招:
我直接把一個代理類的源代碼用字符串拼出來,而後基於這個字符串調用JDK的Compiler(編譯期)API,動態的建立一個新的.java文件,而後動態編譯這個.java文件,這樣也能獲得一個新的代理類。
測試成功:
我拼好了代碼類的源代碼,動態建立了代理類的.java文件,可以在Eclipse裏打開這個用代碼建立的.java文件,
下圖是如何動態建立ProductPwnerSCProxy.java文件:
下圖是如何用JavaCompiler API動態編譯前一步動態建立出的.java文件,生成.class文件:
下圖是如何用類加載器加載編譯好的.class文件到內存:
若是您想試試這篇文章介紹的這四種代理模式(Proxy Design Pattern), 請參考個人github倉庫,所有代碼都在上面。感謝閱讀。
https://github.com/i042416/Ja...
要獲取更多Jerry的原創技術文章,請關注公衆號"汪子熙"或者掃描下面二維碼: