前言
最近在學習的過程中,發現了一個問題,抽像類在沒有實現所有的抽象方法前是不可以通過new來構建該對象的,但是抽象方法卻是可以有自己的構造方法的。這樣就把我搞糊塗了,既然有構造方法,又不可以通過new來創建,那麼抽像類在沒變成具體類的時候究竟可不可以實例化呢?
在Java 中抽像類是不能直接被實例化的。但是很多時候抽像類的該特點成為一個比較麻煩的阻礙。例如如果我想使用動態代理來給一個抽像類賦予其執行抽象方法的能力,就會有兩個困難:1. 動態代理只能創建實現接口的一個代理對象,而不能是一個繼承抽像類的對象。為此標準的JVM 中有一些實現,例如javassist 可以使用字節碼工具來完成這一目的(ProxyFactory)。
在Android 中如果想構造一個抽像類對象,恐怕只有new ClassName() {}或者繼承之後構造了。但是這兩種方法都是不能由其Class 對象直接操作的,這就導致一些問題上達不到我們需要的抽象能力。
這裡詳細描述一下第一段所說的場景:
首先有一個interface 文件定義如下(熟悉Android 的朋友可以看出這是一個提供給Retrofit 生成代理對象的Api 配置接口):
public interface RealApi { @GET("api1") Observable<String> api1(); @GET("api2") Observable<String> api2(); @GET("api3") Observable<String> api3(); //...其他方法}其次再寫一個抽像類,只實現接口的其中一個方法(用來模擬接口數據):
@MockApipublic abstract class MockApi implements RealApi { Observable<String> api3() { return Observable.just("mock data"); }}然後我們需要有一個工具,例如MockManager ,讓他結合我們已存在的RealApi 對象和MockApi 類,來構造出一個混合對象,該對像在執行MockApi 中已經定義的方法時,為直接執行,在MockApi 沒有定義該方法時,去調用RealApi 的方法。其調用方式大概為:
RealApi api = MockManager.build(realApi, MockApi.class);
通過javassist,完成上述功能很簡單,創建一個ProxyFactory 對象,設置其Superclass 為MockApi,然後過濾抽象方法,設置method handler 調用realApi 對象的同名同參方法。這裡就不再給出代碼實現。
但是在Android 上,javassist 的該方法會拋出
Caused by: java.lang.UnsupportedOperationException: can't load this type of class file at java.lang.ClassLoader.defineClass(ClassLoader.java:520) at java.lang.reflect.Method.invoke(Native Method) at javassist.util.proxy.FactoryHelper.toClass2(FactoryHelper.java:182)
類似的異常。原因大概是Android 上的虛擬機的實現和標準略微不同,所以這裡把方向轉為了動態代碼生成的另一個方向Annotation Processor。
使用Annotation Processor 實現的話,思路就簡單的多了,但過程還是有些曲折:
首先定義一個註解,用來標記需要構造對象的抽像類
@Target(ElementType.TYPE)@Documented@Retention(RetentionPolicy.SOURCE)public @interface MockApi {} Processor 根據註解來獲得類的element 對象,該對像是一個類似class 的對象。因為在預編譯階段,class 尚未存在,此時使用Class.forName是不可以獲取運行時需要的Class 對象的,但是Element 提供了類似Class 反射相關的方法,也有TypeElement、ExecutableElement 等區分。使用Element 對象分析註解的抽像類的抽象方法有哪些,生成一個繼承該類的實現類(非抽象),並在該類中實現所有抽象方法,因為不會實際用到這些抽象方法,所以只需要能編譯通過就可以了,我選擇的方式是每個方法體都拋出一個異常,提示該方法為抽象方法不能直接調用。生成代碼的方法可以使用一些工具來簡化工作,例如AutoProcessor 和JavaPoet,具體實現參考文尾的項目代碼,生成後的代碼大致像這樣:
// 生成的類名使用原類名+"$Impl"的後綴來命名,避免和其他類名衝突,後面也使用該約束進行反射來調用該類public final class MockApi$Impl extends MockApi { @Override public Observable<String> api1() { throw new IllegalStateException("api1() is an abstract method!"); } @Override public Observable<String> api2() { throw new IllegalStateException("api2() is an abstract method!"); }}根據該抽像類的類名去反射獲得該實現類,然後再根據反射調用其構造方法構造出一個實現對象。
// 獲得生成代碼構造的對象private static <T> T getImplObject(Class<T> cls) { try { return (T) Class.forName(cls.getName() + "$Impl").newInstance(); } catch (Exception e) { return null; }}構造一個動態代理,傳入RealApi 的真實對象,和上一步構造出的抽像類的實現對象,根據抽像類中的定義來判斷由哪個對象代理其方法行為:如果抽像類中有定義,即該方法不是抽象方法,則抽像類的實現對象執行;反之,由接口的真實對象執行。
public static <Origin, Mock extends Origin> Origin build(final Origin origin, final Class<Mock> mockClass) { // 如果Mock Class 標記為關閉,則直接返回真實接口對象if (!isEnable(mockClass)) { return origin; } final Mock mockObject = getImplObject(mockClass); Class<?> originClass = origin.getClass().getInterfaces()[0]; return (Origin) Proxy.newProxyInstance(originClass.getClassLoader(), new Class[]{originClass}, new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { // 獲取定義的抽像類中的同名方法,判斷是否已經實現Method mockMethod = null; try { mockMethod = mockClass.getDeclaredMethod(method.getName(), method.getParameterTypes()); } catch (NoSuchMethodException ignored) { } if (mockMethod == null || Modifier.isAbstract(mockMethod.getModifiers())) { return method.invoke(origin, objects); } else { return mockMethod.invoke(mockObject, objects); } } });}完成上述工作以後,就可以像開頭所說的那樣,使用build 方法來構造一個混合了真實接口和抽像類方法的代理對象了,雖然調用的類本質上還是硬編碼,但是由Annotation Processor 自動生成免於手動維護,使用上來講和使用Javassist 實現還是基本相同的。
我用本文中所屬的方法實現了一個模擬retrofit 請求的工具(文尾有鏈接),但本質上可以用它來實現很多需要構造抽像類的需求,更多的使用場景還有待挖掘。
文中提到的源碼實現可以在項目retrofit-mock-result 或本地下載中找到;
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。