Java编译时注解处理器的学习总结
APT的简介
定义
APT即是Annotation Processing Tool ,它是一个javac的一个工具,中文的意思是编译时注解处理器,可以用来在编译时扫描和处理注解。通过APT可以取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动生成一些代码,省去了手动编写。获取注解及生成代码都是在代码编译的时候完成的,相比反射在运行时处理注解大大提高了程序性能。APT的核心就是AbstractProcessor类。
如何在Android中使用APT呢
在Android工程中使用APT,至少需要两个Java Library模块组成,在Android中创建Java Library的的步骤是,首先建一个Android项目,然后点击File-> New ->New Module,打开如图所示,然后选择Java Library模块。
image.png
这两个模块的作用是:
一个Annotation模块,这个用来存在自定义的注解。
一个Compiler模块,这个模块依赖Annotation模块。
在项目的App模块和其它的业务模块都需要依赖Annotation模块,同时需要通过annotationProcessor依赖Compiler模块。
app模块的gradle中依赖关系如下:
implementation project(':annotation')
annotationProcessor project(':factory-compiler')
实现ButterKnife例子学习APT
步骤:
新建一个Android的app的工程。
创建一个Java Library ,定义要被处理的注解(apt-annotation)。
创建一个Java Library,定义注解处理器生成具体的类。(apt-processor)
创建一个Android library module,通过反射调用apt-processor模块生成的方法,实现View的绑定。
工程目录如下:
image.png
在apt-annotation中定义一个注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindView {
int value() default -1;
}
因为这个注解是在成员变量中使用的,保留的时间是在.class字节码文件,不需要值JVM期间保留,这也体现了APT只是在编译器的编译时期完成工作。
在apt-processor中的gradle文件中加入两个依赖。
为了解决android studio升级到3.4.2,gradle升级到5.1.1后,apt不会执行,没办法自动生成注解文件的问题,还需要添加 annotationProcessor。
implementation project(':apt-annotation')
implementation 'com.google.auto.service:auto-service:1.0-rc4'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc4'
第二个依赖是AutoService,它是google开发的一个库,在使用@AutoService注解时需要用到,它的作用是用来生成META.INF/services/javax.annotation.processing.Processor文件的,在使用注解器的时候就不需要手动添加该文件。
在apt-processor中创建注解处理器
该Module是Java Library,不能是Android Library,因为Android平台的是基于OpenJDK的,而OpenJDK中是不包含APT的相关代码。
BindViewProcessor要引用AutoService的注解,以及继承AbstractProcessor.并重写相应的方法:
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
return false;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
return super.getSupportedAnnotationTypes();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return super.getSupportedSourceVersion();
}
}
接下来对重写的方法进行解析
- public synchronized void init()
这个 方法是用来初始化处理器的,方法中有个ProcessingEnvironment processingEnvironment类型的参数,它是一个注解处理工具的集合。包含了众多的工具类,例如Filer可以用来编写新文件。Meessage可以用来处理错误信息。Elements是一个可以处理Element的工具类。
- 什么是Elments?
在Java语言中,Elenent是一个接口,表示一个程序的元素,例如包,类,方法,变量。Element已知的子接口有如下几种。
PackageElement 表示一个包程序元素。
ExecutableElement表示某个类或者接口的方法,构造方法和初始化程序,包括注释类型元素。
TypeElement 表示一个类或者接口的元素。注意对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。
VariableElement表示一个字段,enum常量,方法或者构成方法参数,局部变量或者异常参数。
- public boolean process()
这个是AbstractProcessor中最重要的一个方法,该方法的返回值是一个boolean类型,返回值表示注解是否由当前Processor处理,如果返回true,则这些注解由这个处理器进行处理,后续的其他Processor无需处理它们。如果返回false,则这些注解未在次Processor中处理,需要后续的其他Processor处理它们。在这个方法中,我们需要检验被注解的对象是否合法,可以编写处理注解的代码,以及自动生成需要的java文件等。处理的大部分逻辑都在这个类中。
- public Set<String> getSupportedAnnotationTypes()
这方法是返回一个set集合,集合中指定那些注解是需要这个处理器处理的。
- public SourceVersion getSupportedSourceVersion()
这个返回是返回当前的正在使用的java版本。通常返回SourceVersion.latestSupported()就可以了。
接下来是编写BingViewProcessor的代码,注意改文件中不可以含有中文,否则编译不通过。代码中的中文是为了方便理解加的解析,在运行的时候要删除。如果需要中文解析的可以在build.gradle中添加
//中文乱码问题(错误:编码GBK不可映射字符)
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
@AutoService(Processor.class)
public class BindViewProcessor extends AbstractProcessor {
private Elements mElementUtil;
private Map<String,ClassCreatorFactory> mClassCreatorFactoryMap=new HashMap<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//初始化Element
mElementUtil= processingEnvironment.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
mClassCreatorFactoryMap.clear();
//获取所有包含了@BingView注解的集合
Set<? extends Element> elements=roundEnvironment.getElementsAnnotatedWith(BindView.class);
for (Element element:elements){
VariableElement variableElement= (VariableElement) element;
TypeElement typeElement= (TypeElement) variableElement.getEnclosingElement();
String fullClassName=typeElement.getQualifiedName().toString();
ClassCreatorFactory classCreatorFactory=mClassCreatorFactoryMap.get(fullClassName);
if (classCreatorFactory==null){
classCreatorFactory=new ClassCreatorFactory( mElementUtil,typeElement);
mClassCreatorFactoryMap.put(fullClassName,classCreatorFactory);
}
BindView bindViewAnnotation=variableElement.getAnnotation(BindView.class);
int id=bindViewAnnotation.value();
classCreatorFactory.putElement(id,variableElement);
}
//开始创建Java类
for (String key:mClassCreatorFactoryMap.keySet()){
ClassCreatorFactory factory=mClassCreatorFactoryMap.get(key);
try {
JavaFileObject fileObject=processingEnv.getFiler().createSourceFile(factory.getClassFullName(),factory.getTypeElement());
Writer writer=fileObject.openWriter();
writer.write(factory.generateJavaCode());
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//返回true说明这个processor要处理这个注解,后续的Processor不需
return true;
}
@Override
public Set<String> getSupportedAnnotationTypes() {
//设置这个注解器是给哪个注解类用的,这里是给BingView的注解类使用
HashSet<String> supportType=new LinkedHashSet<>();
supportType.add(BindView.class.getCanonicalName());
return supportType;
}
@Override
public SourceVersion getSupportedSourceVersion() {
//返回java的版本
return SourceVersion.latestSupported();
}
}
ClassCreatorFactory的代码如下,注意该类的代码不能出现中文的注释,该类的代码就是相当于在代码中通过字符串的方式编写代码,需要注意空格等细节问题,这个类很容易出错,可以利用JavaPoet根据实体类生成。
public class ClassCreatorFactory {
private String mClassName;
private String mPackageName;
private TypeElement typeElement;
private Map<Integer, VariableElement> mVariableElementMap = new HashMap<>();
public ClassCreatorFactory(Elements elements, TypeElement typeElement) {
//获取该类
this.typeElement = typeElement;
//获取包元素
PackageElement packageElement = elements.getPackageOf(typeElement);
mPackageName = packageElement.getQualifiedName().toString();
String temClassName = typeElement.getSimpleName().toString();
//生成类的名称
mClassName = temClassName + "_ViewBinding";
}
/**
* 添加VariableElement,例如字段,局部变量,参数等
* @param id
* @param element
*/
public void putElement(int id,VariableElement element){
mVariableElementMap.put(id,element);
}
/**
* 创建java代码,可以看出是文本的形式,对字符进行拼接即可。
* @return
*/
public String generateJavaCode(){
StringBuilder stringBuilder=new StringBuilder();
//注释
stringBuilder.append("/**\n"+"* 通过APT自动创建的类"+"\n*/\n");
//包名
stringBuilder.append("package ").append(mPackageName).append(";\n\n");
stringBuilder.append("public class ").append(mClassName).append("{\n");
//创建方法
generateMethod(stringBuilder);
stringBuilder.append("\n}\n");
return stringBuilder.toString();
}
/**
* 创建方法
* @param stringBuilder
*/
private void generateMethod(StringBuilder stringBuilder) {
stringBuilder.append("\tpublic void bindView(");
stringBuilder.append(typeElement.getQualifiedName());
stringBuilder.append(" value) { \n");
for (int id:mVariableElementMap.keySet()){
VariableElement variableElement=mVariableElementMap.get(id);
String viewName=variableElement.getSimpleName().toString();
String viewType=variableElement.asType().toString();
stringBuilder.append("\t\tvalue.");
stringBuilder.append(viewName);
stringBuilder.append(" = ");
stringBuilder.append("(");
stringBuilder.append(viewType);
//findViewById(id);
stringBuilder.append(")(((android.app.Activity)value).findViewById( ");
stringBuilder.append(id+" ));");
stringBuilder.append("\n}\n");
}
}
public String getClassFullName(){
return mPackageName+"."+mClassName;
}
public TypeElement getTypeElement(){
return typeElement;
}
}
创建apt-sdk
通过反射调用生成类的方法。
public class BindViewUtil {
public static void bindView(Activity activity) throws ClassNotFoundException, NoSuchMethodException {
try {
Class cla=activity.getClass();
Class bindViewClass=Class.forName(cla.getName()+"_ViewBinding");
Method bindViewMethod=bindViewClass.getMethod("bindView",cla);
bindViewMethod.setAccessible(true);
bindViewMethod.invoke(bindViewClass.newInstance(),activity);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
在App中调用
需要加入以下的权限:
implementation project(":apt-annotation")
//要用 annotationProcessor ,否则编译不通过
annotationProcessor project(":apt-procrssor")
implementation project(':apt-sdk')
使用该Apt
public class MainActivity extends AppCompatActivity {
@BindView(R.id.textView)
TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
try {
BindViewUtil.bindView(this);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
textView.setText("你好,注解处理器");
}
}
接下来我们看到生成的类在下面的目录中
image.png
public class MainActivity_ViewBinding{
public void bindView(com.example.hx.apttest.MainActivity value) {
value.textView = (android.widget.TextView)(((android.app.Activity)value).findViewById( 2131165353 ));
}
}
从上面的类中可以看到调用用findViewById的方法。
参考链接:
https://blog.csdn.net/qq_20521573/article/details/82321755
http://77blogs.com/?p=199