「Android」通过注解自动生成类文件:APT实战(Abstr
文/毛毛
欠了自己好几篇文章还没开始动笔。。。
今天讲点技术干货吧!
最近在做一个自动生成代码的架构,这两天调研了一下APT自动生成代码的流程,动手写了个小demo。
demo 内容:通过获取注解内容来生成新类,再通过调用新类的方法来获取注解的内容,并展示出来。
本文作为总结文,讲解demo的创建过程以及遇到的问题解决。如有描述不详之处,或是遇到了新的问题,欢迎留言探讨。
一、新建工程
创建一个普通的Android工程。
二、新建AbstractProcessor类的实现类。
@SupportedAnnotationTypes("com.autotestdemo.maomao.javalib.VInjector")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class VInjectProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment){
return false;
}
}
坑一:首先你把我这代码拷过去你会发现导不了包!根本找不到AbstractProcessor类。
原因是AbstractProcessor不在Android SDK里面!
所以我们要建【java工程】
但是我们最终要放在app里面运行的,怎么办?
那我们需要建一个java library的module来做为你主工程的引用工程,专门存放AbstractProcessor实例的相关内容。
建好library之后,需要在主工程引用它:
上面的javalib是我新建的java工程,app是我的主工程代码。
我们要在app里的build.gradle文件里添加对javalib的引用:
dependencies {
implementation project(':javalib') // 添加依赖模块
}
三、添加注解
要实现通过获取注解内容来生成新类,所以首先要有个注解。
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface VInjector {
int id();
String name();
String text();
}
@Retention(RetentionPolicy.CLASS)指定了该注解是编译时注解,即程序编译时就能获取到所有该注解的内容。
若指定的是RetentionPolicy.RUNTIME就表示是运行时注解。
@Target(ElementType.TYPE)指定了该注解是作用在类上面的,而不是属性上。
然后指定了一个int类型和两个String类型的接收字段。
用法:
@VInjector(id=1,name="Maomao",text="这是动态代码生成的")
public class MainActivity extends AppCompatActivity {
四、实现AbstractProcessor实例
这是最重要的一步:代码实现
首先看看代码:
@SupportedAnnotationTypes("com.autotestdemo.maomao.javalib.VInjector")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class VInjectProcessor extends AbstractProcessor {
private Filer mFiler;
private Messager mMessager;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
//初始化我们需要的基础工具
mFiler = processingEnv.getFiler();
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 遍历所有注解元素
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(VInjector.class)) {
analysisAnnotated(annotatedElement);
}
return false;
}
private static final String SUFFIX = "AutoClass";
/**
* 生成java文件
* @param classElement 注解
*/
private void analysisAnnotated(Element classElement) {
VInjector annotation = classElement.getAnnotation(VInjector.class);
int id = annotation.id();
String name = annotation.name();
String text = annotation.text();
String newClassName = name + SUFFIX;
StringBuilder builder = new StringBuilder()
.append("package com.autotestdemo.maomao.autotestdemo.auto;\n\n")
.append("public class ")
.append(newClassName)
.append(" {\n\n") // open class
.append("\tpublic String getMessage() {\n") // open method
.append("\t\treturn \"");
// this is appending to the return statement
builder.append(id).append(text).append(newClassName).append(" !\\n");
builder.append("\";\n") // end returne
.append("\t}\n") // close method
.append("}\n"); // close class
try { // write the file
JavaFileObject source = mFiler.createSourceFile("com.autotestdemo.maomao.autotestdemo.auto." + newClassName);
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
// Note: calling e.printStackTrace() will print IO errors
// that occur from the file already existing after its first run, this is normal
}
}
上面代码分为两部分。
第一部分:获取注解
首先我们看VInjectProcessor上面的两个注解:
@SupportedAnnotationTypes("com.autotestdemo.maomao.javalib.VInjector")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public class VInjectProcessor extends AbstractProcessor {
@SupportedAnnotationTypes()是指定哪些注解会由该类处理,里面放注解的全包名路径。
@SupportedSourceVersion()是指定编译器版本
我们再来看看process方法:
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
// 遍历所有注解元素
for (Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(VInjector.class)) {
analysisAnnotated(annotatedElement);
}
return false;
}
process方法体是获取注解内容的唯一途径。
这里面包含了所有符合条件的注解(在@SupportedAnnotationTypes()里指定的),因此我们需要循环取出当个注解实例。
roundEnvironment.getElementsAnnotatedWith(VInjector.class)是获取所有的VInjector注解集合。
第二部分:生成java文件
analysisAnnotated()方法是用于获取到注解内容之后生成与内容相关的java文件。
private static final String SUFFIX = "AutoClass";
/**
* 生成java文件
* @param classElement 注解
*/
private void analysisAnnotated(Element classElement) {
VInjector annotation = classElement.getAnnotation(VInjector.class);
int id = annotation.id();
String name = annotation.name();
String text = annotation.text();
String newClassName = name + SUFFIX;
StringBuilder builder = new StringBuilder()
.append("package com.autotestdemo.maomao.autotestdemo.auto;\n\n")
.append("public class ")
.append(newClassName)
.append(" {\n\n") // open class
.append("\tpublic String getMessage() {\n") // open method
.append("\t\treturn \"");
// this is appending to the return statement
builder.append(id).append(text).append(newClassName).append(" !\\n");
builder.append("\";\n") // end returne
.append("\t}\n") // close method
.append("}\n"); // close class
try { // write the file
JavaFileObject source = mFiler.createSourceFile("com.autotestdemo.maomao.autotestdemo.auto." + newClassName);
Writer writer = source.openWriter();
writer.write(builder.toString());
writer.flush();
writer.close();
} catch (IOException e) {
// Note: calling e.printStackTrace() will print IO errors
// that occur from the file already existing after its first run, this is normal
}
}
代码大致内容:拿到注解里面的所有内容,生成一个输出所有内容的类。
五、使用Processor
VInjectProcessor类实现好以后,我们怎么使用它?系统如何知道运行它里面的代码?
注解处理器类编写完后,还需要创建一个 java META_INF 文件来告诉系统具有注解处理功能。Java 代码在编译的时候,系统编译器会查找所有的 META_INF 中的注册的注解处理器来处理注解。
在项目中创建如下目录:
src/main/resources/META_INF/services
在main目录下创建如下目录和文件:
resources
- META-INF
- services
- javax.annotation.processing.Processor
在 services 目录下面创建一个名字为 “javax.annotation.processing.Processor” 的文本文件,Processor内容如下:
com.autotestdemo.maomao.javalib.VInjectProcessor # 指定处理器全类名
由于我们的VInjectProcessor是在子工程里面,因此我们的目录也需在子工程里面:
六、编译
做完以上步骤,编译工程之后,就可以生出新的类,生成好的类长这样:
package com.autotestdemo.maomao.autotestdemo.auto;
public class MaomaoAutoClass {
public String getMessage() {
return "1这是动态代码生成的MaomaoAutoClass !\n";
}
}
该类需要在app目录下的build目录里找,路径如下:
这里有个坑:如果你编译之后,source文件夹下面怎么也找不到apt文件夹,或者报以下错误:
Annotation processors must be explicitly declared now. The following dependencies on the compile classpath are found to contain annotation processor. Please add them to the annotationProcessor configuration.
- javalib.jar (project :javalib)
Alternatively, set android.defaultConfig.javaCompileOptions.annotationProcessorOptions.includeCompileClasspath = true to continue with previous behavior. Note that this option is deprecated and will be removed in the future.
这时候你需要在app下的build.gradle里加入如下引用:
android {
defaultConfig {
//解决多包依赖显示使用注解
javaCompileOptions { annotationProcessorOptions { includeCompileClasspath = true } }
}
}
七、调用生成的代码
编译成功以后,我们就能直接访问生成的类。
@VInjector(id=1,name="Maomao",text="这是动态代码生成的")
public class MainActivity extends AppCompatActivity {
private TextView tv;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = findViewById(R.id.text);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
MaomaoAutoClass auto = new MaomaoAutoClass();
tv.setText(auto.getMessage());
}
});
}
}
我把类名的前缀指定为Maomao,因此生成的类叫MaomaoAutoClass。
此时我们可以访问MaomaoAutoClass类并调用里面的方法。从方法获取的字符串我给它替换掉TextView原有的字符串。
至此,该功能讲解全部完毕。
效果图:
新建空的项目,点击文字
点击文字之后替换成新类获取的文字
【参考链接】
https://www.jianshu.com/p/003be1b75e28
https://www.jianshu.com/p/07ef8ba80562
https://blog.csdn.net/feirose/article/details/68486790
https://blog.csdn.net/keep_holding_on/article/details/76188657