Java註解之編譯時註解

編譯時註解

關於註解的介紹和運行時註解能夠參考上一篇Java註解之運行時註解,這裏就再也不贅述。java

編譯時註解應用同樣十分普遍,除了以前提到ButterKnife,還有ARouter是經過編譯時註解生成路由表,Tinker經過編譯時註解生成Application的代理類。編譯時註解和運行時註解定義的方式是徹底同樣的,不一樣的是它們對於註解的處理方式,運行時註解是在程序運行時經過反射獲取註解而後處理的,編譯時註解是程序在編譯期間經過註解處理器處理的。因此咱們學習編譯時註解主要就是學習註解處理器相關API的使用。android

annotation-6

從上面的流程圖咱們也能夠看出,編譯時註解的處理過程是遞歸的,先掃描原文件,再掃描生成的文件,直到全部文件中都沒有待處理的註解纔會結束。其中掃描註解並不須要咱們處理,咱們須要關心的就是如何處理註解以及如何將處理後的結果寫入到文件中。git

註解處理器

註解處理器早在JDK1.5的時候就有這個功能了,只不過當時的註解處理器是apt,相關的api是在com.sun.mirror包下的。從JDK1.6開始,apt相關的功能已經包含在了javac中,並提供了新的api在javax.annotation.processing和javax.lang.model to process annotations這兩個包中。舊版的註解處理器api在JDK1.7已經被標記爲deprecated,並在JDK1.8中移除了apt和相關api。github

Processor

下圖是JDK中Processor的類圖,Processor就是用於處理編譯器註解的類。經過繼承AbstractProcessor就能夠自定義處理註解。api

annotation-2

  • init(ProcessingEnvironment processingEnvironment):初始化方法,這個方法會被註解處理工具調用,並傳入一個ProcessingEnvironment變量,這個變量很是重要,它會提供一些很是使用的工具如Elements, Filer, Messager,Types等,後面咱們會單獨介紹它們。app

  • getSupportedOptions: 這個方法容許咱們自定義一些參數傳給Processor,例如咱們在getSupportOptions中加一個MODULE_NAME參數ide

    @Override
    public Set<String> getSupportedOptions() {
        HashSet<String> set = new HashSet<>();
        set.add("MODULE_NAME");
        return set;
    }
    複製代碼

    而後在gralde文件中的傳一個參數函數

    javaCompileOptions {
        annotationProcessorOptions {
            arguments = [MODULE_NAME: "this module name is " + project.getName()]
        }
    }
    複製代碼

    最後在Processor的init方法中獲取參數工具

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        System.out.println(processingEnvironment.getOptions().get("MODULE_NAME"));
    }
    複製代碼

    運行結果以下post

    annotation-3

  • getSupportedAnnotationTypes: 這個方法用於註解的註冊,只有在這個方法中註冊過的註解纔會被註解處理器所處理

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new LinkedHashSet<>();
        types.add(BindView.class.getCanonicalName());
        return types;
    }
    複製代碼
  • getSupportedSourceVersion:返回你目前使用的JDK版本,一般返回SourceVersion.latestSupported(),固然若是你沒使用最新的JDK版本的話,也能夠返回指定版本。

  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment):這個方法是Processor中最重要的方法,全部關於註解的處理和文件的生成都是在這個方法中完成的。它有兩個參數,第一個Set<? extends TypeElement> set包含全部待處理的的註解,須要注意的是,若是你定義了一個註解可是沒有在代碼中使用它,這樣是不會加到set中的。第二個參數roundEnvironment表示當前註解所處的環境,經過這個參數能夠查詢到當前這一輪註解處理的信息。第一個參數咱們一般用不到它,最經常使用的是roundEnvironment中的getElementsAnnotatedWith方法,這個方法能夠返回被特定註解標註的全部元素。process方法還有一個boolean類型的返回值,當返回值爲true的時候表示這個Processor處理的註解不會再被後續的Processor處理。若是返回false,則表示這些註解還會被後續的Processor處理,相似攔截器模式。

Processor接口中定義了註解處理器中必要的方法,AbstractProcessor是實現Processor接口的一個抽象類,它在Processor的基礎上提供了三個個註解功能,分別對應上面的三個方法。從下圖中的名字也很容易看出對應的哪些方法。

annotation-4

Element

全部被註解標註的部分都會被解析成element,在上面介紹的process方法中,經過roundEnvironment的getElementsAnnotatedWith方法就能夠獲取到element的set,element既多是類,也多是類屬性,還多是方法,因此接下來咱們還須要將element轉換成對應的子類。

annotation-9

  • ExecutableElement: 可執行元素,包括類或者接口的方法。
  • PackageElement: 包元素
  • TypeElement:類,接口,或者枚舉。
  • VariableElement: 類屬性,枚舉常量,方法參數,局部變量或者異常參數。
  • TypeParameterElement: 表示一個泛型元素

咱們在定義註解的能夠指定註解的ElementType,這個ElementType和Element是有對應關係的,經過測試可獲得下面表格。

ElementType Element
TYPE TypeElement
FIELD VariableElement
METHOD ExecutableElement
PARAMETER VariableElement
CONSTRUCTOR ExecutableElement
LOCAL_VARIABLE 獲取不到
ANNOTATION_TYPE TypeElement
PACKAGE PackageElement
TYPE_PARAMETER TypeParameterElement
TYPE_USE 1對多,取決於使用的位置

拿到對應Element以後,還須要收集Element的相關信息,下面咱們介紹幾個經常使用的方法

  • getSimpleName:獲取該元素的名字

  • getModifiers:獲取該元素的訪問權限,返回一個Set

  • asType: 獲取該元素的類型,好比TextView會返回android.widget.TextView

  • getEnclosingElement:獲取父級元素,好比參數的父級是方法,方法的父級是類或者接口。

    @Retention(RetentionPolicy.SOURCE)
    @Target(ElementType.PARAMETER)
    public @interface TestCompiler {
        int value() default -1;
    }
    
    public class MainActivity extends FragmentActivity {
        @Override
        protected void onCreate(@TestCompiler Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element element : roundEnvironment.getElementsAnnotatedWith(TestCompiler.class)) {
            System.out.println(element.getEnclosingElement().getSimpleName());
            System.out.println(element.getEnclosingElement().getEnclosingElement().getSimpleName());
            System.out.println(element.getEnclosingElement().getEnclosingElement().getEnclosingElement().getSimpleName());
            System.out.println(element.getEnclosingElement().getEnclosingElement().getEnclosingElement().getEnclosingElement());
        }
        return false;
    }
    複製代碼

    運行結果爲

    annotation-10

    包名之上再調用這個方法就會返回null。

  • getEnclosedElements: 這個和上面的方法是對應的,獲取當前元素的一級子級元素列表。

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element element : roundEnvironment.getElementsAnnotatedWith(TestCompiler.class)) {
            System.out.println(element.getEnclosingElement().getSimpleName());
            System.out.println(Arrays.toString(element.getEnclosingElement().getEnclosedElements().toArray()));
            System.out.println(element.getEnclosingElement().getEnclosingElement().getSimpleName());
            System.out.println(Arrays.toString(element.getEnclosingElement().getEnclosingElement().getEnclosedElements().toArray()));
            System.out.println(element.getEnclosingElement().getEnclosingElement().getEnclosingElement().getSimpleName());
            System.out.println(Arrays.toString(element.getEnclosingElement().getEnclosingElement().getEnclosingElement().getEnclosedElements().toArray()));
            System.out.println(element.getEnclosingElement().getEnclosingElement().getEnclosingElement().getEnclosingElement());
        }
    }
    複製代碼

    運行結果

    annotation-11

ProcessingEnvironment

ProcessingEnvironment就是在Processor的init方法中傳進來的變量,它爲咱們提供了一些很是實用的工具類,下面是它的類圖

annotation-7

  • getLocale:返回Locale對象,這個沒什麼可說的,就是國際化的東西

  • getSourceVersion:支持的Java版本

  • getOptions:這個在上面介紹getSupportedOptions有用到過,就是用來接收外部參數的

  • getMessager:返回一個Messager對象,Messager是一個分等級的log工具,一共分爲NOTE,WARNING,MANDATORY_WARNING,ERROR,OTHER五個等級。實際上在Processor中咱們也可使用sout的方式打印信息,咱們能夠經過一段代碼測試它們之間的區別

    @Override
     public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
         System.out.println("==================================sout");
         processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING," ========warning");
         processingEnv.getMessager().printMessage(Diagnostic.Kind.MANDATORY_WARNING," ========mandatory_warning");
         processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE," ========note");
         processingEnv.getMessager().printMessage(Diagnostic.Kind.OTHER," ========other");
         processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR," ========error");
         return false;
     }
    複製代碼

    運行效果

    annotation-8

    能夠看到Messager打印的日誌前面是有標註的,並且若是用Messager打印Error等級的日誌會致使Build失敗。Messager的主要做用是給使用者打印日誌,由於Processor一般是開發給別人使用的,當用戶在使用不當的時候可以清晰明確的提醒用戶是很是重要的,例如固然咱們用ButterKnife的時候把View設置成private了,就會報錯: 錯誤: @BindView fields must not be private or static。

  • getElementUtils:返回一個Elements對象,和Element相關的工具類。好比咱們要獲取包名怎麼辦?能夠經過上面介紹過的getEnclosingElement方法一層一層網上找,很是麻煩也很容易出錯。還能夠經過Elements中的getPackageOf方法直接獲取到

  • getTypeUtils:返回一個Types對象,和元素類型相關的工具類

  • getFiler:返回一個Filer對象,負責生成文件,裏面的方法不多隻有4個,咱們要生成java文件的時候就調用createClassFile方法就能夠了。

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        for (Element element : roundEnvironment.getElementsAnnotatedWith(TestCompiler.class)) {
            try {
                JavaFileObject jfo =  processingEnv.getFiler().createSourceFile("Test");
                BufferedWriter bw = new BufferedWriter(jfo.openWriter());
                bw.append("public class Test {}");
                bw.flush();
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }
    複製代碼

    這樣咱們就在app/build/generated/source/apt/debug目錄下生成了一個Test的java文件。

    annotation-12

實戰

概念看的再多也不如實際動手應用一下。和以前的運行時註解同樣,咱們再使用編譯期註解來實現一下ButterKnife。

  1. 新建兩個module

    • annotation用來定義註解
    • compiler用來編寫處理註解的代碼

    這兩個module都要選擇Java Library

    annotation-5

    那爲何要拆分兩個module呢,由於編譯期註解的處理代碼是隻在代碼編譯的時候使用的,因此這些代碼要和主module分開拆成compiler,可是compiler又依賴於註解,主module也要使用註解。因此就將註解的定義也拆分出來。這樣作的好處是能夠在compiler中引入任何庫,而不用考慮Android關於方法數的限制。例如Guava。

  2. 定義註解

    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.FIELD)
    public @interface BindViewCompiler {
        int value() default -1;
    }
    
    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.METHOD)
    public @interface OnClickCompiler {
        int value() default -1;
    }
    
    複製代碼
  3. 定義註解處理器

    public class BindViewProcessor extends AbstractProcessor {
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latestSupported();
        }
    
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> types = new LinkedHashSet<>();
            types.add(BindViewCompiler.class.getCanonicalName());
            types.add(OnClickCompiler.class.getCanonicalName());
            return types;
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
            return false;
        }
    }
    複製代碼
  4. 定義一個描述Java文件的類ClassModel

    public class ClassModel {
        /** * 成員變量 */
        private HashSet<VariableElement> variableElements;
        /** * 類方法 */
        private HashSet<ExecutableElement> executableElements;
        /** * 包 */
        private PackageElement packageElement;
        /** * 類 */
        private TypeElement classElement;
    
        public ClassModel(TypeElement classElement) {
            this.classElement = classElement;
            packageElement = (PackageElement) classElement.getEnclosingElement();
            variableElements = new HashSet<>();
            executableElements = new HashSet<>();
        }
    
        public void addVariableElement(VariableElement element) {
            variableElements.add(element);
        }
    
        public void addExecutableElement(ExecutableElement element) {
            executableElements.add(element);
        }
    
        /** * 生成Java文件 */
        public void generateJavaFile(Filer filer) {
        }
    }
    複製代碼
  5. 實現process方法

    private HashMap<String, ClassModel> classMap;
     @Override
     public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
         // 由於掃描會有多輪,因此須要清空一下,classMap在init方法中初始化
         classMap.clear();
         for (Element element : roundEnvironment.getElementsAnnotatedWith(BindViewCompiler.class)) {
             ClassModel model = checkModel(element);
             model.addVariableElement((VariableElement) element);
         }
    
         for (Element element : roundEnvironment.getElementsAnnotatedWith(OnClickCompiler.class)) {
             ClassModel model = checkModel(element);
             model.addExecutableElement((ExecutableElement) element);
         }
    
         for (ClassModel model : classMap.values()) {
             model.generateJavaFile(processingEnv.getFiler());
         }
         return true;
     }
    
     private ClassModel checkModel(Element element) {
         // 獲取當前類
         TypeElement classElement = (TypeElement) element.getEnclosingElement();
         String qualifiedName = classElement.getQualifiedName().toString();
         // 查看是否已經保存在classMap中了,若是沒有就新建立一個
         ClassModel model = classMap.get(qualifiedName);
         if (model == null) {
             model = new ClassModel(classElement);
             classMap.put(qualifiedName, model);
         }
         return model;
     }
    複製代碼
  6. 實現generateJavaFile方法

    /** * 生成Java文件 */
     public void generateJavaFile(Filer filer) {
         try {
             JavaFileObject jfo = filer.createSourceFile(classElement.getQualifiedName() + "$$view_binding");
             BufferedWriter bw = new BufferedWriter(jfo.openWriter());
             bw.append("package ").append(packageElement.getQualifiedName()).append(";\n");
             bw.newLine();
             bw.append(getImportString());
             bw.newLine();
             bw.append("public class ").append(classElement.getSimpleName()).append("$$view_binding implements Injectable {\n");
             bw.newLine();
             bw.append(getFiledString());
             bw.newLine();
             bw.append(getConstructString());
             bw.newLine();
             bw.append("}");
             bw.flush();
             bw.close();
         } catch (IOException e) {
             e.printStackTrace();
         }
     }
    
     /** * 生成import代碼 */
     private String getImportString() {
         StringBuilder stringBuilder = new StringBuilder();
         stringBuilder.append("import android.view.View;\n");
         stringBuilder.append("import com.example.hao.learnself.date_2018_12_28.Injectable;\n");
         stringBuilder.append("import ").append(classElement.getQualifiedName()).append(";\n");
         HashSet<String> importStrs = new HashSet<>();
         for (VariableElement element : variableElements) {
             importStrs.add("import " + element.asType().toString() + ";\n");
         }
         for (String str : importStrs) {
             stringBuilder.append(str);
         }
         return stringBuilder.toString();
     }
    
     /** * 生成成員變量 */
     private String getFiledString() {
         return "private " + classElement.getSimpleName().toString() + " target;\n";
     }
    
     /** * 生成構造函數 */
     private String getConstructString() {
         StringBuilder stringBuilder = new StringBuilder();
         stringBuilder.append("public ").append(classElement.getSimpleName().toString()).append("$$view_binding")
                 .append("(").append(classElement.getSimpleName()).append(" target, ").append("View view) {\n");
         stringBuilder.append("this.target = target;\n");
         for (VariableElement element : variableElements) {
             int resId = element.getAnnotation(BindViewCompiler.class).value();
             stringBuilder.append("target.").append(element.getSimpleName()).append(" = (").append(element.asType().toString())
                     .append(")view.findViewById(").append(resId).append(");\n");
         }
    
         for (ExecutableElement element : executableElements) {
             int resId = element.getAnnotation(OnClickCompiler.class).value();
             stringBuilder.append("view.findViewById(").append(resId).append(").setOnClickListener(new View.OnClickListener() {\n")
                     .append("@Override\n").append("public void onClick(View v) {\n")
                     .append("target.").append(element.getSimpleName()).append("();\n")
                     .append("}\n});\n");
         }
         stringBuilder.append("}");
         return stringBuilder.toString();
     }
    複製代碼
  7. 註冊Processor。 Processor須要註冊一下才能被註解處理器處理,在src/main/resources/META-INF/services下建立一個javax.annotation.processing.Processor文件,若是沒有當前目錄就新建一個

    annotation-13

    在該文件中寫入Processor的全類名

    com.example.compiler.BindViewProcessor
    複製代碼
  8. build一下並查看生成的文件,在/app/build/generated/source/apt下

    package com.example.hao.learnself.date_2018_12_28;
    
    import android.view.View;
    import com.example.hao.learnself.date_2018_12_28.Injectable;
    import com.example.hao.learnself.date_2018_12_28.AnnotationTestActivity;
    import android.widget.TextView;
    
    public class AnnotationTestActivity$$view_binding implements Injectable {
    
    private AnnotationTestActivity target;
    
    public AnnotationTestActivity$$view_binding(AnnotationTestActivity target, View view) {
    this.target = target;
    target.compileSumTv = (android.widget.TextView)view.findViewById(2131165231);
    target.compileAddBtn = (android.widget.TextView)view.findViewById(2131165230);
    view.findViewById(2131165230).setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
    target.compileAdd();
    }
    });
    }
    }
    複製代碼
  9. 定義Injectable接口和Injection工具類

    public interface Injectable {
    }
    
    public class Injection {
        private static final String SUFFIX = "$$view_binding";
    
        public static void inject(@NonNull Activity target) {
            inject(target, target.getWindow().getDecorView());
        }
    
        public static void inject(@NonNull Object target, @NonNull View view) {
            String className = target.getClass().getName();
            try {
                // 經過反射建立
                Class<?> clazz = target.getClass().getClassLoader().loadClass(className + SUFFIX);
                Constructor<Injectable> constructor = (Constructor<Injectable>) clazz.getConstructor(target.getClass(), View.class);
                constructor.newInstance(target, view);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }
    複製代碼
  10. 在測試頁面中使用

    public class AnnotationTestActivity extends BaseActivity {
        @BindViewRuntime(R.id.runtime_add_btn)
        private TextView runtimeAddBtn;
        @BindViewRuntime(R.id.runtime_sum_tv)
        private TextView runtimeSumTv;
        @BindViewCompiler(R.id.compile_add_btn)
        TextView compileAddBtn;
        @BindViewCompiler(R.id.compile_sum_tv)
        TextView compileSumTv;
    
        @Override
        protected int getLayoutId() {
            return R.layout.activity_annotation_test;
        }
    
        @Override
        protected void initView() {
            Injection.inject(this);
        }
    
        @OnClickRuntime(R.id.runtime_add_btn)
        void runTimeAdd() {
            String text = runtimeSumTv.getText().toString();
            runtimeSumTv.setText(String.valueOf(Integer.parseInt(text) + 1));
        }
    
        @OnClickCompiler(R.id.compile_add_btn)
        void compileAdd() {
            String text = compileSumTv.getText().toString();
            compileSumTv.setText(String.valueOf(Integer.parseInt(text) + 1));
        }
    }
    複製代碼
  11. 查看效果

annotation-15

demo下載地址

相關文章
相關標籤/搜索