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'
-
javapoet
是创建和修改java文件的工具,不是必须的,但建议使用,Java原生的API实在是难用,没有必要花时间去学习不好的东西。 -
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文件
这是由
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 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)};
}
}
我们来看下几个核心对象和方法的作用(代码细节这里不再详述,很容易看懂,而且代码中也有注释):
-
Elements
:元素操作工具,如果使用javapoet
的话该原生工具基本用不到,不需要过多关注。 -
Messager
:日志输出工具,Messager
输出的日志本应显示在Messages窗口中,不过AS3.0之后已经找不到这个窗口了,好在Messager
输出的日志会显示在终端中(和使用System.out已经区别不大了),前提是在终端中使用命令编译工程。 -
Filer
:文件操作工具,通过Filer
生成的文件位于app/build/generated/source/apt
中。 -
getSupportedSourceVersion
:返回jdk的版本(也可以通过@SupportedSourceVersion
注解到Processor
类),默认1.6。 -
init
:初始化方法,可以在这里进行一些初始化操作以及获取环境信息(环境信息已经保存processingEnv
成员中,子类可以直接使用)。Processor
类必须保留默认构造函数(编译时反射),并且由于初始化方法的存在,因此一般没有必要编写构造函数。 -
getSupportedAnnotationTypes
:返回当前Processor
支持的注解(也可以通过@SupportedAnnotationTypes
注解到Processor
类),为了保持代码的整洁及可维护,一般一个Processor
只处理一个注解。 -
process
:最核心的方法,用来处理注解,该方法是由基类定义的抽象方法,子类必须实现。这个方法千万不能出现异常,否则编译时会出现各种莫名其妙的问题。
常见问题整理
Processor不执行
如果Processor
已经执行过则再次build便不会再执行,可以build之前clean,或者直接rebuild。
process方法不执行
如果在Processor
中注册的注解没有使用,那么process
方法就不用执行。需要特别注意的是,如果注解的使用者和Processor
位于同一个module,那么该使用者会被忽略(笔者在这里吃了大亏,花了很长时间才找出问题)。