運用動態代理來提高程式碼的靈活性

weixin_34290000發表於2017-09-10

前言

本文會簡單介紹下 Java 中動態代理模式的使用,然後著重分享下動態代理如何在提高程式碼靈活性方面大展身手。文中會列舉兩個例項,一個是在 MVP 中如何巧妙解決 Presenter 中頻繁使用 if (getView() != null) { } 這種重複程式碼的問題;另一個是在專案中如何讓多個 modules 間解耦更加靈活、更加純粹的問題。

動態代理的基本使用

要使用動態代理,主要涉及到兩個類,一個是 Proxy 類,一個是 InvocationHandler 類。在介紹如何使用之前,需要明確的是:動態代理的代理物件只能是 Interface,不能是 Class ,也不能是 abstract class。這是因為所有動態生成的代理類都繼承自 Proxy。而 java 是單繼承的,所以只有介面物件能被動態代理。

回到剛才介紹的兩個類,Proxy 描述了一個代理物件,同時它提供了建立並例項化一個代理物件的靜態方法。InvocationHandler 是一個代理物件的呼叫處理器,它只有一個 invoke 方法,所有被代理的物件的方法呼叫都會通過這個方法執行,我們的代理行為也就是在這個方法裡面實現的。下面給出一個很簡單的示例:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class DemonstrationProxy {

    interface A {
        void method();
    }


    static class AIpml implements A {

        @Override
        public void method() {
            System.out.println("method in AIpml");
        }
    }

    //動態代理物件
    static class AProxy implements InvocationHandler {
        
        //被代理的物件例項
        final Object origin;

        AProxy(Object origin) {
            this.origin = origin;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("start to invoke method " + method.getName() + " proxy is " + proxy.getClass().getCanonicalName());
            //執行實際物件的方法
            return method.invoke(origin, args);
        }
    }

    public static void main(String[] args) {
        final A a = new AIpml();
        final InvocationHandler handler = new AProxy(a);
        final A proxyA = (A) Proxy.newProxyInstance(a.getClass().getClassLoader(), a.getClass().getInterfaces(), handler);
        proxyA.method();
    }
}

編譯執行後你將看到如下輸出:

start to invoke method proxy is $Proxy0
method in AIpml

MVP 中如何精簡不必要的程式碼

在 MVP 的開發模式中,Presenter 持有 View 的引用,當我們需要與 View 進行互動時,通過 getView() 方法獲得 View 物件。為了避免記憶體洩漏,在不需要 Presenter 的時候(比如在 Activity 的 onDestroyed() 生命週期)將 View 物件置空。然而此時可能一些非同步任務沒有結束,當它們結束後,getView() 就會返回 null 。為了避免 NPE,通常的做法是在所有非同步任務裡需要訪問 View 的地方,都要進行 if(getView() != null) { } 這樣的檢查。當你寫了好幾個 Presenter 之後,便會發現這是一件很煩的事情,不僅僅是因為每次要寫同樣的東西,還有就是:

It's always a bad sign when the else branch is missing.

關於這個問題,在 Medium 上也有討論,上面也列出了一些解決方案。比如用 ThirtyInch 這個第三方 MVP 框架,它把所有對 View 的操作封裝為一個個的 ViewActionTiPresenter 內部會管理這些 ViewAction 的執行。只有在 View attach 到 Presenter 的時候,才會執行 ViewAction,否則會保留 ViewAction 直到 View 再次 attach 到 Presenter。還有就是用 WeakReference 或者 Optional 來管理 View 。對於第一個,算是一個不錯的解決方案,但它作為一個框架,使用它有一定的引入成本,還有另一個弊端就是 Presenter 和 View 的生命週期繫結得更加緊密,增加了 ViewAction 的維護成本 。對於第二個方案,感覺像是轉移話題一樣,並沒有解決什麼根本問題。

其實這個問題的源頭在於 getView() 方法是 nullable 的,如果該方法返回的 View 能確保非空,而且不存在記憶體洩漏問題,且無論是 View 處於哪種生命週期程式碼都能得到正確的呼叫,那麼問題就解決了。

此時就輪到動態代理出場了,當 View 還沒結束的時候,getView() 物件返回的是真實的 View 物件,而當 View 的生命週期結束後,getView() 物件只需要返回一個代理 View 即可,這樣就確保了 getView() 不會返回一個空的物件,自然就不需要反覆檢查,而且代理物件並不會對真實的 View 有任何的影響,所以程式碼邏輯也不會有任何問題。

Talk is cheap. Show me the code.

public class AbsPresenter<View extends IView> implements IPresenter {

    private View mView;
    private Class<? extends IView> mViewClass;

    public AbsPresenter(@NonNull View iView) {
        this.mView = iView;
        this.mViewClass = iView.getClass();
        if (this.mViewClass.getInterfaces().length == 0) {
            throw new IllegalArgumentException("iView must implement IView interface");
        }
    }

    public void detach() {
        this.mView = null;
    }

    public @NonNull View getView() {
        if (mView == null) {
            return ViewProxy.newInstance(mViewClass);
        }
        return mView;
    }

    private static final class ViewProxy implements InvocationHandler {

        public static <View> View newInstance(Class<? extends IView> clazz) {
            return (View) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), new ViewProxy());
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Type type = method.getReturnType();
            if (type == boolean.class) {
                return false;
            } else if (type == int.class) {
                return 0;
            } else if (type == short.class) {
                return (short)0;
            } else if(type == char.class) {
                return (char)0;
            } else if (type == byte.class) {
                return (byte)0;
            } else if(type == long.class) {
                return 0L;
            } else if (type == float.class) {
                return 0f;
            } else if (type == double.class) {
                return 0D;
            } else {
                return null;
            }
        }
    }
}

這就是我在流利說專案裡抽象出的 Presenter,主要展示了 getView() 方法是怎麼利用動態代理保證返回值為非空。如此一來,就大可放心地在 Presenter 中使用 getView() 方法,而不用擔心 NPE ,也不用擔心記憶體洩漏,程式碼還能更乾淨,一舉三得!

模組間結構如何更加靈活,更加精簡

流利說專案裡有很多代表不同功能的模組,為了將模組間解耦,我們在一個公共的模組中定義各個功能模組對外開放的介面,並在各自模組中實現。此外還需要一個類 (以下稱作 ModuleProvider) 管理這些模組介面,它需要向外提供介面的 setget 方法。在 Application 初始化的時候,通過反射將這些介面例項化,然後各個模組便可以通過 ModuleProvider 的 get 方法獲取其他模組的介面。一切看起來既美好又和諧,可是這裡有個問題,如果我正在開發 A 模組,為了更快的編譯速度,我在 build.gradle 中去掉了 B 模組,ModuleProvider.getB() 將會返回為 null ,那麼 A 模組中很多地方都會出現 NPE 。當然這可以通過判空解決,但顯然很蠢,而且如此一來,程式碼豈不是解耦地不徹底?

一開始的做法是對於所有的介面都有一個預設的空實現,對應到上面的例子就是 ModuleProvider.getB() 會有兩個不同的返回結果,一個是在 B 模組內對介面的實現,另一個是在公共模組的一個空實現 。如此一來,ModuleProvider.getB()方法就變成了這樣:

public static B getB() {
    if (b == null) {
      b = new EmptyB();
    }
    return b;
  }

現在隨意去掉不想要的模組也能愉快地敲程式碼了。

這樣的實現看起來已經很不錯了,可還是有優化的空間。問題在於每一個介面都有兩個實現,每次要對介面作修改的時候,要同時維護兩個實現類,而且其中一個實現並沒有實際的作用,更多地是隻想把精力放在模組中的實現上。這個時候動態代理又可以大顯身手了。既然只是一個空實現,那麼當模組不存在時返回一個介面的動態代理不就好了嗎?最重要的是,現在不需要同時維護兩個實現,可以集中精力在有意義的改動上。為此我在專案中提供了一個生成介面代理的工具類:

import com.xxx.xxx.annotations.SpecifyBooleanValue;
import com.xxx.xxx.annotations.SpecifyClassValue;
import com.xxx.xxx.annotations.SpecifyIntegerValue;
import com.xxx.xxx.annotations.SpecifyStringValue;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;


public class EmptyModuleProxy implements InvocationHandler {

    public static <T> T newInstance(Class<T> clazz) {
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new EmptyModuleProxy());
    }

   @Override
   public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
       final SpecifyClassValue specifyClassValue = method.getAnnotation(SpecifyClassValue.class);
       if (specifyClassValue != null) return specifyClassValue.returnValue();
       final SpecifyIntegerValue specifyIntegerValue = method.getAnnotation(SpecifyIntegerValue.class);
       if (specifyIntegerValue != null) return specifyIntegerValue.returnValue();
       final SpecifyStringValue specifyStringValue = method.getAnnotation(SpecifyStringValue.class);
       if(specifyStringValue != null) return specifyStringValue.returnValue();
       final SpecifyBooleanValue specifyBooleanValue = method.getAnnotation(SpecifyBooleanValue.class);
       if(specifyBooleanValue != null) return specifyBooleanValue.returnValue();
       return defaultValueByType(method.getReturnType());
   }

   private Object defaultValueByType(Class type) {
       if (type == boolean.class) {
           return false;
       } else if (type == int.class) {
           return 0;
       } else if (type == short.class) {
           return (short)0;
       } else if(type == char.class) {
           return (char)0;
       } else if (type == byte.class) {
           return (byte)0;
       } else if(type == long.class) {
           return 0L;
       } else if (type == float.class) {
           return 0f;
       } else if (type == double.class) {
           return 0D;
       } else {
           return null;
       }
   }
}

這樣一來ModuleProvider.getB() 方法就變成這樣:

public static B getB() {
    if (b == null) {
      b = EmptyModuleProxy.newInstance(B.class);
    }
    return b;
  }

至此,通過動態代理完美地解決了問題。注意到 EmptyModuleProxy 中還有很多註解,這是因為當一些模組沒有引入的時候,希望它的某些介面能返回一些指定的值以方便測試,所以額外定義了一些註解來解決這個問題。比如 B 介面裡面定義了一個 getBoolean() 方法,預設返回的是 false ,但實際上我希望在沒有引入 B 模組的時候返回 true。那麼 B 介面就可以做如下宣告:

public interface B {

    @SpecifyBooleanValue(returnValue = true)
    Class<?> getBoolean();
}

總結

通過 MVP 和模組間解耦這兩個實際專案中的例子,能夠充分地說明動態代理技術的運用,能夠給我們的程式碼帶來很多靈活性,讓一些實現變得更加簡潔、也更加優雅。當然動態代理能帶給我們的不僅僅是靈活性。比如 Retrofit 就通過動態代理將我們宣告的各種 Service 介面轉換為一個個的 ServiceMethod ,然後交給 OkHttp 執行具體的網路操作,從而讓網路請求變得如此優雅自然。所以說合理地運用這項技術,能讓你把程式碼敲地更嗨!

相關文章