注解處理器APT的使用

簡介

APT 全稱 Annotation Processing Tool峰弹,即注解處理器。更確切的說大年,它是 javac 的一部分换薄,能夠在編譯期掃描和處理注解,并生成文件专控。

那么使用 APT 有什么好處呢柏蘑?

  1. 將一些通用的重復(fù)的代碼通過 APT 生成革半,減少開發(fā)工作量,提高開發(fā)效率碘赖;
  2. 在不考慮編譯期耗時的情況下撼班,相較于在運行期通過反射處理的方式,更能提高程序運行效率宫静。

現(xiàn)在很多著名的三方庫都使用了 APT 技術(shù)捌袜,比如 butterknife候引,ARouter逛揩,dagger 等。

要使用 APT麸俘,首先得了解 AbstractProcessor.java 息尺,所有自定義的注解處理器都需要繼承這個類。

public abstract class AbstractProcessor implements Processor {

    protected ProcessingEnvironment processingEnv;
    private boolean initialized = false;

    public Set<String> getSupportedAnnotationTypes() {
            SupportedAnnotationTypes sat = this.getClass().getAnnotation(SupportedAnnotationTypes.class);
            if  (sat == null) {
                if (isInitialized())
                    processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                             "No SupportedAnnotationTypes annotation " +
                                                             "found on " + this.getClass().getName() +
                                                             ", returning an empty set.");
                return Collections.emptySet();
            }
            else
                return arrayToSet(sat.value());
        }

    public SourceVersion getSupportedSourceVersion() {
        SupportedSourceVersion ssv = this.getClass().getAnnotation(SupportedSourceVersion.class);
        SourceVersion sv = null;
        if (ssv == null) {
            sv = SourceVersion.RELEASE_6;
            if (isInitialized())
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING,
                                                         "No SupportedSourceVersion annotation " +
                                                         "found on " + this.getClass().getName() +
                                                         ", returning " + sv + ".");
        } else
            sv = ssv.value();
        return sv;
    }

    public synchronized void init(ProcessingEnvironment processingEnv) {
        if (initialized)
            throw new IllegalStateException("Cannot call init more than once.");
        Objects.requireNonNull(processingEnv, "Tool provided null ProcessingEnvironment");

        this.processingEnv = processingEnv;
        initialized = true;
    }

    public abstract boolean process(Set<? extends TypeElement> annotations,
                                    RoundEnvironment roundEnv);

    protected synchronized boolean isInitialized() {
        return initialized;
    }

}

有四個重要的方法疾掰。

  1. init() 初始化方法搂誉;
  2. process() 注解處理器處理注解和生成文件的地方,一般邏輯都會寫在這里静檬;
  3. getSupportedAnnotationTypes() 返回當(dāng)前注解處理器能夠處理的注解信息炭懊;
  4. getSupportedSourceVersion() 返回當(dāng)前注解處理器支持的版本,沒有特殊要求拂檩,一般都會使用 SourceVersion.latestSupported()侮腹。

ProcessingEnvironment

它是一個接口,通過它可以獲取到配置信息和一些常用的工具類稻励。

public interface ProcessingEnvironment {
    // 獲取配置信息
    Map<String,String> getOptions();

    // 打印日志的工具類父阻,也可以用 System.out.println()
    Messager getMessager();

    // 創(chuàng)建文件的工具類
    Filer getFiler();

    // Element相關(guān)的工具類
    Elements getElementUtils();

    // Type相關(guān)的工具類
    Types getTypeUtils();

    // 獲取源碼版本
    SourceVersion getSourceVersion();

}
  1. getOptions() 可以獲取到配置信息,比如 ARouterbuild.gradle 文件中配置的 AROUTER_MODULE_NAME 信息望抽;
Map<String, String> options = processingEnv.getOptions();
if (MapUtils.isNotEmpty(options)) {
    // KEY_MODULE_NAME 就是 "AROUTER_MODULE_NAME"
    moduleName = options.get(KEY_MODULE_NAME);
    generateDoc = VALUE_ENABLE.equals(options.get(KEY_GENERATE_DOC_NAME));
}
  1. getMessager() 可以獲取到日志工具類加矛,通過 Messager 可以在打印一些日志信息,當(dāng)然你也可以直接使用 System.out.println() 來輸出日志煤篙;
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,  "日志信息");
  1. getFiler() 可以獲取到 Filer 工具類斟览,用來創(chuàng)建源文件,字節(jié)碼文件等辑奈;
  2. getElementUtils() 可以獲取到 Elements 工具類苛茂,這是一個比較有用的工具類;
// 獲取包元素
PackageElement getPackageElement(CharSequence name);
PackageElement getPackageOf(Element type);
// 是否過時
boolean isDeprecated(Element e);
// 根據(jù)全路徑獲取到某個類的 TypeElement 元素鸠窗,這是非常有用的
TypeElement getTypeElement(CharSequence name);
...
  1. getTypeUtils() 獲取到 Types 工具類妓羊,這是一個比較有用的工具類;
// t1 是否是 t2 的子類
boolean isSubtype(TypeMirror t1, TypeMirror t2);
...

Element

Element 是注解處理器中比較重要存在稍计。所有經(jīng)過注解處理器掃描后的元素都會被封裝成 Element躁绸。

Element 是一個接口,有五個實現(xiàn)類,分別代表了不同類型的元素涨颜,舉個栗子费韭。

// 1. 包,被封裝為 PackageElement 
package com.ppdai;

/* 
 * 2. 類庭瑰,被封裝為 TypeElement
 * 3. 泛型星持,被封裝為 TypeParameterElement
 */
public class Example<T> {
    // 4. 變量,被分裝為 VariableElement
    private int a;

    // 5. 方法弹灭,被封裝為 ExecutableElement
    public void b() {
    }
}
  1. TypeElement 一個類或接口的元素督暂,如果注解處理器處理的對象是類或者接口,那么這個元素將被封裝為 TypeElemnet穷吮;
  2. Packagelement 表示包元素逻翁;
  3. VariableElement 表示變量、枚舉捡鱼、方法參數(shù)八回;
  4. ExecutableElement 表示構(gòu)造函數(shù)、方法驾诈;
  5. TypeParameterElement 泛型元素缠诅。

實踐

一般注解處理器都會由三個部分組成,compile乍迄,annotation管引,api

  1. compile 一般編寫注解處理器相關(guān)闯两;
  2. annotation 一般編寫一些注解和一些基礎(chǔ)類褥伴,接口等;
  3. api 一般會編寫暴露給上層業(yè)務(wù)的封裝漾狼,工具等重慢。

比如ARouterButterKnife 的結(jié)構(gòu)邦投。

ARouter.png
Butterknife.png

接下來我們一步一步實現(xiàn)自己的注解處理器伤锚。

定義 Annotation

這里仿寫 ARouter@Autowired 注解擅笔。

新建一個 java library module志衣,并定義自己的 @Autowired 注解。

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface Autowired {
    String name();

    String desc() default "";
}

它的結(jié)構(gòu)如下所示猛们。

annotation.png

定義 Annotation Processor

想要在編譯期對注解進行處理念脯,并生成對應(yīng)的文件,需要實現(xiàn) AbstractProcessor弯淘。

新建一個 java library module绿店,定義 AutowiredProcessor 繼承自 AbstractProcessor,并重寫 getSupportedAnnotationTypes() 添加對 @Autowired 注解的支持。

public class AutowiredProcessor extends AbstractProcessor {

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

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

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

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> set = new HashSet<>();
        set.add(Autowired.class.getCanonicalName());
        return set;
    }

}

因為每個類都可以有多個使用 @Autowired 注解的屬性假勿,這里新增一個 categories() 先對所有使用了 @Autowired 的屬性進行分類借嗽,存放到 map 里面。

// 用來存放分類后的數(shù)據(jù)
private HashMap<TypeElement, List<Element>> map = new HashMap<>();

private void categories(Set<? extends Element> set) {
    for (Element element : set) {
        TypeElement typeElement = (TypeElement) element.getEnclosingElement();
        if (map.containsKey(typeElement)) {
            map.get(typeElement).add(element);
        } else {
            List<Element> list = new ArrayList<>();
            list.add(element);
            map.put(typeElement, list);
        }
    }
}

分類后 key 是類對應(yīng)的 TypeElement转培,value 是當(dāng)前類里面所有使用了 @Autowired 注解的屬性對應(yīng)的 Element恶导。

然后新增一個 generate() 用來處理分類后的數(shù)據(jù),并生成對應(yīng)的文件浸须。簡單起見惨寿,這里只對 Activity 里面的邏輯進行了處理。

private void generateFile() {
    for (Map.Entry<TypeElement, List<Element>> entry : map.entrySet()) {
        TypeElement typeElement = entry.getKey();
        List<Element> elementList = entry.getValue();

        PackageElement packageElement = elementUtils.getPackageOf(typeElement);
        // 獲取包名
        String packageName = packageElement.getQualifiedName().toString();

        String sourceClassName = typeElement.getSimpleName().toString();
        // 定義生成的文件名
        String genClassName = String.format("%s$$ARouter$$Autowired", sourceClassName);

        // 方法
        MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("inject")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(Object.class, "target")
                .addStatement("$T inject = ($T) target", ClassName.get(typeElement), ClassName.get(typeElement));

        // 類名$$ARouter$$Autowired
        TypeSpec.Builder typeBuilder = TypeSpec.classBuilder(genClassName)
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ISyringe.class);

        TypeMirror activity = elementUtils.getTypeElement("android.app.Activity").asType();

        for (Element element : elementList) {
            Autowired autowired = element.getAnnotation(Autowired.class);
            String key = autowired.name();

            TypeMirror typeMirror = element.asType();
            String fieldName = element.getSimpleName().toString();

            if (typeUtils.isSubtype(typeElement.asType(), activity)) {
                String source = "inject.getIntent()";
                switch (typeMirror.getKind().toString()) {
                    case "BOOLEAN":
                        methodBuilder.addStatement("inject.$L = $L.getBooleanExtra($S, inject.$L)", fieldName, source, key, fieldName);
                        break;
                    case "LONG":
                        methodBuilder.addStatement("inject.$L = $L.getLongExtra($S, inject.$L)", fieldName, source, key, fieldName);
                        break;
                    // ...
                    default:
                }
            }
        }

        try {
            JavaFile.builder(packageName, typeBuilder.addMethod(methodBuilder.build()).build()).build().writeTo(filer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

注冊 Annotation Processor

這里提供兩種注冊方式删窒,手動注冊和自動注冊裂垦。

手動注冊

  1. src/main 下新建一個 resources 的目錄;
  2. resources 下新建一個 META-INF 的目錄肌索;
  3. META-INF 下新建一個 services 的目錄蕉拢;
  4. services 下新建一個 javax.annotation.processing.Processor 的文件,并將要注冊的 Annotation Processor 的全路徑寫入诚亚。

它的結(jié)構(gòu)大致是這樣子的企量。

注冊Processor.png

自動注冊

google 提供了 auto-service 庫來簡化注冊過程。

修改 build.gradle 文件亡电,添加依賴關(guān)系届巩。

implementation 'com.google.auto.service:auto-service-annotations:1.0-rc7'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'

修改 AutowiredProcessor,在類上添加注解 @AutoService 即可份乒。

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

其實 @AutoService 也是通過 Annotation Processor 來實現(xiàn)的恕汇。具體實現(xiàn)我們可以查看 AutoServiceProcessor.java 文件,它的調(diào)用鏈如下 process() -> processImpl() -> generateConfigFiles() 或辖,當(dāng) @AutoService 注解處理完的時候瘾英,會調(diào)用 generateConfigFiles(), 我們可以看看 generateConfigFiles() 方法的具體實現(xiàn)颂暇。

private void generateConfigFiles() {
  Filer filer = processingEnv.getFiler();

  for (String providerInterface : providers.keySet()) {
    // 熟悉吧缺谴,這里就是我們前面創(chuàng)建的 src/main/resources/META-INF/services/javax.annotation.processing.Processor
    String resourceFile = "META-INF/services/" + providerInterface;
    log("Working on resource file: " + resourceFile);
    try {
      SortedSet<String> allServices = Sets.newTreeSet();
      try {
        FileObject existingFile = filer.getResource(StandardLocation.CLASS_OUTPUT, "",
            resourceFile);
        log("Looking for existing resource file at " + existingFile.toUri());
        Set<String> oldServices = ServicesFiles.readServiceFile(existingFile.openInputStream());
        log("Existing service entries: " + oldServices);
        allServices.addAll(oldServices);
      } catch (IOException e) {
        log("Resource file did not already exist.");
      }

      Set<String> newServices = new HashSet<String>(providers.get(providerInterface));
      if (allServices.containsAll(newServices)) {
        log("No new service entries being added.");
        return;
      }

      allServices.addAll(newServices);
      log("New service file contents: " + allServices);
      FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "",
          resourceFile);
      OutputStream out = fileObject.openOutputStream();
      ServicesFiles.writeServiceFile(allServices, out);
      out.close();
      log("Wrote to: " + fileObject.toUri());
    } catch (IOException e) {
      fatalError("Unable to create " + resourceFile + ", " + e);
      return;
    }
  }
}

定義 API

創(chuàng)建一個 android library module,定義一個工具類耳鸯,來調(diào)用生成 XX$$ARouter$$Autowired.javainject()湿蛔。

public class PPdaiHelper {

    public static void inject(Object target) {
        String className = target.getClass().getName() + "$$ARouter$$Autowired";
        try {
            ISyringe iSyringe = (ISyringe) Class.forName(className).getConstructor().newInstance();
            iSyringe.inject(target);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

修改 app 下面的 build.gradle 文件,增加對 Annotation Processor 的使用县爬。

dependencies {
    //...
    implementation project(":ppdai-api")
    kapt project(":ppdai-compile")
}

做完這些阳啥,自定義的 Annotation Processor 就完成了。接下來就是驗證了财喳。簡單起見察迟,我們編寫了兩個 Activity斩狱,其中一個使用 @Autowired 注解進行數(shù)據(jù)傳遞。

class Main2Activity : AppCompatActivity() {

    @Autowired(name = "id")
    @JvmField
    var id: Long = 0L

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        PPdaiHelper.inject(this)

        Log.d("ppdai", "id : $id")
    }

}

跳轉(zhuǎn) Main2Activity.java 的地方增加參數(shù) id 的傳遞扎瓶。

val intent = Intent(this, Main2Activity::class.java)
intent.putExtra("id", 1000L)
startActivity(intent)

然后編譯項目所踊,看看我們的注解處理器生成的文件。

package com.ppdai.annotationprocessor;

import com.ppdai.core.ISyringe;
import java.lang.Object;
import java.lang.Override;

public class Main2Activity$$ARouter$$Autowired implements ISyringe {
  @Override
  public void inject(Object target) {
    Main2Activity inject = (Main2Activity) target;
    inject.id = inject.getIntent().getLongExtra("id", inject.id);
  }
}

運行代碼概荷,打開 Logcat污筷,點擊跳轉(zhuǎn),查看日志如下乍赫。

驗證.png

傳送門

Github 項目地址
ARouter 之@Autowired源碼分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末瓣蛀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子雷厂,更是在濱河造成了極大的恐慌惋增,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件改鲫,死亡現(xiàn)場離奇詭異诈皿,居然都是意外死亡,警方通過查閱死者的電腦和手機像棘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門稽亏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人缕题,你說我怎么就攤上這事截歉。” “怎么了烟零?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵瘪松,是天一觀的道長。 經(jīng)常有香客問我锨阿,道長宵睦,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任墅诡,我火速辦了婚禮壳嚎,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘末早。我一直安慰自己烟馅,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布荐吉。 她就那樣靜靜地躺著焙糟,像睡著了一般。 火紅的嫁衣襯著肌膚如雪样屠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天,我揣著相機與錄音痪欲,去河邊找鬼悦穿。 笑死,一個胖子當(dāng)著我的面吹牛业踢,可吹牛的內(nèi)容都是我干的栗柒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼知举,長吁一口氣:“原來是場噩夢啊……” “哼瞬沦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起雇锡,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤逛钻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后锰提,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體曙痘,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年立肘,在試婚紗的時候發(fā)現(xiàn)自己被綠了边坤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡谅年,死狀恐怖茧痒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情融蹂,我是刑警寧澤文黎,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站殿较,受9級特大地震影響耸峭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜淋纲,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一劳闹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧洽瞬,春花似錦本涕、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至为障,卻和暖如春晦闰,著一層夾襖步出監(jiān)牢的瞬間放祟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工呻右, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留跪妥,地道東北人。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓声滥,卻偏偏與公主長得像眉撵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子落塑,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,871評論 2 354