文章目录
- 简介
- APT注解开发工具
- 使用场景
- 开发流程
- 前情提要
- 创建aar
- 创建jar
- 声明注解
- 注解编译处理
- gradle
- Processor
- process方法
- 完整举例
- 编译结果
- 总结
简介
相信各位Android开发对注解这个东西并不陌生,毕竟在用Arouter的时候,注解@Route是基本要使用的注解。但这只是对注解的使用。
那么在日常开发中,如何去定义注解,并通过注解在编译期间去生成一些代码,这便是本文要介绍的APT技术。
APT注解开发工具
使用场景
对于一些格式相对固定的代码,可能并不希望开发者去花费时间去编写这些固定的代码。针对这种场景,就可以考虑使用注解开发。通过声明注解,给定相对应的参数,并在编译期生成这些固定的代码。
举个简单的例子,让我们来简化一下Arouter的流程:
场景描述:一个模块内有多个类需要路由,路由表维护了名称和类的全类名用于反射(简单来说就是有个map,key是路由名称,value时全类名),那么就可以通过名称获取到对应的类。
开发流程
注解开发通常涉及两个部分(最终产物是俩jar包):
- 注解声明部分, annotation
- 注解编译处理部分,compile
前情提要
这点很重要,在这吃过亏,耽误过一段时间
library的类型有aar和jar两种
aar和安卓相关,jar和java(kotlin)相关
aar可以引用jar,但jar不能引用aar
创建aar
创建jar
声明注解
注解的声明十分简单,没有什么坑,本文介绍场景都是对类的注解(其他的没研究,偷个懒)。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface MethodBridge {
String subMethod();
}
MethodBridge这就是一个注解的名称,String subMethod()定义了注解内部包含的属性,这在编译期会被用到。
OK注解声明部分就这么些玩意。
注解编译处理
这是注解开发的核心部分,所有注解处理的相关操作都是在这里执行的
gradle
首先,这玩意是需要在编译期生成代码的,所以kapt插件是得有的。我现在用的是ktx版本的gradle,语法如下:
plugins {
kotlin("kapt")
}
依赖呢,直接抄作业就行:
dependencies {
implementation(fileTree("libs"){
include("*.jar,*.aar")
})
implementation("com.google.auto.service:auto-service:1.0-rc6")
kapt("com.google.auto.service:auto-service:1.0-rc6")
}
注意一下这个dependencies里引一下上一节定义的annotation包,因为解析的就是这里的注解。
Processor
可以将这部分理解为注解处理器
大体结构为:
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class MethodBridgeCompile extends AbstractProcessor {
private String moduleName = "CommonBridgeModule";
Filer filer;
Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager(); // 打印对象
filer = processingEnv.getFiler(); // 文件操作对象
messager.printMessage(Diagnostic.Kind.NOTE, "moduleName:::" + processingEnv.getOptions().get("BRIDGE_MODULE_NAME"));
if (processingEnv.getOptions().get("BRIDGE_MODULE_NAME") != null){
moduleName = removeInvalidChars(processingEnv.getOptions().get("BRIDGE_MODULE_NAME"));
}
}
/**
* 获取注解类型
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> types = new HashSet<>();
types.add(MethodBridge.class.getCanonicalName());
return types;
}
/**
* 声明支持的Java版本
*/
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 具体的注解处理逻辑
}
public String removeInvalidChars(String input) {
return input.replaceAll("[^A-Za-z0-9]", "");
}
}
结构这块可以直接抄作业,主要逻辑都在process方法里。注意一下getSupportedAnnotationTypes里要给的是要处理的注解的全类名。
其次,外部声明的俩成员变量:
- filer用于创建类
- messager用于编译器在控制台打日志
messager.printMessage(Diagnostic.Kind.NOTE, "你的日志");
process方法
- 首先需要获取到注解标识的类的所有节点
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(ModuleBridge.class);
- 遍历节点,因为明确该注解只会用于类注解,因此每个元素都是类节点,可以强转为TypeElement
HashMap<String, String> map = new HashMap<>();
for (Element element : elements) {
TypeElement typeElement = (TypeElement) element; // 节点一定是类节点,可以强转
String subMethod = typeElement.getAnnotation(MethodBridge.class).subMethod();
String clazzName = typeElement.getSimpleName().toString();
String package_name = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
map.put(subMethod, package_name + "." + clazzName);
}
简化的arouter的实现是想要将当前module内的所有注解标识类遍历完放到生成的类里,依次添加到路由表中。
那么路由表里每次添加到key对应了subMethod,value对应了全类名clazzName。都是通过类节点获取的。
typeElement.getAnnotation(MethodBridge.class).subMethod(); 用于获取注解中的参数
String clazzName = typeElement.getSimpleName().toString(); 用于获取类节点对应的类名
processingEnv.getElementUtils().getPackageOf(typeElement).toString();用于获取类节点所在包名。
- 创建文件
JavaFileObject javaFileObject = filer.createSourceFile(packageName + "." + newClass);
writer = javaFileObject.openWriter();
StringBuffer buffer = new StringBuffer();
......
writer.write(buffer.toString());
writer.close();
省略号部分,就是要插入的代码,你的buffer里写了啥,他就会在编译后生成啥。
完整举例
上面对一些关键代码简要介绍一下,下面完整点贴一下
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
/*
* 得到注解的Element节点
* 类节点:TypeElement
* 方法节点:ExecutableElement
* 成员变量节点:VariableElement
*/ messager.printMessage(Diagnostic.Kind.NOTE, "MethodBridgeCompiler::process");
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(MethodBridge.class);
// 初始化list,元素为被@MethodBridge标记的类节点
// Set<String> set = new HashSet<>();
HashMap<String, String> map = new HashMap<>();
messager.printMessage(Diagnostic.Kind.NOTE, "MethodBridgeCompiler开始遍历");
for (Element element : elements) {
TypeElement typeElement = (TypeElement) element; // 节点一定是类节点,可以强转
String subMethod = typeElement.getAnnotation(MethodBridge.class).subMethod();
String clazzName = typeElement.getSimpleName().toString();
String package_name = processingEnv.getElementUtils().getPackageOf(typeElement).toString();
map.put(subMethod, package_name + "." + clazzName);
}
messager.printMessage(Diagnostic.Kind.NOTE, "map.size: " + map.size());
// 编译期构建类
if (!map.isEmpty()){
Writer writer = null;
Iterator<String> iterator = map.keySet().iterator();
String packageName = "包名"; // 固定包名
messager.printMessage(Diagnostic.Kind.NOTE, "moduleName: " + moduleName);
String newClass = moduleName + "Method";
try {
JavaFileObject javaFileObject = filer.createSourceFile(packageName + "." + newClass);
writer = javaFileObject.openWriter();
StringBuffer buffer = new StringBuffer();
......
writer.write(buffer.toString());
writer.close();
}catch (Exception e){
messager.printMessage(Diagnostic.Kind.NOTE, "exception: " + e.getMessage());
}
}
return false;
}
编译结果
集成后走一下编译,产物会在如下目录
总结
基本的使用就是这样,可以试一下在buffer里随便给一些字符串看一下效果。map的逻辑也可以按照自己的逻辑选择是否添加。