Java編譯時註解處理器(APT)詳解

本文同步發佈在CSDN,未經本人容許不得轉載html

上篇文章咱們使用註解+反射實現了一個仿ButterKnife功能的示例。考慮到反射是在運行時完成的,多少會影響程序性能。所以,ButterKnife自己並不是基於註解+反射來實現的,而是用APT技術在編譯時處理的。APT什麼呢?接下來一塊兒來看。java

1、APT簡介

1.什麼是APT? APT即爲Annotation Processing Tool,它是javac的一個工具,中文意思爲編譯時註解處理器。APT能夠用來在編譯時掃描和處理註解。經過APT能夠獲取到註解和被註解對象的相關信息,在拿到這些信息後咱們能夠根據需求來自動的生成一些代碼,省去了手動編寫。注意,獲取註解及生成代碼都是在代碼編譯時候完成的,相比反射在運行時處理註解大大提升了程序性能。APT的核心是AbstractProcessor類,關於AbstractProcessor類後面會作詳細說明。git

2.哪裏用到了APT?github

APT技術被普遍的運用在Java框架中,包括Android項以及Java後臺項目,除了上面咱們提到的ButterKnife以外,像EventBus 、Dagger2以及阿里的ARouter路由框架等都運用到APT技術,所以要想了解以、探究這些第三方框架的實現原理,APT就是咱們必需要掌握的。bash

3.如何在Android Studio中構建一個APT項目?app

APT項目須要由至少兩個Java Library模塊組成,不知道什麼是Java Library?不要緊,手把手來叫你如何建立一個Java Library。 框架

在這裏插入圖片描述
首先,新建一個Android項目,而後File-->New-->New Module,打開如上圖所示的面板,選擇Java Library便可。剛纔說到一個APT項目至少應該由兩個Java Library模塊。那麼這兩個模塊分別是什麼做用呢?

1.首先須要一個Annotation模塊,這個用來存放自定義的註解。ide

2.另外須要一個Compiler模塊,這個模塊依賴Annotation模塊。工具

3.項目的App模塊和其它的業務模塊都須要依賴Annotation模塊,同時須要經過annotationProcessor依賴Compiler模塊。 app模塊的gradle中依賴關係以下:性能

implementation project(':annotation')
annotationProcessor project(':factory-compiler')
複製代碼

APT項目的模塊的結構圖以下所示:

在這裏插入圖片描述

爲何要強調上述兩個模塊必定要是Java Library?若是建立Android Library模塊你會發現不能找到AbstractProcessor這個類,這是由於Android平臺是基於OpenJDK的,而OpenJDK中不包含APT的相關代碼。所以,在使用APT時,必須在Java Library中進行。

2、從一個例子開始認識APT

在學習Java基礎的時候想必你們都寫過簡單工廠模式的例子,回想一下什麼是簡單工廠模式。接下來引入一個工廠模式的例子,首先定義一個形狀的接口IShape,併爲其添加 draw()方法:

public interface IShape {
	void draw();
}
複製代碼

接下來定義幾個形狀實現IShape接口,並重寫draw()方法:

public class Rectangle implements IShape {
	@Override
	public void draw() {
		System.out.println("Draw a Rectangle");
	}
}

public class Triangle implements IShape {
	@Override
	public void draw() {
		System.out.println("Draw a Triangle");
	}
}

public class Circle implements IShape {  
    @Override
    public void draw() {   
        System.out.println("Draw a circle");
    }
}
複製代碼

接下來咱們須要一個工廠類,這個類接收一個參數,根據咱們傳入的參數建立出對應的形狀,代碼以下:

public class ShapeFactory {
  public Shape create(String id) {
    if (id == null) {
      throw new IllegalArgumentException("id is null!");
    }
    if ("Circle".equals(id)) {
      return new Circle();
    }
    if ("Rectangle".equals(id)) {
      return new Rectangle();
    }
    if ("Triangle".equals(id)) {
      return new Triangle();
    }
    throw new IllegalArgumentException("Unknown id = " + id);
  }
}
複製代碼

以上就是一個簡單工廠模式的示例代碼,想必你們都可以理解。

那麼,如今問題來了,在項目開發過程當中,咱們隨時可能會添加一個新的形狀。此時就不得不修改工廠類來適配新添加的形狀了。試想一下,每添加一個形狀類都須要咱們手動去更新Factory類,是否是影響了咱們的開發效率?若是這個Factory類可以根據咱們添加新的形狀來同步更新Factory代碼,豈不是就省了咱們不少時間了嗎?

應該怎麼作才能知足上述需求呢?在第一節中已經提到了使用APT能夠幫助咱們自動生成代碼。那麼這個工廠類是否是可使用APT技術來自動生成呢?咱們惟一要作的事情就是新添加的形狀類上加上一個註解,註解處理器就會在編譯時根據註解信息自動生成ShapeFactory類的代碼了,美哉,美哉!理想很豐滿,可是,現實很骨感。雖然已經明確了要作什麼,可是想要註解處理器幫咱們生成代碼,卻還有很長的路要走。不過,不當緊,接下來咱們將一步步實現註解處理器並讓其自動生成Factory類。

3、使用APT處理註解

1.定義Factory註解 首先在annotation模塊下添加一個Factory的註解,Factory註解的Target爲ElementType,表示它能夠註解類、接口或者枚舉。Retention指定爲RetentionPolicy.CLASS,表示該在字節碼中有效。Factory註解添加兩個成員,一個Class類型的type,用來表示註解的類的類型,相同的類型表示屬於同一個工廠。令需一個String類型的id,用來表示註解的類的名稱。Factory註解代碼以下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Factory {

    Class type();

    String id();
}
複製代碼

接下來咱們用@Factory去註解形狀類,以下:

@Factory(id = "Rectangle", type = IShape.class)
public class Rectangle implements IShape {
	@Override
	public void draw() {
		System.out.println("Draw a Rectangle");
	}
}
... 其餘形狀類代碼相似再也不貼出
複製代碼

**2.認識AbstractProcessor **

接下來,就到了咱們本篇文章所要講的核心了。沒錯,就是AbstractProcessor!咱們先在factory-compiler模塊下建立一個FactoryProcessor類繼承AbstractProcessor ,並重寫相應的方法,代碼以下:

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

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

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

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return super.getSupportedSourceVersion();
    }
}
複製代碼

能夠看到,在這個類上添加了@AutoService註解,它的做用是用來生成META-INF/services/javax.annotation.processing.Processor文件的,也就是咱們在使用註解處理器的時候須要手動添加META-INF/services/javax.annotation.processing.Processor,而有了@AutoService後它會自動幫咱們生成。AutoService是Google開發的一個庫,使用時須要在factory-compiler中添加依賴,以下:

implementation 'com.google.auto.service:auto-service:1.0-rc4'
複製代碼

接下來咱們將目光移到FactoryProcessor類內部,能夠看到在這個類中重寫了四個方法,咱們由易到難依次來看:

(1) public SourceVersion getSupportedSourceVersion()

這個方法很是簡單,只有一個返回值,用來指定當前正在使用的Java版本,一般return SourceVersion.latestSupported()便可。

(2) public Set<String> getSupportedAnnotationTypes()

這個方法的返回值是一個Set集合,集合中指要處理的註解類型的名稱(這裏必須是完整的包名+類名,例如com.example.annotation.Factory)。因爲在本例中只須要處理@Factory註解,所以Set集合中只須要添加@Factory的名稱便可。

(3) public synchronized void init(ProcessingEnvironment processingEnvironment)

這個方法用於初始化處理器,方法中有一個ProcessingEnvironment類型的參數,ProcessingEnvironment是一個註解處理工具的集合。它包含了衆多工具類。例如: Filer能夠用來編寫新文件; Messager能夠用來打印錯誤信息; Elements是一個能夠處理Element的工具類。

在這裏咱們有必要認識一下什麼是Element

在Java語言中,Element是一個接口,表示一個程序元素,它能夠指代包、類、方法或者一個變量。Element已知的子接口有以下幾種:

PackageElement 表示一個包程序元素。提供對有關包及其成員的信息的訪問。 ExecutableElement 表示某個類或接口的方法、構造方法或初始化程序(靜態或實例),包括註釋類型元素。 TypeElement 表示一個類或接口程序元素。提供對有關類型及其成員的信息的訪問。注意,枚舉類型是一種類,而註解類型是一種接口。 VariableElement 表示一個字段、enum 常量、方法或構造方法參數、局部變量或異常參數。

接下來,我但願你們先來理解一個新的概念,即拋棄咱們現有對Java類的理解,把Java類看做是一個結構化的文件。什麼意思?就是把Java類看做一個相似XML或者JSON同樣的東西。有了這個概念以後咱們就能夠很容易的理解什麼是Element了。帶着這個概念來看下面的代碼:

package com.zhpan.mannotation.factory;  //    PackageElement

public class Circle {  //  TypeElement

    private int i; //   VariableElement
    private Triangle triangle;  //  VariableElement

    public Circle() {} //    ExecuteableElement

    public void draw(   //  ExecuteableElement
                        String s)   //  VariableElement
    {
        System.out.println(s);
    }

    @Override
    public void draw() {    //  ExecuteableElement
        System.out.println("Draw a circle");
    }
}
複製代碼

如今明白了嗎?不一樣類型Element其實就是映射了Java中不一樣的類元素!知曉這個概念後將對理解後邊的代碼有很大的幫助。

(4) public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)

終於,到了FactoryProcessor類中最後一個也是最重要的一個方法了。先看這個方法的返回值,是一個boolean類型,返回值表示註解是否由當前Processor 處理。若是返回 true,則這些註解由此註解來處理,後續其它的 Processor 無需再處理它們;若是返回 false,則這些註解未在此Processor中處理並,那麼後續 Processor 能夠繼續處理它們。 在這個方法的方法體中,咱們能夠校驗被註解的對象是否合法、能夠編寫處理註解的代碼,以及自動生成須要的java文件等。所以說這個方法是AbstractProcessor 中的最重要的一個方法。咱們要處理的大部分邏輯都是在這個方法中完成。

瞭解上述四個方法以後咱們即可以初步的來編寫FactoryProcessor類的代碼了,以下:

@AutoService(Processor.class)
public class FactoryProcessor extends AbstractProcessor {
        private Types mTypeUtils;
	    private Messager mMessager;
	    private Filer mFiler;
	    private Elements mElementUtils;
	    private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<>();

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

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

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

		   //	掃描全部被@Factory註解的元素
	   for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {

		}

        return false;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
}
複製代碼

上述FactoryProcessor 代碼中在process方法中經過roundEnv.getElementsAnnotatedWith(Factory.class)方法已經拿到了被註解的元素的集合。正常狀況下,這個集合中應該包含的是全部被Factory註解的Shape類的元素,也就是一個TypeElement。但在編寫程序代碼時可能有新來的同事不太瞭解@Factory的用途而誤把@Factory用在接口或者抽象類上,這是不符合咱們的標準的。所以,須要在process方法中判斷被@Factory註解的元素是不是一個類,若是不是一個類元素,那麼就拋出異常,終止編譯。代碼以下:

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
	 //  經過RoundEnvironment獲取到全部被@Factory註解的對象
    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {
		if (annotatedElement.getKind() != ElementKind.CLASS) {
			 throw new ProcessingException(annotatedElement, "Only classes can be annotated with @%s",
                    Factory.class.getSimpleName());
	         }
	         TypeElement typeElement = (TypeElement) annotatedElement;
	         FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass(typeElement);
				...
        }
        return true;
}
複製代碼

基於面向對象的思想,咱們能夠將annotatedElement中包含的信息封裝成一個對象,方便後續使用,所以,另外能夠另外聲明一個FactoryAnnotatedClass來解析並存放annotatedElement的相關信息。FactoryAnnotatedClass代碼以下:

public class FactoryAnnotatedClass {
    private TypeElement mAnnotatedClassElement;
    private String mQualifiedSuperClassName;
    private String mSimpleTypeName;
    private String mId;

    public FactoryAnnotatedClass(TypeElement classElement) {
        this.mAnnotatedClassElement = classElement;
        Factory annotation = classElement.getAnnotation(Factory.class);
        mId = annotation.id();
        if (mId.length() == 0) {
            throw new IllegalArgumentException(
                    String.format("id() in @%s for class %s is null or empty! that's not allowed",
                            Factory.class.getSimpleName(), classElement.getQualifiedName().toString()));
        }
        
        // Get the full QualifiedTypeName
        try {  // 該類已經被編譯
            Class<?> clazz = annotation.type();
            mQualifiedSuperClassName = clazz.getCanonicalName();
            mSimpleTypeName = clazz.getSimpleName();
        } catch (MirroredTypeException mte) {// 該類未被編譯
            DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();
            TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();
            mQualifiedSuperClassName = classTypeElement.getQualifiedName().toString();
            mSimpleTypeName = classTypeElement.getSimpleName().toString();
        }
    }

    // ...省去getter
}
複製代碼

爲了生成合乎要求的ShapeFactory類,在生成ShapeFactory代碼前須要對被Factory註解的元素進行一系列的校驗,只有經過校驗,符合要求了才能夠生成ShapeFactory代碼。根據需求,咱們列出以下規則:

1.只有類才能被@Factory註解。由於在ShapeFactory中咱們須要實例化Shape對象,雖然@Factory註解聲明瞭Target爲ElementType.TYPE,但接口和枚舉並不符合咱們的要求。 2.被@Factory註解的類中須要有public的構造方法,這樣才能實例化對象。 3.被註解的類必須是type指定的類的子類 4.id須要爲String類型,而且須要在相同type組中惟一 5.具備相同type的註解類會被生成在同一個工廠類中

根據上面的規則,咱們來一步步完成校驗,以下代碼:

private void checkValidClass(FactoryAnnotatedClass item) throws ProcessingException {

        TypeElement classElement = item.getTypeElement();

        if (!classElement.getModifiers().contains(Modifier.PUBLIC)) {
            throw new ProcessingException(classElement, "The class %s is not public.",
                    classElement.getQualifiedName().toString());
        }

        // 若是是抽象方法則拋出異常終止編譯
        if (classElement.getModifiers().contains(Modifier.ABSTRACT)) {
            throw new ProcessingException(classElement,
                    "The class %s is abstract. You can't annotate abstract classes with @%",
                    classElement.getQualifiedName().toString(), Factory.class.getSimpleName());
        }

        // 這個類必須是在@Factory.type()中指定的類的子類,不然拋出異常終止編譯
        TypeElement superClassElement = mElementUtils.getTypeElement(item.getQualifiedFactoryGroupName());
        if (superClassElement.getKind() == ElementKind.INTERFACE) {
            // 檢查被註解類是否實現或繼承了@Factory.type()所指定的類型,此處均爲IShape
            if (!classElement.getInterfaces().contains(superClassElement.asType())) {
                throw new ProcessingException(classElement,
                        "The class %s annotated with @%s must implement the interface %s",
                        classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),
                        item.getQualifiedFactoryGroupName());
            }
        } else {
            TypeElement currentClass = classElement;
            while (true) {
                TypeMirror superClassType = currentClass.getSuperclass();

                if (superClassType.getKind() == TypeKind.NONE) {
                    // 向上遍歷父類,直到Object也沒獲取到所需父類,終止編譯拋出異常
                    throw new ProcessingException(classElement,
                            "The class %s annotated with @%s must inherit from %s",
                            classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),
                            item.getQualifiedFactoryGroupName());
                }

                if (superClassType.toString().equals(item.getQualifiedFactoryGroupName())) {
                    // 校驗經過,終止遍歷
                    break;
                }
                currentClass = (TypeElement) mTypeUtils.asElement(superClassType);
            }
        }

        // 檢查是否由public的無參構造方法
        for (Element enclosed : classElement.getEnclosedElements()) {
            if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {
                ExecutableElement constructorElement = (ExecutableElement) enclosed;
                if (constructorElement.getParameters().size() == 0 &&
                        constructorElement.getModifiers().contains(Modifier.PUBLIC)) {
                    // 存在public的無參構造方法,檢查結束
                    return;
                }
            }
        }

        // 爲檢測到public的無參構造方法,拋出異常,終止編譯
        throw new ProcessingException(classElement,
                "The class %s must provide an public empty default constructor",
                classElement.getQualifiedName().toString());
    }
複製代碼

若是經過上述校驗,那麼說明被@Factory註解的類是符合咱們的要求的,接下來就能夠處理註解信息來生成所需代碼了。可是本着面向對象的思想,咱們還需聲明FactoryGroupedClasses來存放FactoryAnnotatedClass,而且在這個類中完成了ShapeFactory類的代碼生成。FactoryGroupedClasses 代碼以下:

public class FactoryGroupedClasses {

    private static final String SUFFIX = "Factory";
    private String qualifiedClassName;

    private Map<String, FactoryAnnotatedClass> itemsMap = new LinkedHashMap<>();

    public FactoryGroupedClasses(String qualifiedClassName) {
        this.qualifiedClassName = qualifiedClassName;
    }

    public void add(FactoryAnnotatedClass toInsert) {
        FactoryAnnotatedClass factoryAnnotatedClass = itemsMap.get(toInsert.getId());
        if (factoryAnnotatedClass != null) {
            throw new IdAlreadyUsedException(factoryAnnotatedClass);
        }
        itemsMap.put(toInsert.getId(), toInsert);
    }

  public void generateCode(Elements elementUtils, Filer filer) throws IOException {
        //  Generate java file
        ...
    }
}
複製代碼

接下來將全部的FactoryGroupedClasses都添加到集合中去

private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<>();

	// 	...
	FactoryGroupedClasses factoryClass = factoryClasses.get(annotatedClass.getQualifiedFactoryGroupName());
    if (factoryClass == null) {
            String qualifiedGroupName = annotatedClass.getQualifiedFactoryGroupName();
            factoryClass = new FactoryGroupedClasses(qualifiedGroupName);
            factoryClasses.put(qualifiedGroupName, factoryClass);
      }
	factoryClass.add(annotatedClass);
	// ...
複製代碼

OK!到目前爲止,全部的準備工做都已經完成了。接下來就是根據註解信息來生成ShapeFactory類了,有沒有很興奮?遍歷factoryClasses集合,並調用FactoryGroupedClasses類的generateCode()方法來生成代碼了:

for (FactoryGroupedClasses factoryClass : factoryClasses.values()) {
          factoryClass.generateCode(mElementUtils, mFiler);
     }
複製代碼

但是,當咱們去掉用generateCode(mElementUtils, mFiler)方法的時候.....納尼?仍是一個空方法,咱們還沒由實現呢!笑哭😂...

4、認識JavaPoet並用其生成ShapeFactory類

到此爲止,咱們惟一剩餘的需求就是生成ShapeFactory類了。上一節中咱們在FactoryProcessor類的init(ProcessingEnvironment processingEnvironment)方法中經過processingEnvironment拿到了Filer,而且咱們也提到經過Filer能夠用來編寫文件,便可以經過Filer來生成咱們所須要的ShapeFactory類。可是,直接使用Filer須要咱們手動拼接類的代碼,極可能一不當心寫錯了一個字母就導致所生成的類是無效的。所以,咱們須要來認識一下JavaPoet這個庫。 JavaPoet是square公司的一個開源框架JavaPoet,由Jake Wharton大神所編寫。JavaPoet能夠用對象的方式來幫助咱們生成類代碼,也就是咱們能只要把要生成的類文件包裝成一個對象,JavaPoet即可以自動幫咱們生成類文件了。關於這個庫的使用就不詳細在這裏講解了,有須要瞭解的能夠到github查看,使用起來很簡單。

好了,步入正題,使用JavaPoet構建並自動生成ShapeFactory類的代碼以下:

public void generateCode(Elements elementUtils, Filer filer) throws IOException {
        TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);
        String factoryClassName = superClassName.getSimpleName() + SUFFIX;
        String qualifiedFactoryClassName = qualifiedClassName + SUFFIX;
        PackageElement pkg = elementUtils.getPackageOf(superClassName);
        String packageName = pkg.isUnnamed() ? null : pkg.getQualifiedName().toString();

        MethodSpec.Builder method = MethodSpec.methodBuilder("create")
                .addModifiers(Modifier.PUBLIC)
                .addParameter(String.class, "id")
                .returns(TypeName.get(superClassName.asType()));
        method.beginControlFlow("if (id == null)")
                .addStatement("throw new IllegalArgumentException($S)", "id is null!")
                .endControlFlow();

        for (FactoryAnnotatedClass item : itemsMap.values()) {
            method.beginControlFlow("if ($S.equals(id))", item.getId())
                    .addStatement("return new $L()", item.getTypeElement().getQualifiedName().toString())
                    .endControlFlow();
        }

        method.addStatement("throw new IllegalArgumentException($S + id)", "Unknown id = ");

        TypeSpec typeSpec = TypeSpec
                .classBuilder(factoryClassName)
                .addModifiers(Modifier.PUBLIC)
                .addMethod(method.build())
                .build();

        JavaFile.builder(packageName, typeSpec).build().writeTo(filer);
    }
複製代碼

好了,如今項目已經能夠幫咱們自動來生成須要的Java文件啦。接下來驗證一下,Build一下項目,切換到project模式下,在app-->build-->generated-->source-->apt-->debug-->(package)-->factory下面就能夠看到ShapeFactory類,以下圖:

這裏寫圖片描述
這個類並不是是咱們本身編寫的,而是經過使用APT的一系列騷操做自動生成的。如今能夠再添加一個形狀類實現IShape並附加@Factory註解,再次編譯後都自動會生成到ShapeFactory中!

到此爲止,本篇文章就告一段落了。相信看完本篇文章必定大有所獲,由於掌握了APT技術以後,再去研究使用APT的第三方框架源碼,必定會遊刃有餘,事半功倍。

因爲本篇文章結構比較複雜且代碼也較多,項目的源碼已經放在文章末尾,可做參考。

源碼下載

參考資料

Java註解處理器

JDK文檔AbstractProcessor

相關文章
相關標籤/搜索