Android使用AnnotationProcessor處理編譯時注解

Android中處理編譯時注解使用AnnotationProcessor盯腌,下面我們來看下如何使用AnnotationProcessor

創(chuàng)建module

新建一個Java Library Module(必須為java library)弓摘。

如果出現(xiàn)Plugin with id 'java-library' not found.這樣的錯誤圣蝎,則在build.gradle中將apply plugin: 'java-library'改成apply plugin: 'java'刃宵,或者將gradle版本升級到4或以上。java-library插件是gradle4才引入的徘公,在這之前合租java插件牲证。

配置gradle

在上面創(chuàng)建的module的build.gradle中加入如下依賴:

// 下面兩個庫版本是jdk 1.7最后一個版本,再沒改成1.8之前关面,不要升級
compile 'com.squareup:javapoet:1.9.0'
compile 'com.google.auto.service:auto-service:1.0-rc3'
  1. javapoet是創(chuàng)建和修改java文件的工具坦袍,不是必須的,但建議使用等太,Java原生的API實在是難用捂齐,沒有必要花時間去學習不好的東西。
  2. autoservice也不是必需的缩抡,但建議使用奠宜,它可以處理好編譯時注解需要的配置,如果不使用它則需要手動配置瞻想,后面講到autoservice的使用時會同步說明下手動配置的相關操作压真。

在調用方的build.gradle中加入如下依賴:

// annotationProcessor專用依賴方式,此依賴下的庫不會打包到apk中
annotationProcessor(':sgetter')
// 如果庫中僅有processor類蘑险,那么不需要compile滴肿;如果注解也放在庫里,或者還提供了其他類可供調用佃迄,那么需要compile
compile project(':sgetter')

用例實踐

本文所使用的例子是動態(tài)生成一個類泼差,該類的職責是為其它對象的字段賦值,我們先來看下使用注解的類以及自動生成的類長啥樣:

// 使用注解的類
public class TestEntity {
    @Sgetter
    long id;

    @Sgetter
    String name;

    String remark;
}

// 自動生成的類和屎,兩個類位于同一個包下
public class TestEntitySgetter {
  private TestEntity target;

  public TestEntitySgetter(TestEntity target) {
    this.target = target;
  }

  public void setId(long id) {
    target.id = id;
  }

  public long getId() {
    return target.id;
  }

  public void setName(String name) {
    target.name = name;
  }

  public String getName() {
    return target.name;
  }
}

接下去開始講解具體的編碼過程拴驮。

創(chuàng)建注解

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.FIELD)
public @interface Sgetter {
}

創(chuàng)建Processor

創(chuàng)建一個Processor類繼承自AbstractProcessor

@AutoService(Processor.class)
public class SgetterProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

觸發(fā)下編譯,可以發(fā)現(xiàn)自動生成了如下圖所示的Processor文件

image

這是由autoservice自動生成的柴信,是編譯時注解生效的必要條件套啤,如果沒有使用autoservice,那么需要手動創(chuàng)建Processor文件(路徑和文件全名參照上圖)随常,然后將自定義的Processor類的全名寫入該文件中潜沦,多個Processor以換行隔開。

接著绪氛,我們來看下如何通過SgetterProcessor實現(xiàn)自動生成setter唆鸡、getter方法:

@AutoService(Processor.class)
public class SgetterProcessor extends AbstractProcessor {
    private Elements mElementUtils;
    private Messager mMessager;
    private Filer mFiler;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mElementUtils = processingEnvironment.getElementUtils();    // 元素操作輔助工具
        mMessager = processingEnvironment.getMessager();            // 日志輔助工具
        mFiler = processingEnvironment.getFiler();                  // 文件操作輔助工具
        log("init");
    }

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        /*
           1. set:攜帶getSupportedAnnotationTypes()中的注解類型,一般不需要用到枣察。
           2. roundEnvironment:processor將掃描到的信息存儲到roundEnvironment中争占,從這里取出所有使用Sgetter注解的字段燃逻。
          */
        Set<? extends Element> sgetterElements = roundEnvironment.getElementsAnnotatedWith(Sgetter.class);
        Map<String, ClassInfo> classes = new HashMap<>();
        if (!sgetterElements.isEmpty()) {
            log("----------------------------------");
        }
        for (Element element : sgetterElements) {
            log("process element [" + element.getSimpleName().toString() + "]");

            // 獲取注解目標所在的包,在本例中臂痕,即使用Sgetter注解的字段所在的類所在的包
            PackageElement packageElement = mElementUtils.getPackageOf(element);
            String pkgName = packageElement.getQualifiedName().toString();
            log("pkg=" + pkgName);

            // 獲取包裝類類型伯襟,在本例中,即使用Sgetter注解的字段所在的類
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String enclosingName = enclosingElement.getQualifiedName().toString(); // enclosingName為完整類名
            String simpleName = enclosingElement.getSimpleName().toString();
            log("class=" + enclosingName);

            // 獲取字段信息握童,因為Sgetter只作用于字段姆怪,因此這里可以直接強轉
            VariableElement variableElement = (VariableElement) element;
            String fieldname = variableElement.getSimpleName().toString();  // 獲取字段名
            String fieldtype = variableElement.asType().toString();         // 獲取字段類型
            log("field name=" + fieldname + ", type=" + fieldtype);

            ClassInfo classInfo = classes.get(enclosingName);
            if (classInfo == null) {
                classInfo = new ClassInfo();
                classInfo.pkgName = pkgName;
                classInfo.classname = enclosingName;
                classInfo.fields = new LinkedList<>();
                classes.put(enclosingName, classInfo);
            }
            FieldInfo fieldInfo = new FieldInfo();
            fieldInfo.name = fieldname;
            fieldInfo.type = fieldtype;
            classInfo.fields.add(fieldInfo);
            log("----------------------------------");
        }

        for (ClassInfo classInfo : classes.values()) {
            generateJavaFile(classInfo);
        }

        return true;
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> types = new HashSet<>();
        types.add(Sgetter.class.getCanonicalName());
        log("types=" + types);
        return types;
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        SourceVersion version = SourceVersion.RELEASE_7;
        //    SourceVersion version = SourceVersion.latestSupported();
        log("version=" + version);
        return version;
    }

    private void log(String msg) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, "SgetterProcessor xxx " + msg);
        System.out.println("SgetterProcessor " + msg);
    }

    /**
     * 使用javapoet生成java文件
     * @param classInfo
     */
    private void generateJavaFile(ClassInfo classInfo) {
        try {
            TypeSpec.Builder builder = TypeSpec.classBuilder(splitClassName(classInfo.classname)[1] + "Sgetter")
                    .addModifiers(Modifier.PUBLIC);

            // 生成target字段
            TypeName targetClass = getClassName(classInfo.classname);
            FieldSpec targetField = FieldSpec.builder(targetClass, "target")
                    .addModifiers(Modifier.PRIVATE)
                    .addStatement("this.target = target")
                    .build();
            builder.addField(targetField);

            // 生成構造函數(shù)
            MethodSpec constructor = MethodSpec.constructorBuilder()
                    .addParameter(ParameterSpec.builder(targetClass, "target").build())
                    .addModifiers(Modifier.PUBLIC)
                    .build();
            builder.addMethod(constructor);

            for(FieldInfo fieldInfo : classInfo.fields) {
                // 生成set方法
                StringBuilder sb = new StringBuilder();
                sb.append("set").append(fieldInfo.name.substring(0, 1).toUpperCase())
                        .append(fieldInfo.name.substring(1, fieldInfo.name.length()));
                MethodSpec setMethod = MethodSpec.methodBuilder(sb.toString())
                        .addParameter(ParameterSpec.builder(getClassName(fieldInfo.type), fieldInfo.name).build())
                        .addModifiers(Modifier.PUBLIC)
                        .addStatement("target.$L = $L", fieldInfo.name, fieldInfo.name)
                        .build();
                builder.addMethod(setMethod);

                // 生成get方法
                sb.delete(0, sb.length());
                sb.append("get").append(fieldInfo.name.substring(0, 1).toUpperCase())
                        .append(fieldInfo.name.substring(1, fieldInfo.name.length()));
                MethodSpec getMethod = MethodSpec.methodBuilder(sb.toString())
                        .addModifiers(Modifier.PUBLIC)
                        .addStatement("return target.$L", fieldInfo.name)
                        .returns(getClassName(fieldInfo.type))
                        .build();
                builder.addMethod(getMethod);
            }

            JavaFile javaFile = JavaFile.builder(classInfo.pkgName, builder.build()).build();
            javaFile.writeTo(mFiler);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    private class FieldInfo {
        String name;
        String type;
    }

    private class ClassInfo {
        String pkgName;
        String classname;
        List<FieldInfo> fields;
    }

    private TypeName getClassName(String classname) {
        switch (classname) {
            case "void":
                return TypeName.VOID;
            case "boolean":
                return TypeName.BOOLEAN;
            case "byte":
                return TypeName.BYTE;
            case "short":
                return TypeName.SHORT;
            case "int":
                return TypeName.INT;
            case "long":
                return TypeName.LONG;
            case "char":
                return TypeName.CHAR;
            case "float":
                return TypeName.FLOAT;
            case "double":
                return TypeName.DOUBLE;
            default:
                String[] fields = splitClassName(classname);
                return ClassName.get(fields[0], fields[1]);

        }
    }

    private String[] splitClassName(String classname) {
        int pos = classname.lastIndexOf('.');
        if(pos == -1) { // int等基礎類型
            return null;
        }
        return new String[]{classname.substring(0, pos), classname.substring(pos + 1)};
    }
}

我們來看下幾個核心對象和方法的作用(代碼細節(jié)這里不再詳述,很容易看懂澡绩,而且代碼中也有注釋):

  1. Elements:元素操作工具稽揭,如果使用javapoet的話該原生工具基本用不到,不需要過多關注肥卡。
  2. Messager:日志輸出工具溪掀,Messager輸出的日志本應顯示在Messages窗口中,不過AS3.0之后已經(jīng)找不到這個窗口了步鉴,好在Messager輸出的日志會顯示在終端中(和使用System.out已經(jīng)區(qū)別不大了)膨桥,前提是在終端中使用命令編譯工程。
  3. Filer:文件操作工具唠叛,通過Filer生成的文件位于app/build/generated/source/apt中。
  4. getSupportedSourceVersion:返回jdk的版本(也可以通過@SupportedSourceVersion注解到Processor類)沮稚,默認1.6艺沼。
  5. init:初始化方法,可以在這里進行一些初始化操作以及獲取環(huán)境信息(環(huán)境信息已經(jīng)保存processingEnv成員中蕴掏,子類可以直接使用)障般。Processor類必須保留默認構造函數(shù)(編譯時反射),并且由于初始化方法的存在盛杰,因此一般沒有必要編寫構造函數(shù)挽荡。
  6. getSupportedAnnotationTypes:返回當前Processor支持的注解(也可以通過@SupportedAnnotationTypes注解到Processor類),為了保持代碼的整潔及可維護即供,一般一個Processor只處理一個注解定拟。
  7. process:最核心的方法,用來處理注解逗嫡,該方法是由基類定義的抽象方法青自,子類必須實現(xiàn)。這個方法千萬不能出現(xiàn)異常驱证,否則編譯時會出現(xiàn)各種莫名其妙的問題延窜。

常見問題整理

Processor不執(zhí)行

如果Processor已經(jīng)執(zhí)行過則再次build便不會再執(zhí)行,可以build之前clean抹锄,或者直接rebuild逆瑞。

process方法不執(zhí)行

如果在Processor中注冊的注解沒有使用荠藤,那么process方法就不用執(zhí)行。需要特別注意的是获高,如果注解的使用者和Processor位于同一個module哈肖,那么該使用者會被忽略(筆者在這里吃了大虧,花了很長時間才找出問題)谋减。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末牡彻,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子出爹,更是在濱河造成了極大的恐慌庄吼,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件严就,死亡現(xiàn)場離奇詭異总寻,居然都是意外死亡,警方通過查閱死者的電腦和手機梢为,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進店門渐行,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人铸董,你說我怎么就攤上這事祟印。” “怎么了粟害?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵蕴忆,是天一觀的道長。 經(jīng)常有香客問我悲幅,道長套鹅,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任汰具,我火速辦了婚禮卓鹿,結果婚禮上,老公的妹妹穿的比我還像新娘留荔。我一直安慰自己吟孙,他們只是感情好,可當我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布聚蝶。 她就那樣靜靜地躺著拔疚,像睡著了一般。 火紅的嫁衣襯著肌膚如雪既荚。 梳的紋絲不亂的頭發(fā)上稚失,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天,我揣著相機與錄音恰聘,去河邊找鬼句各。 笑死吸占,一個胖子當著我的面吹牛,可吹牛的內容都是我干的凿宾。 我是一名探鬼主播矾屯,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼初厚!你這毒婦竟也來了件蚕?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤产禾,失蹤者是張志新(化名)和其女友劉穎排作,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體亚情,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡妄痪,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了楞件。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片衫生。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖土浸,靈堂內的尸體忽然破棺而出罪针,到底是詐尸還是另有隱情,我是刑警寧澤黄伊,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布站故,位于F島的核電站,受9級特大地震影響毅舆,放射性物質發(fā)生泄漏。R本人自食惡果不足惜愈腾,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一憋活、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧虱黄,春花似錦悦即、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至泳叠,卻和暖如春作瞄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背危纫。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工宗挥, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留乌庶,地道東北人。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓契耿,卻偏偏與公主長得像瞒大,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子搪桂,可洞房花燭夜當晚...
    茶點故事閱讀 44,614評論 2 353

推薦閱讀更多精彩內容