編譯時動態生成代碼技術之註解處理器(三)

概念

註解處理器(Annotation Processor)是javac內置的一個用於編譯時掃描和處理註解(Annotation)的工具。在源代碼編譯階段,通過註解處理器,我們可以獲取源文件內註解(Annotation)相關內容。
APT(Annotation Process Tool),是一種在代碼編譯時處理註解,按照一定的規則,生成相應的java文件,多用於對自定義註解的處理,對運行時的性能影響很小。

用途

由於註解處理器可以在程序編譯階段工作,所以我們可以在編譯期間通過註解處理器進行我們需要的操作。比較常用的用法就是在編譯期間獲取相關注解數據,然後動態生成.java源文件(讓機器幫我們寫代碼),通常是自動產生一些有規律性的重複代碼,解決了手工編寫重複代碼的問題,大大提升編碼效率。

註解處理器可以生成Java代碼,這些生成的Java代碼會組成 .java 文件,但不能修改已經存在的Java類(即不能向已有的類中添加方法)。而這些生成的Java文件,會同時與其他普通的手寫Java源代碼一起被javac編譯。

抽象處理器 AbstractProcessor

每一個註解處理器都要繼承於AbstractProcessor

import javax.annotation.processing.AbstractProcessor;

public class MyAnnotationProcessor extends AbstractProcessor
{
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment)
    {
        super.init(processingEnvironment);
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)
    {
        return false;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes()
    {
        return super.getSupportedAnnotationTypes();
    }

    @Override
    public SourceVersion getSupportedSourceVersion()
    {
        return super.getSupportedSourceVersion();
    }
}
  • init(ProcessingEnvironment processingEnvironment): 每一個註解處理器類都必須有一個空的構造函數。然而,這裏有一個特殊的init()方法,它會被註解處理工具調用,並輸入ProcessingEnviroment參數。
    ProcessingEnviroment提供很多有用的工具類Elements,TypesFiler
  • process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment): 這相當於每個處理器的主函數main()。在這裏寫你的掃描、評估和處理註解的代碼,以及生成Java文件。
    輸入參數RoundEnviroment,可以讓你查詢出包含特定註解的被註解元素。

注意:process()函數中不能直接進行異常拋出,否則的話,運行Annotation Processor的進程會異常崩潰,然後彈出一大堆讓人捉摸不清的堆棧調用日誌顯示.

  • getSupportedAnnotationTypes(): 這裏必須指定,這個註解處理器是註冊給哪個註解的。注意,它的返回值是一個字符串的集合,包含本處理器想要處理的註解類型的合法全稱,完整的包名+類名。
    在這裏定義你的註解處理器註冊到哪些註解上。
  • getSupportedSourceVersion(): 用來指定你使用的Java版本。通常這裏返回SourceVersion.latestSupported()。然而,如果你有足夠的理由只支持Java 7的話,你也可以返回SourceVersion.RELEASE_7,推薦使用前者。

在Java 7以後,你也可以使用註解來代替getSupportedAnnotationTypes()和getSupportedSourceVersion()

@SupportedSourceVersion(SourceVersion.RELEASE_7)
@SupportedAnnotationTypes("baijunyu.com.test_annotation.AAAA")
@AutoService(Processor.class)
public class TestProcessor extends AbstractProcessor {
  • @AutoService(Processor.class) :向javac註冊我們這個自定義的註解處理器,這樣,在javac編譯時,纔會調用到我們這個自定義的註解處理器方法。
    AutoService這裏主要是用來生成 META-INF/services/javax.annotation.processing.Processor文件的。如果不加上這個註解,那麼,你需要自己進行手動配置進行註冊。手動註冊的方式這裏不再闡述,建議直接採用@AutoService(Processor.class)進行自定義註解處理器註冊,簡潔方便

Gradle引入方式:

implementation 'com.google.auto.service:auto-service:1.0-rc4'

基礎工具

在init()中獲得如下引用:

    private Logger mLogger;
    private Elements mElementUtils;
    private Filer mFiler;
    private Types mTypeUtils;
	@Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mTypeUtils = processingEnv.getTypeUtils();
        mLogger = new Logger(processingEnvironment.getMessager());
        mElementUtils = processingEnvironment.getElementUtils();
        mFiler = processingEnvironment.getFiler();
    }
  • Elements:一個用來處理Element的工具類
  • Types:一個用來處理TypeMirror的工具類
  • Filer :使用Filer你可以創建文件
  • Messager:用於編譯時在打印日誌信息,在gradle console

在Android Studio使用Annotation Processor

由於Android平臺是基於OpenJDK的,而OpenJDK中不包含Annotation Processor的相關代碼。因此,在使用Annotation Processor時,必須在新建Module時選擇Java Library,處理註解相關的代碼都需要在Java Library模塊下完成。

整個項目的結構

annotation模塊(Java Library) 該模塊存放的是我們自定義的註解,是一個Java Library
compiler模塊 (Java Library) 依賴annotation模塊,處理註解並自動生成代碼等,同樣也是Java Library
app (Android App) 依賴compiler模塊,需要使用annotationProcessor依賴compiler模塊

一個簡單的例子:

1.創建annotation Module,類型爲Java Library,在此模塊的定義我自定義註解類
在annotation中自定義一個註解

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface BindView {
 int value() default -1;
}

2.創建compiler Module,類型爲Java Library,在此模塊中定義註解處理器
gradle配置:

  • 依賴google.auto.service
 implementation 'com.google.auto.service:auto-service:1.0-rc4'
  • 依賴annotation
implementation project(':test-annotation')

創建一個自定義Annotation Processor繼承於AbstractProcessor

@AutoService(Processor.class)
public class MyAnnotationProcessor extends AbstractProcessor {

    private Filer mFiler;
    private Messager mMessager;
    private Elements mElementUtils;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }

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

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

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        Set<? extends Element> bindViewElements = roundEnvironment.getElementsAnnotatedWith(BindView.class);
        for (Element element : bindViewElements) {
            //1.獲取包名
            PackageElement packageElement = mElementUtils.getPackageOf(element);
            String pkName = packageElement.getQualifiedName().toString();
            note(String.format("package = %s", pkName));

            //2.獲取包裝類類型
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String enclosingName = enclosingElement.getQualifiedName().toString();
            note(String.format("enclosindClass = %s", enclosingElement));


            //因爲BindView只作用於filed,所以這裏可直接進行強轉
            VariableElement bindViewElement = (VariableElement) element;
            //3.獲取註解的成員變量名
            String bindViewFiledName = bindViewElement.getSimpleName().toString();
            //3.獲取註解的成員變量類型
            String bindViewFiledClassType = bindViewElement.asType().toString();

            //4.獲取註解元數據
            BindView bindView = element.getAnnotation(BindView.class);
            int id = bindView.value();
            note(String.format("%s %s = %d", bindViewFiledClassType, bindViewFiledName, id));

            //4.生成文件
            createFile(enclosingElement, bindViewFiledClassType, bindViewFiledName, id);
            return true;
        }
        return false;
    }

    private void createFile(TypeElement enclosingElement, String bindViewFiledClassType, String bindViewFiledName, int id) {
        String pkName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
        try {
            JavaFileObject jfo = mFiler.createSourceFile(pkName + ".ViewBinding", new Element[]{});
            Writer writer = jfo.openWriter();
            writer.write(brewCode(pkName, bindViewFiledClassType, bindViewFiledName, id));
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    private String brewCode(String pkName, String bindViewFiledClassType, String bindViewFiledName, int id) {
        StringBuilder builder = new StringBuilder();
        builder.append("package " + pkName + ";\n\n");
        builder.append("//Auto generated by apt,do not modify!!\n\n");
        builder.append("public class ViewBinding { \n\n");
        builder.append("public static void main(String[] args){ \n");
        String info = String.format("%s %s = %d", bindViewFiledClassType, bindViewFiledName, id);
        builder.append("System.out.println(\"" + info + "\");\n");
        builder.append("}\n");
        builder.append("}");
        return builder.toString();
    }


    private void note(String msg) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
    }

    private void note(String format, Object... args) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, String.format(format, args));
    }

}

注:藉助Messager,我們可以在編譯時在 gradle console 輸出日誌.

3.使用註解

app gradle配置:

annotationProcessor project(':test-compiler ')
implementation project(':test-annotation')

在app Module中使用註解

public class MainActivity extends AppCompatActivity {

    @BindView(R.id.tv)
    TextView tv;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

rebuild一下,可以在Gradle Console窗口中看到打印結果:
在這裏插入圖片描述據註解獲取到的數據還生成了一個java文件:
在這裏插入圖片描述

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