反射、註解與依賴注入總結

linghu_java發表於2016-04-21

現在在我們構建自己或公司的專案中,或多或少都會依賴幾個流行比較屌的第三方庫,比如:Butter KnifeRetrofitDagger 2等,如果你沒用過,那你需要找時間補一下啦;有時在使用後我們會好奇他們到底是怎麼做到這種簡潔、高效、鬆耦合等諸多優點的,當然這裡我不探討它們具體怎麼實現的,而關心的是它們都用到同樣的技術反射和註解,並實現的依賴注入。

如果你好奇這些庫具體是怎麼實現的,或者想了解他們實現的原理,這裡向你推薦幾篇文章:
1、android註解Butterknife的使用及程式碼分析
2、Retrofit原始碼1: 為什麼寫一個interface就可以實現http請求
3、Retrofit分析-漂亮的解耦套路
4、Dagger 原始碼解析
5、Android:dagger2讓你愛不釋手-基礎依賴注入框架篇
6、Android:dagger2讓你愛不釋手-重點概念講解、融合篇
7、Android:dagger2讓你愛不釋手-終結篇

這些好文章已經幫你收藏了,下面直接進入我的主題【反射、註解與依賴注入總結】。

● 反射(Reflection)

反射的概念

主要是指程式可以訪問,檢測和修改它本身狀態或行為的一種能力,並能根據自身行為的狀態和結果,調整或修改應用所描述行為的狀態和相關的語義。

概念看著就有些暈或不知所云啦,可以通過反射的作用理解它的概念。

反射的作用

反射可以讓我們在執行時獲取類的屬性,方法,構造方法、父類、介面等資訊,通過反射還可以讓我們在執行期例項化物件、呼叫方法、即使方法或屬性是私有的的也可以通過反射的形式呼叫。

所有為什麼第三方庫基本都會使用到反射,正是因為反射這種 “看透 Class” 的能力。

反射相關的類、方法

要看透一個類,首先要獲取這個類的物件,其它資訊都是通過這個物件獲取的,下面的所有的示例具體操作程式碼請參考 【個人學習專案DroidStudy】,我在這個工程下新建一個 ReflectionActivity,包的路徑為 com.sun.study.ui.activity.ReflectionActivity,通過反射相關的類、方法讓我看透這個類。

1、獲取物件的三種方式:

第一種、知道一個類,直接獲取 Class 物件

Class<?> cls1 = ReflectionActivity.class;

第二種、如果已經得到了某個物件,可以通過這個物件獲取 Class 物件

ReflectionActivity activity = new ReflectionActivity();
Class<?> cls2 = activity.getClass();

第三種、如果你在編譯期獲取不到目標型別,但是你知道它的完整類路徑,那麼你可以通過如下的形式來獲取 Class 物件,這樣獲取可能會丟擲異常 ClassNotFoundException。

try {
    Class<?> cls3 = Class.forName("com.sun.study.ui.activity.ReflectionActivity");
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

2、反射的相關方法和示例

列出反射的相關方法

getName():獲得類的完整名字。  
newInstance():通過類的不帶引數的構造方法建立這個類的一個物件。

getFields():獲得類的public型別的屬性。  
getDeclaredFields():獲得類的所有屬性。

getMethods():獲得類的public型別的方法。  
getDeclaredMethods():獲得類的所有方法。  
getMethod(String name, Class[] parameterTypes):獲得類的特定方法。

getModifiers()和Modifier.toString():獲得屬修飾符,例如privatepublicstatic等  
getReturnType():獲得方法的返回型別  
getParameterTypes():獲得方法的引數型別

getConstructors():獲得類的public型別的構造方法。  
getConstructor(Class[] parameterTypes):獲得類的特定構造方法。

getSuperclass():獲取某類的父類  
getInterfaces():獲取某類實現的介面

示例一:獲得類的所有方法(Method)資訊

private void getMethodsInfo() {
    Class<ReflectionActivity> cls = ReflectionActivity.class;
    Method[] methods = cls.getDeclaredMethods();
    if (methods == null) return;

    StringBuilder sb = new StringBuilder();
    for (Method method:methods) {
        sb.append(Modifier.toString(method.getModifiers())).append(" ");
        sb.append(method.getReturnType()).append(" ");
        sb.append(method.getName()).append("(");
        Class[] parameters = method.getParameterTypes();
        if (parameters != null) {
            for (int i=0; i<parameters.length; i++) {
                Class paramCls = parameters[i];
                sb.append(paramCls.getSimpleName());
                if (i < parameters.length - 1) sb.append(", ");
            }
        }
        sb.append(")\n\n");
    }

    tvInfo.setText(sb.toString());
}

執行結果如下圖:


reflection_icon1.png

示例一:獲得類的所有屬性(Field)資訊,並修改型別Int屬性i的值

private void modifyFieldValue() {
    Class<ReflectionActivity> cls = ReflectionActivity.class;
    Field[] fields = cls.getDeclaredFields();
    if (fields == null) return;

    StringBuilder sb = new StringBuilder();
    sb.append("獲得類的所有屬性資訊:\n\n");
    for (Field field:fields) {
        sb.append(Modifier.toString(field.getModifiers())).append(" ");
        sb.append(field.getType().getSimpleName()).append(" ");
        sb.append(field.getName()).append(";");
        sb.append("\n\n");
    }

    try {
        sb.append("屬性i的預設值:i = ");
        Field f = cls.getDeclaredField("i");
        sb.append(f.getInt("i")).append("\n\n");
        f.set("i", 100);
        sb.append("屬性i修改後的值:i = ");
        sb.append(f.getInt("i")).append("\n\n");
    } catch (Exception e) {
        e.printStackTrace();
    }

    tvInfo.setText(sb.toString());
    toolbar.setSubtitle("修改型別Int屬性i的值");
}

執行結果如下圖:


reflection_icon2.png

更多示例請參考 【個人學習專案DroidStudy】

反射的相關內容先記錄到這,接下來看看註解相關概念與使用。

● 註解(Annotation)

註解的概念

註解(Annotation),也叫後設資料。一種程式碼級別的說明。它是JDK 1.5及以後版本引入的一個特性,與類、介面、列舉是在同一個層次。它可以宣告在包、類、欄位、方法、區域性變數、方法引數等的前面,用來對這些元素進行說明,註釋。

註解的作用

1、標記作用,用於告訴編譯器一些資訊讓編譯器能夠實現基本的編譯檢查,如@Override、Deprecated,看下它倆的原始碼

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Deprecated {
}

2、編譯時動態處理,動態生成程式碼,如Butter KnifeDagger 2

3、執行時動態處理,獲得註解資訊,如Retrofit

註解的分類

註解的分類有兩種分法:

第一種分法

1、基本內建註解,是指Java自帶的幾個Annotation,如@Override、Deprecated、@SuppressWarnings等

2、元註解(meta-annotation),是指負責註解其他註解的註解,JDK 1.5及以後版本定義了4個標準的元註解型別,如下:

1@Target
2@Retention
3@Documented
4@Inherited

3、自定義註解,根據需要可以自定義註解,自定義註解需要用到上面的meta-annotation

第二種分法,根據作用域分類

1、原始碼時註解(RetentionPolicy.SOURCE)
2、編譯時註解(RetentionPolicy.CLASS)
3、執行時註解(RetentionPolicy.RUNTIME)

註解相關知識點

1、元註解相關資訊

@Target:指Annotation所修飾的物件範圍,通過ElementType取值有8種,如下

TYPE:類、介面(包括註解型別)或列舉
FIELD:屬性
METHOD:方法
PARAMETER:引數
CONSTRUCTOR:建構函式
LOCAL_VARIABLE:區域性變數
ANNOTATION_TYPE:註解型別
PACKAGE:包

@Retention:指Annotation被保留的時間長短,通過RetentionPolicy取值有3種,如下:

SOURCE:在原始檔中有效(即原始檔保留)  
CLASS:在class檔案中有效(即class保留)  
RUNTIME:在執行時有效(即執行時保留)

@Documented:是一個標記註解,用於描述其它型別的註解應該被作為被標註的程式成員的公共API,因此可以被例如javadoc此類的工具文件化。

@Inherited:也是一個標記註解,@Inherited闡述了某個被標註的型別是被繼承的

2、註解定義格式

public @interface 註解名 { 定義體 }

3、註解引數可支援的資料型別:

8種基本資料型別 intfloatbooleanbytedoublecharlongshort  
String、Class、enum、Annotation  
以上所有型別的陣列

4、⚠注意:自定義註解如果只有一個引數成員,最好把定義體引數名稱設為"value",如@Target

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {
    ElementType[] value();
}

看一個示例

具體要求和執行結果都在下面這張圖上顯示出來了,貼下圖


annotation_icon.png

再貼三塊程式碼,首先是自定義註解程式碼:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface RequestAnnotation {
    boolean withDialog() default true;
    String withMessage() default "正在載入,請稍後...";
}

其次是執行模擬的網路請求,核心程式碼是通過上面的反射和註解完成的;具體詳細程式碼請參考 【個人學習專案DroidStudy】,下次使用動態代理和Google的dexmaker完成這個功能,敬請關注,如果你對執行緒池還不清晰請參考我以前的文章【執行緒、多執行緒與執行緒池總結】。貼下核心程式碼:

// 執行緒池
private static ExecutorService pool = Executors.newCachedThreadPool();

// 模擬處理網路請求
public boolean process(final Class<?> clazz, String methodName, final Object... args) throws Exception {
    Class[] argsClass = getClazzByArgs(args);

    final Method method = clazz.getDeclaredMethod(methodName, argsClass);
    if (method == null) {
        sendMsg(TYPE_ERROR);
        return false;
    }

    // 獲取註解資訊
    RequestAnnotation annotation = method.getAnnotation(RequestAnnotation.class);
    if (annotation != null && annotation.withDialog()) {
        loadingDialog.show(annotation.withMessage());
    }

    pool.execute(new Runnable() {
        @Override
        public void run() {
            try {
                method.setAccessible(true);
                method.invoke(clazz.newInstance(), args);
                sendMsg(TYPE_SUCCESS);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
    return true;
}

最後是呼叫網路請求介面:

@RequestAnnotation(withDialog = false, withMessage = "正在載入,請稍後...")
public void apiTestFunc(String param1, String param2) {
    try {
        // 模擬網路請求的耗時操作
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

// 點選執行的程式碼
DynamicProxyUtil proxyUtil = new DynamicProxyUtil(AnnotationActivity.this);
proxyUtil.process(RequestNetworkApi.class, "apiTestFunc", "引數一", "引數二");

● 依賴注入(Dependency Injection)

依賴注入(Dependency Injection):可以通過這個服務來安全的注入元件到應用程式中,在應用程式部署的時候還可以選擇從特定的介面屬性進行注入。

看完上面反射和註解的記錄後,可以更好的理解依賴注入,如果你不用那些第三方的注入庫你也在經常用到依賴注入,比如下面這一段從codekk上擷取的程式碼:

public class Human {
    ...
    Father father;
    ...
    public Human(Father father) {
        this.father = father;
    }
}

上面程式碼中,我們將 father 物件作為建構函式的一個引數傳入。在呼叫 Human 的構造方法之前外部就已經初始化好了 Father 物件。像這種非自己主動初始化依賴,而通過外部來傳入依賴的方式,我們就稱為依賴注入。

依賴注入的實現有多種途徑,而在 Java 中,使用註解是最常用的。比如通過Butter Knife、Dagger依賴注入庫實現,都是使用註解來實現依賴注入,但它利用 APT(Annotation Process Tool) 在編譯時生成輔助類,這些類繼承特定父類或實現特定介面,程式在執行時載入這些輔助類,呼叫相應介面完成依賴生成和注入。

依賴注入在這裡僅僅剖析下概念,有時間將會補一個例子,暫且到這吧。



文/孫福生微博(簡書作者)
原文連結:http://www.jianshu.com/p/24820bf3df5c
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。

相關文章