Android使用AnnotationProcessor处理编译时注解

Android中处理编译时注解使用AnnotationProcessor,下面我们来看下如何使用AnnotationProcessor

创建module

新建一个Java Library Module(必须为java library)。

如果出现Plugin with id 'java-library' not found.这样的错误,则在build.gradle中将apply plugin: 'java-library'改成apply plugin: 'java',或者将gradle版本升级到4或以上。java-library插件是gradle4才引入的,在这之前合租java插件。

配置gradle

在上面创建的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是创建和修改java文件的工具,不是必须的,但建议使用,Java原生的API实在是难用,没有必要花时间去学习不好的东西。
  2. autoservice也不是必需的,但建议使用,它可以处理好编译时注解需要的配置,如果不使用它则需要手动配置,后面讲到autoservice的使用时会同步说明下手动配置的相关操作。

在调用方的build.gradle中加入如下依赖:

// annotationProcessor专用依赖方式,此依赖下的库不会打包到apk中
annotationProcessor(':sgetter')
// 如果库中仅有processor类,那么不需要compile;如果注解也放在库里,或者还提供了其他类可供调用,那么需要compile
compile project(':sgetter')

用例实践

本文所使用的例子是动态生成一个类,该类的职责是为其它对象的字段赋值,我们先来看下使用注解的类以及自动生成的类长啥样:

// 使用注解的类
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;
  }
}

接下去开始讲解具体的编码过程。

创建注解

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

创建Processor

创建一个Processor类继承自AbstractProcessor

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

触发下编译,可以发现自动生成了如下图所示的Processor文件

image

这是由autoservice自动生成的,是编译时注解生效的必要条件,如果没有使用autoservice,那么需要手动创建Processor文件(路径和文件全名参照上图),然后将自定义的Processor类的全名写入该文件中,多个Processor以换行隔开。

接着,我们来看下如何通过SgetterProcessor实现自动生成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);

            // 生成构造函数
            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)};
    }
}

我们来看下几个核心对象和方法的作用(代码细节这里不再详述,很容易看懂,而且代码中也有注释):

  1. Elements:元素操作工具,如果使用javapoet的话该原生工具基本用不到,不需要过多关注。
  2. Messager:日志输出工具,Messager输出的日志本应显示在Messages窗口中,不过AS3.0之后已经找不到这个窗口了,好在Messager输出的日志会显示在终端中(和使用System.out已经区别不大了),前提是在终端中使用命令编译工程。
  3. Filer:文件操作工具,通过Filer生成的文件位于app/build/generated/source/apt中。
  4. getSupportedSourceVersion:返回jdk的版本(也可以通过@SupportedSourceVersion注解到Processor类),默认1.6。
  5. init:初始化方法,可以在这里进行一些初始化操作以及获取环境信息(环境信息已经保存processingEnv成员中,子类可以直接使用)。Processor类必须保留默认构造函数(编译时反射),并且由于初始化方法的存在,因此一般没有必要编写构造函数。
  6. getSupportedAnnotationTypes:返回当前Processor支持的注解(也可以通过@SupportedAnnotationTypes注解到Processor类),为了保持代码的整洁及可维护,一般一个Processor只处理一个注解。
  7. process:最核心的方法,用来处理注解,该方法是由基类定义的抽象方法,子类必须实现。这个方法千万不能出现异常,否则编译时会出现各种莫名其妙的问题。

常见问题整理

Processor不执行

如果Processor已经执行过则再次build便不会再执行,可以build之前clean,或者直接rebuild。

process方法不执行

如果在Processor中注册的注解没有使用,那么process方法就不用执行。需要特别注意的是,如果注解的使用者和Processor位于同一个module,那么该使用者会被忽略(笔者在这里吃了大亏,花了很长时间才找出问题)。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容