面試問題:Java裡的代理設計模式(Proxy Design Pattern)一共有幾種實作方式?這個題目很像孔乙己問“茴香豆的茴字有哪幾種寫法?”
所謂代理模式,是指用戶端(Client)并不直接調用實際的對象(下圖右下角的RealSubject),而是通過調用代理(Proxy),來間接的調用實際的對象。
代理模式的使用場合,一般是由于用戶端不想直接通路實際對象,或者通路實際的對象存在技術上的障礙,因而通過代理對象作為橋梁,來完成間接通路。
實作方式一:靜态代理
開發一個接口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方法之前,加上一個寫文檔的調用,這樣就確定了程式員寫代碼時都伴随着文檔更新。
測試代碼:
靜态代理方式的優點
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;
}
}
通過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();
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();
}
}
再次執行測試代碼,這次就報錯了: Cannot subclass final class XXXX。
是以通過CGLIB成功建立的動态代理,實際是被代理類的一個子類。那麼如果被代理類被标記成final,也就無法通過CGLIB去建立動态代理。
Java動态代理實作方式三:通過編譯期提供的API動态建立代理類
假設我們确實需要給一個既是final,又未實作任何接口的ProductOwner類建立動态代碼。除了InvocationHandler和CGLIB外,我們還有最後一招:
我直接把一個代理類的源代碼用字元串拼出來,然後基于這個字元串調用JDK的Compiler(編譯期)API,動态的建立一個新的.java檔案,然後動态編譯這個.java檔案,這樣也能得到一個新的代理類。
如果您想試試這篇文章介紹的這四種代理模式(Proxy Design Pattern), 請參考我的github倉庫,全部代碼都在上面。感謝閱讀。
https://github.com/i042416/JavaTwoPlusTwoEquals5/tree/master/src/proxy