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 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 set, RoundEnvironment roundEnvironment) {
        /*
           1. set:携带getSupportedAnnotationTypes()中的注解类型,一般不需要用到。
           2. roundEnvironment:processor将扫描到的信息存储到roundEnvironment中,从这里取出所有使用Sgetter注解的字段。
          */
        Set sgetterElements = roundEnvironment.getElementsAnnotatedWith(Sgetter.class);
        Map 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 getSupportedAnnotationTypes() {
        Set 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 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,那么该使用者会被忽略(笔者在这里吃了大亏,花了很长时间才找出问题)。

你可能感兴趣的:(Android使用AnnotationProcessor处理编译时注解)