打造簡單的依賴注入功能

weixin_34006468發表於2017-08-30

讓程式碼幫我們自動寫程式碼。

目標:實現類似於Butterknife的id注入。
首先先了解Java 的註解吧。
註解的分類

SOURCE:只是用作標記,會被編譯器丟棄。如@Override、@SuppressWarnings

CLASS:編譯時動態處理。

RUNTIME:執行時處理,可能會用到反射

上面註解的保留時間。用於@Retention
還有一個@Target用以標記註解的作用物件
他的值是一個ElementType陣列

  /** Class, interface (including annotation type), or enum declaration */
TYPE,

/** Field declaration (includes enum constants) */
FIELD,

/** Method declaration */
METHOD,

/** Formal parameter declaration */
PARAMETER,

/** Constructor declaration */
CONSTRUCTOR,

/** Local variable declaration */
LOCAL_VARIABLE,

/** Annotation type declaration */
ANNOTATION_TYPE,

/** Package declaration */
PACKAGE,

/**
 * Type parameter declaration
 *
 * @since 1.8
 */
TYPE_PARAMETER,

/**
 * Use of a type
 *
 * @since 1.8
 */
TYPE_USE

有這些類別。我是從java的原始碼裡面直接拷貝出來的說明和定義。

因為我們是要注入id,所以Target就選擇FIELD了。
宣告一個註解用來注入程式碼View的id:

package com.example;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by lps on 2017/8/28.
 *
 * @version 1
 * @see
 * @since 2017/8/28 16:52
 */

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface InjectView {
int value();
}

這裡我們的Retention就是CLASS的
然後我們需要一個類來處理我們剛剛定義的註解。
我們編寫一個類繼承自AbstractProcessor。(在android中是找不到這個類的,需要在AS中新建一個JavaLib的Moudle才行,原因暫時未知)
並且需要在這個lib下面建立META-INF/services/javax.annotation.processing.Processor檔案
裡面填入我們的Processor的路徑(這一步也不是清楚,可能是jvm需要這樣的定義去解析吧,不定義是不能生成程式碼的)

我自己命名為ViewInjectorProcessor。其實就是通過編譯器編譯時解析到這裡,為我們建立ViewInjector。
遍歷有註解的類,分別建立class+$Injector.
比如這裡的demo就是MainActivity$Injector和Main2Activity$Injector.
其實寫這個Processor主要就是解析註解生成類的程式碼。用程式碼為我們的註解自動生成類和對應的處理程式碼。
process的實現是我們的核心。

package com.example;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;

/**
 * Created by lps on 2017/8/28.
 *
 * @version 1
 * @see
 * @since 2017/8/28 17:21
 */

@SupportedAnnotationTypes("com.example.InjectView")
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class ViewInjectorProcessor extends AbstractProcessor {
private static final String GEN_CLASS_SUFFIX = "$Injector";
private static final String INJECTOR_NAME = "ViewInjector";
private static final String TAG = "ViewInjectorProcessor";
private Types mTypeUtils;
private Elements mElementUtils;
private Filer mFiler;
private Messager mMessager;

@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
    super.init(processingEnvironment);

    mTypeUtils = processingEnv.getTypeUtils();
    mElementUtils = processingEnv.getElementUtils();
    mFiler = processingEnv.getFiler();
    mMessager = processingEnv.getMessager();
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//        使用了@InjectView註解的元素
    Set<? extends Element> mElementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(InjectView.class);
    if (mElementsAnnotatedWith.size() == 0) return true;
    Map<Element, List<Element>> elemtMap = new HashMap<>();
    StringBuffer buffer = new StringBuffer();
//        構建註解類
    buffer.append("package com.example;\n")
            .append("public class " + INJECTOR_NAME + "{\n");
    for (Element mElement : mElementsAnnotatedWith) {
        if (!isView(mElement.asType())) {
            mMessager.printMessage(Diagnostic.Kind.ERROR, "is not a View ", mElement);
        }
        Element clazz = mElement.getEnclosingElement();
        addElment(elemtMap, clazz, mElement);
    }
    System.out.println(TAG+elemtMap);
//        遍歷elemtmap,即遍歷所有的註解類
    for (Map.Entry<Element, List<Element>> entry : elemtMap.entrySet()) {
        Element clazz = entry.getKey();
        String classname = clazz.getSimpleName().toString();
        String packageName = mElementUtils.getPackageOf(clazz).asType().toString();
        generateInjectorCode(packageName, classname, entry.getValue());
        String fullName = clazz.asType().toString();
    //            拼接ViewInjector的inject方法程式碼
        buffer.append("\tpublic static void inject(" + fullName + " arg){\n")
                .append("\t\t" + fullName + GEN_CLASS_SUFFIX + ".inject(arg);\n")
                .append("\t}\n");

    }
    buffer.append("}");
//        生成ViewInjector類
    generateCode(INJECTOR_NAME, buffer.toString());
    return true;
}

/**
 * 生成程式碼
 * @param classname
 * @param code
 */
private void generateCode(String classname, String code) {
    try {
        JavaFileObject file = mFiler.createSourceFile(classname);
        Writer mWriter = file.openWriter();
        mWriter.write(code);
        mWriter.close();
    } catch (IOException mE) {
        mE.printStackTrace();
    }
}

/**
 * 生成注入器的程式碼如MainActivity$Injector
 * @param mPackageName
 * @param mClassname
 * @param views
 */
private void generateInjectorCode(String mPackageName, String mClassname, List<Element> views) {
    StringBuilder mBuilder = new StringBuilder();
    mBuilder.append("package " + mPackageName + ";\n\n")
            .append("public class " + mClassname + GEN_CLASS_SUFFIX + "{\n")
            .append("public static void inject(" + mClassname + " arg){\n");
//        對每個View 遍歷。生成findViewByid程式碼
    for (Element mElement : views) {
        String type = mElement.asType().toString();
        String name = mElement.getSimpleName().toString();
        int id = mElement.getAnnotation(InjectView.class).value();
        mBuilder.append("\t\targ." + name + "=(" + type + ")arg.findViewById(" + id + ");\n");

    }
    mBuilder.append("\t}\n").append("}");
    generateCode(mClassname + GEN_CLASS_SUFFIX, mBuilder.toString());
}

/**
 * 向mElemtMap中存入,elemt和List的鍵值對
 * @param mElemtMap
 * @param mClazz
 * @param mElement
 */
private void addElment(Map<Element, List<Element>> mElemtMap, Element mClazz, Element mElement) {
    List<Element> list = mElemtMap.get(mClazz);
    if (list == null) {
        list = new ArrayList<>();
        mElemtMap.put(mClazz, list);
    }
    list.add(mElement);
}
//判斷是否是View的子類
private boolean isView(TypeMirror mTypeMirror) {
    List<? extends TypeMirror> supers = mTypeUtils.directSupertypes(mTypeMirror);
    if (supers.size() == 0) {
        return false;

    }
    for (TypeMirror supertype :
            supers) {
        if (supertype.toString().equals("android.view.View") || isView(supertype)) {
            return true;
        }
    }
    return false;
}
}

程式碼如上。

@SupportedAnnotationTypes的值為當前類支援的註解的完整類路徑,支援萬用字元。    
@SupportedSourceVersion 標識該處理器支援的原始碼版本

最後就是在app中使用了,首先需要app依賴我們的lib,然後引入apt外掛。
在Project的gradle指令碼中新增程式碼

   classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

在app的gradle指令碼中新增plugin

apply plugin: 'com.neenbedankt.android-apt'

就可以在Activity中使用了。使用了註解,然後make project一下
就可以在app的build下看到apt生成的程式碼了

1795423-bbd42e98b7b485ea.png
image.png

相關文章