现在越来越多的三方库运用了APT技术,如:Dagger2、ButterKnife、ARouter等,在编译时根据annotation生成相关的代码逻辑,动态生成java,class文件给开发带来了很大的便利
APT的含义
APT 的全称为:Annotation Processing Tool 可以解释为注解处理器,
它对源代码文件进行检测找出其中的Annotation,使用指定的Annotation进行额外的处理。
Annotation处理器在处理Annotation时可以根据源文件中的Annotation生成额外的源文件和其它的文件(文件具体内容由Annotation处理器的编写者决定),APT还会编译生成的源文件和原来的源文件,将它们一起生成class文件。
实现一个注解处理器
本例实现gradle版本是3.1.2
第一步:先建立一个Android Studio 工程
第二部:建立一个名为annotationlib的java lib,注意只能是java lib,不可以是android lib,这个里面主要存储我们自定义的注解,注解库指定JDK版本为1.7
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
在这个库中自定义注解
//编译时注解
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface MyAnnotation {
String value();
}
第三步:建立一个名为TestCompiler 的java lib,这个库主要存储注解处理器,注意,这里必须为Java库,不然会找不到javax包下的相关资源
看下build.gradle
apply plugin: 'java-library'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':annotationlib')
//JavaPoet 这个库的主要作用就是帮助我们通过类调用的形式来生成代码。
implementation 'com.squareup:javapoet:1.11.1'
//AutoService 主要的作用是注解 processor 类,并对其生成 META-INF 的配置信息。
implementation 'com.google.auto.service:auto-service:1.0-rc2'
}
sourceCompatibility = "1.7"
targetCompatibility = "1.7"
在这个库里,我们定义自己的注解处理器MyProcessor,每一个处理器都是继承自AbstractProcessor,必须复写 process(),一般我们复写4个方法
/**
* 每一个注解处理器类都必须有一个空的构造函数,默认不写就行;
*/
public class MyProcessor extends AbstractProcessor {
/**
* init()方法会被注解处理工具调用,并输入ProcessingEnviroment参数。
* ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
* @param processingEnv 提供给 processor 用来访问工具框架的环境
*/
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
/**
* 这相当于每个处理器的主函数main(),你在这里写你的扫描、评估和处理注解的代码,以及生成Java文件。
* 输入参数RoundEnviroment,可以让你查询出包含特定注解的被注解元素
* @param annotations 请求处理的注解类型
* @param roundEnv 有关当前和以前的信息环境
* @return 如果返回 true,则这些注解已声明并且不要求后续 Processor 处理它们;
* 如果返回 false,则这些注解未声明并且可能要求后续 Processor 处理它们
*/
@Override
public boolean process(Set annotations, RoundEnvironment roundEnv) {
return false;
}
/**
* 这里必须指定,这个注解处理器是注册给哪个注解的。注意,它的返回值是一个字符串的集合,包含本处理器想要处理的注解类型的合法全称
* @return 注解器所支持的注解类型集合,如果没有这样的类型,则返回一个空集合
*/
@Override
public Set getSupportedAnnotationTypes() {
Set annotataions = new LinkedHashSet();
annotataions.add(MyAnnotation.class.getCanonicalName());
return annotataions;
}
/**
* 指定使用的Java版本,通常这里返回SourceVersion.latestSupported(),默认返回SourceVersion.RELEASE_6
* @return 使用的Java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
}
也可以注解的形式规定JDK版本和处理的注解
//指定编译的Java版本
@SupportedSourceVersion(SourceVersion.RELEASE_7)
//指定处理的注解
@SupportedAnnotationTypes({"com.baidu.bpit.aibaidu.annotationlib.MyAnnotation"})
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> set, RoundEnvironment roundEnvironment) {
MethodSpec main = MethodSpec.methodBuilder("main")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(void.class)
.addParameter(String[].class, "args")
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(main)
.build();
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
return false;
}
}
我们这个处理器生成一个HelloWorld的类
运行注解处理器
在运行前首先,要让主app依赖俩个java lib,看下主app的build.gradle
android {
compileSdkVersion 28
defaultConfig {
....
//记得加上这个
javaCompileOptions {
annotationProcessorOptions {
includeCompileClasspath true
}
}
}
dependencies {
implementation project(':annotationlib')
//用annotationProcessor project(':TestCompiler')替换 implementation project(':TestCompiler')
//annotationProcessor 大体来讲它有两个作用:
//能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留的东西
//能够辅助 Android Studio 在项目的对应目录中存放注解处理器在编译期间生成的文件
annotationProcessor project(':TestCompiler')
...
}
这里有个问题需要注意,用annotationProcessor去依赖注解处理器,这样可以
1 能在编译时期去依赖注解处理器并进行工作,但在生成 APK 时不会包含任何遗留的东西
2能够辅助 Android Studio 在项目的对应目录中存放注解处理器在编译期间生成的文件
这个时候你去编译还是没有任何输出的,你还需要做以下步骤
1、在 processors 库的 main 目录下新建 resources 资源文件夹;
2、在 resources文件夹下建立 META-INF/services 目录文件夹;
3、在 META-INF/services 目录文件夹下创建 javax.annotation.processing.Processor 文件;
4、在 javax.annotation.processing.Processor 文件写入注解处理器的全称,包括包路径;
然后再主项目中加上你的注解就行了
@MyAnnotation("main")
public class MainActivity extends AppCompatActivity {
}
最后我们先Clean以下项目,在测Rebuild 项目就可以了,我们就可以看到生成的helloworld文件
现在注解处理器就可以正常工作了,我们还有一个需要优化的地方,上方我们自己定义了META-INF文件夹和路径,是不是很麻烦没有没有更加方便的方式,答案是,当然有,AutoService就可以帮你解决
首先依赖这个库
//AutoService 主要的作用是注解 processor 类,并对其生成 META-INF 的配置信息。
implementation 'com.google.auto.service:auto-service:1.0-rc2'
然后在你的处理器上加上
@AutoService(Processor.class)
public class MyProcessor extends AbstractProcessor {
// ...
}
只需要在刚才MyProcessor上方加上这句注解就行了,是不是很方便
首先在注解库里面定义俩个注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface DiActivity {
}
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface DiView {
int value() default 0;
}
然后再注解处理器库里实现一个注解处理器
@AutoService(Processor.class)
public class DIProcessor extends AbstractProcessor {
private Elements elementUtils;
@Override
public Set getSupportedAnnotationTypes() {
// 规定需要处理的注解
return Collections.singleton(DiActivity.class.getCanonicalName());
}
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("DIProcessor");
Set extends Element> elements = roundEnv.getElementsAnnotatedWith(DiActivity.class);
for (Element element : elements) {
// 判断是否Class
TypeElement typeElement = (TypeElement) element;
List extends Element> members = elementUtils.getAllMembers(typeElement);
MethodSpec.Builder bindViewMethodSpecBuilder = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(TypeName.VOID)
.addParameter(ClassName.get(typeElement.asType()), "activity");
for (Element item : members) {
DiView diView = item.getAnnotation(DiView.class);
if (diView == null) {
continue;
}
bindViewMethodSpecBuilder.addStatement(String.format("activity.%s = (%s) activity.findViewById(%s)", item.getSimpleName(), ClassName.get(item.asType()).toString(), diView.value()));
}
TypeSpec typeSpec = TypeSpec.classBuilder("DI" + element.getSimpleName())
.superclass(TypeName.get(typeElement.asType()))
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addMethod(bindViewMethodSpecBuilder.build())
.build();
JavaFile javaFile = JavaFile.builder(getPackageName(typeElement), typeSpec).build();
try {
javaFile.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private String getPackageName(TypeElement type) {
return elementUtils.getPackageOf(type).getQualifiedName().toString();
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
elementUtils = processingEnv.getElementUtils();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.RELEASE_7;
}
}
然后再主app里面使用
@DiActivity()
public class MainActivity extends AppCompatActivity {
@DiView(R.id.text)
public TextView textView;
@DiView(R.id.edit)
public EditText editText;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
DIMainActivity.bindView(this);
textView.setText("我是通过注解来的");
editText.setText("3333333");
}
}
其他步骤和上面一样,不在重复叙述
运行之后就会生成一个类
public final class DIMainActivity extends MainActivity {
public static void bindView(MainActivity activity) {
activity.textView = (android.widget.TextView) activity.findViewById(2131165316);
activity.editText = (android.widget.EditText) activity.findViewById(2131165237);
}
}
好了本片文章正式完结
GitHub:
参考:https://juejin.im/entry/57ad3fa47db2a200540c9251
https://joyrun.github.io/2016/07/19/AptHelloWorld/