JavaPoet-編譯時注入代碼

JavaPoet 是 Square 公司推出的開源 Java代碼生成框架,提供接口生成 Java 源文件。

它的項目主頁及源碼:https://github.com/square/javapoet

這個框架功能非常有用,我們可以很方便的使用它根據註解、數據庫模式、協議格式等來對應生成代碼。通過這種自動化生成代碼的方式,可以讓我們用更加簡潔優雅的方式要替代繁瑣冗雜的重複工作。

例如業內的一些開源庫 Butterknife,GreenDao,Dagger 2 等等,也都有運用 JavaPoet 結合註解來實現編譯時動態生成代碼。

以下是 JavaPoet 裏面的一些關鍵類:
JavaFile : 用於構造輸出包含一個頂級類的Java文件
TypeSpec :   生成類,接口,或者枚舉
MethodSpec :    生成構造函數或方法
FieldSpec  :  生成成員變量或字段
ParameterSpec :   用來創建參數
AnnotationSpec  :  用來創建註解

 

下面參考 Butterknife 源碼,實現綁定 View 和設置監聽的功能:

1,首先是 註解類:

@Retention(CLASS)
@Target(FIELD)
public @interface BindView {
  int value();
}

2,繼承 AbstractProcessor ,實現 Processor :

@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor {
    private Filer filer;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        // 代碼 輸出到 哪
        filer = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        // 拿到 每個類,要生成的 代碼塊;
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = findAndParseTargets(env);
        for (TypeElement typeElement : builderMap.keySet()) {
            List<CodeBlock.Builder> codeList = builderMap.get(typeElement);
            // 去生成對應的 類文件;
            ViewBindHelper.writeBindView(typeElement, codeList, filer);
        }
        return true;
    }

    private Map<TypeElement, List<CodeBlock.Builder>> findAndParseTargets(RoundEnvironment env) {
        Map<TypeElement, List<CodeBlock.Builder>> builderMap = new HashMap<>();

        // 遍歷帶 對應註解的 元素,具體來看實際也就是 某個View對象
        for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
            ViewBindHelper.parseBindView(element, builderMap);
        }

        // 遍歷帶 對應註解的 元素,具體來看實際也就是 某個方法
        for (Element element : env.getElementsAnnotatedWith(OnClick.class)) {
            ViewBindHelper.parseListenerView(element, builderMap);
        }
        return builderMap;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        types.add(OnClick.class.getCanonicalName());
        return types;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

}

下面來看幾個關鍵性代碼,ViewBindHelper 類的 parseBindView 方法:

public static void parseBindView(Element element, Map<TypeElement, List<CodeBlock.Builder>> codeBuilderMap) {
        // 獲取最裏面的節點, 具體實際可能就是 某個Activity對象
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        // 這個view是哪個類 Class
        String typeMirror = element.asType().toString();
        // 註解的值,具體實際可能就是 R.id.xxx
        int annotationValue = element.getAnnotation(BindView.class).value();
        // 這個view對象 名稱
        String name = element.getSimpleName().toString();

        //創建代碼塊
        CodeBlock.Builder builder = CodeBlock.builder()
                .add("target.$L = ", name); //$L是佔位符,會把後面的 name 參數拼接到 $L 所在的地方
        builder.add("($L)source.findViewById($L)", typeMirror, annotationValue);

        List<CodeBlock.Builder> codeList = codeBuilderMap.get(enclosingElement);
        if (codeList == null) {
            codeList = new ArrayList<>();
            codeBuilderMap.put(enclosingElement, codeList);
        }
        codeList.add(builder);
    }

看到這裏你就知道根據 BindView 註解怎麼生成 findViewById 的代碼了。OnClick註解也是類似。

最後看怎麼生成Java文件,ViewBindHelper 類的 writeBindView 方法:

    public static void writeBindView(TypeElement enclosingElement, List<CodeBlock.Builder> codeList, Filer filer) {
        // enclosingElement ,暗指 某個Activity.
        // 先拿到 Activity 所在包名
        String packageName = enclosingElement.getQualifiedName().toString();
        packageName = packageName.substring(0, packageName.lastIndexOf("."));
        // 再拿到 Activity 類名
        String className = enclosingElement.getSimpleName().toString();

        // 再拿到 Activity 是 哪個類 
        TypeName type = TypeName.get(enclosingElement.asType());//此元素定義的類型
        if (type instanceof ParameterizedTypeName) {
            type = ((ParameterizedTypeName) type).rawType;
        }

        ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");

        // 創建構造方法 如果 Activity是 MainActivity,則會有 生成如下構造方法
//        public MainActivity_ViewBinding(final MainActivity target, final View source) {
//            target.btn1 = (android.widget.Button)source.findViewById(2131165217);
//            source.findViewById(2131165217).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onBtn1Click(v); }});
//        }
        MethodSpec.Builder methodSpecBuilder = MethodSpec.constructorBuilder()
                .addModifiers(Modifier.PUBLIC)
                .addParameter(type, "target", Modifier.FINAL)
                .addParameter(ClassName.get("android.view", "View"), "source", Modifier.FINAL);
        for (CodeBlock.Builder codeBuilder : codeList) {
            //方法裏面 ,代碼是什麼
            methodSpecBuilder.addStatement(codeBuilder.build());
        }
        methodSpecBuilder.build();

        // 創建類 MainActivity_ViewBinding
        TypeSpec bindClass = TypeSpec.classBuilder(bindingClassName.simpleName())
                .addModifiers(Modifier.PUBLIC)
                .addMethod(methodSpecBuilder.build())
                .build();

        try {
            // 生成文件
            JavaFile javaFile = JavaFile.builder(packageName, bindClass).build();
            // 將文件寫出
            javaFile.writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

而主工程代碼,看 MainActivity 裏面怎麼使用的:

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.btn1)
    public Button btn1;

    @BindView(R.id.btn2)
    public Button btn2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        MyAnnotationUtils.bind(this);
        HelloWorld.main(null);

        btn1.setText("測試成功1");

        btn2.setText("測試成功2");


    }

    @OnClick({R.id.btn1, R.id.btn2})
    public void onBtn1Click(View v) {
        Toast.makeText(this, "測試" + ((Button)v).getText().toString(), Toast.LENGTH_SHORT).show();
    }
}

通過上面,整個項目編譯完成後,在 app\build\generated\source\apt\debug\com\mill\apt 目錄下面有一個自動生成的類:

package com.mill.apt;

import android.view.View;

public class MainActivity_ViewBinding {
  public MainActivity_ViewBinding(final MainActivity target, final View source) {
    target.btn1 = (android.widget.Button)source.findViewById(2131165217);
    target.btn2 = (android.widget.Button)source.findViewById(2131165218);
    source.findViewById(2131165217).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onBtn1Click(v); }});
    source.findViewById(2131165218).setOnClickListener(new android.view.View.OnClickListener() { public void onClick(View v) { target.onBtn1Click(v); }});
  }
}

而我們關心的這個類裏面的方法是怎麼調用呢?那就要看 MyAnnotationUtils類的 bind 方法:

public class MyAnnotationUtils {

    public static void bind(Activity activity) {
        //獲取activity的decorView(根view)
        View view = activity.getWindow().getDecorView();
        bind(activity, view);
    }

    private static void bind(Object obj, View view) {
        String qualifiedName = obj.getClass().getName();
        //找到該activity對應的Bind類的名字
        String generateClass = qualifiedName + "_ViewBinding";
        //然後調用Bind類的構造方法,從而完成activity裏view的初始化
        try {
            Class.forName(generateClass).getConstructor(obj.getClass(), View.class).newInstance(obj, view);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

實際就是通過反射,調用動態生成類的構造方法。

 

這裏提幾個需要注意的點:

1,HelloProcessor  類是必須在 Java Module裏面的,不然找不到 javax.annotation.processing.AbstractProcessor;

2,自Android Gradle 插件 2.2 版本開始,官方提供了名爲 AnnotationProcessor 的功能來完全代替 Android-Apt;

3,建議使用 com.google.auto.service:auto-service 庫,註冊註解處理器;

導入庫之後,類似 加上 @AutoService(Processor.class) ,就會自動生成對應的

\META-INF\services javax.annotation.processing.Processor:

@AutoService(Processor.class)
public class HelloProcessor extends AbstractProcessor {
    //......
}

 

最後附上demo地址:https://github.com/miLLlulei/AnnotationProcessor

歡迎大家star~

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章