注解处理器(APT)是什么
作者:老?匡 发布时间:2021-09-29 09:03:30
上一篇讲完注解,这篇咱们科普一 * 解的其中一种用途——注解处理器(APT),文章会手把手的帮助大家学会APT的使用,并使用简单的例子来进行练习。
一、定义
注解处理器(Annotation Processing Tool,简称APT),是JDK提供的工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。处理方式大部分都是根据注解的信息生成新的Java代码与文件。
APT使用相当广泛,EventBus、ARouter、ButterKnife等流行框架都使用了该技术。
二、生成注解处理器
2.1 创建注解模块
① 在项目中新建Module,选择【Java or Kotlin Library】,名字和包名随意填入,点击Finish。
② 在模块中定义注解,注解保留范围选择SOURCE即可(因为APT是作用在源码阶段的,生成class之前),当然选择CLASS和RUNTIME也可以,因为他们都包含SOURCE阶段。
我们创建一个Test注解,并且包含int、String、Class、String[]四种类型。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Test {
int key();
String value() default "";
Class clazz();
String[] array() default {};
}
2.2 创建注解处理器模块
① 在项目中新建Module,选择【Java or Kotlin Library】,名字和包名随意填入,点击Finish。
② 修改新建Module的build.gradle文件,根据是否使用Kotlin分为两种情况。
项目不使用Kotlin:
apply plugin: "java-library"
dependencies {
// 注解模块(必选)
implementation project(':lib-annotation')
// 注解处理器(必选)
compileOnly 'com.google.auto.service:auto-service:1.0-rc7'
annotationProcessor 'com.google.auto.service:auto-service:1.0-rc7'
// 生成代码方式之一(可选)
implementation 'com.squareup:javapoet:1.13.0'
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
项目使用Kotlin:
apply plugin: "java-library"
apply plugin: "kotlin"
apply plugin: "kotlin-kapt"
dependencies {
// 注解模块(必选)
implementation project(':lib-annotation')
// 注解处理器(必选)
kapt 'com.google.auto.service:auto-service:1.0-rc7'
implementation 'com.google.auto.service:auto-service:1.0-rc7'
// 生成代码方式之一(可选)
implementation 'com.squareup:javapoet:1.13.0'
}
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
2.3 创建注解处理器
在2.2的注解处理器模块中新建类,继承自AbstractProcessor类。
使用@AutoService、@SupportedAnnotationTypes、@SupportedSourceVersion注解注释该类,注解处理器即创建完成,具体如下:
// 让该类拥有了获取注解的能力(必选)
@AutoService(Processor.class)
// 设置该处理器支持哪几种注解(必选)
// 字符串类型,例:com.kproduce.annotation.TEST
@SupportedAnnotationTypes({Const.CARD_ANNOTATION,Const.TEST_ANNOTATION})
// 源码版本(可选)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class TestAnnotationProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
}
2.4 在app模块中引入注解处理器
在主项目app模块的build.gradle中引入注解处理器,使用kapt或annotationProcessor修饰,所以在gradle中看到这两种修饰的项目就是注解处理器项目了。
dependencies {
// 注解模块
implementation project(":lib-annotation")
// 注解处理器模块,以下二选一
// 使用Kotlin选择这种
kapt project(":compiler")
// 使用Java选择这种
annotationProcessor project(":compiler")
}
2.5 测试
经过上面的一系列操作,注解处理器已经注册完成,在其process方法中会接收到想要处理的注解。
① 在项目中使用@Test注解
@Test(id = 100, desc = "Person类", clazz = Person.class, array = {"111", "aaa", "bbb"})
public class Person {
}
② 在注解处理器的init方法中,可以通过ProcessingEnvironment参数获取Messager对象(可以打印日志),在process方法中获取到注解后输出日志查看被注解的类名。(注意:如果打印日志使用Diagnostic.Kind.ERROR,会中断构建)
@AutoService(Processor.class)
@SupportedAnnotationTypes(Const.TEST_ANNOTATION)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class TestAnnotationProcessor extends AbstractProcessor {
private Messager messager;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
// 获取Messager对象
messager = processingEnv.getMessager();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 获取所有的被Test注解的对象,无论是类还是属性都会被封装成Element
for (Element element : roundEnv.getElementsAnnotatedWith(Test.class)) {
messager.printMessage(Diagnostic.Kind.NOTE, ">>>>>>>>>>>>>>>GetAnnotation:" + element.getSimpleName());
}
return false;
}
}
③ 构建项目,查看日志,成功获取到注解注释过的类名
三、解析注解
获取到了注解,我们看一下如何正确的拿到注解内的信息,在注解处理器中类、方法、属性都会被形容成Element,由于我们定义的@Test只修饰类,所以Element也都是类。
@AutoService(Processor.class)
@SupportedAnnotationTypes(Const.TEST_ANNOTATION)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class TestAnnotationProcessor extends AbstractProcessor {
private Messager messager;
// 这个是处理Element的工具
private Elements elementTool;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
messager = processingEnv.getMessager();
elementTool = processingEnv.getElementUtils();
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 拿到被Test修饰的Element,因为我们只修饰类,所以拿到的Element都是类
for (Element element : roundEnv.getElementsAnnotatedWith(Test.class)) {
// ===============获取当前被修饰的类的信息===============
// 获取包名,例:com.kproduce.androidstudy.test
String packageName = elementTool.getPackageOf(element).getQualifiedName().toString();
// 获取类名,例:Person
String className = element.getSimpleName().toString();
// 拼装成文件名,例:com.kproduce.androidstudy.test.Person
String fileName = packageName + Const.DOT + className;
// ===============解析注解===============
// 获取注解
Test card = element.getAnnotation(Test.class);
// 注解中的int值
int id = card.id();
// 注解中的String
String desc = card.desc();
// 注解中的数组[]
String[] array = card.array();
// 获取类有比较奇葩的坑,需要特别注意!
// 在注解中拿Class然后调用getName()会抛出MirroredTypeException异常
// 处理方式可以通过捕获异常后,在异常中获取类名
String dataClassName;
try {
dataClassName = card.clazz().getName();
} catch (MirroredTypeException e) {
dataClassName = e.getTypeMirror().toString();
}
}
return true;
}
}
四、生成代码
获取到了注解信息,下一步就是根据注解生成Java代码了。但是生成代码的意义是什么呢?
答:WMRouter路由在获取到了注解信息之后,会根据注解的内容生成路由注册的代码。 把一些复杂的可能写错的冗长的代码变成了自动生成,避免了人力的浪费和错误的产生。下面是WMRouter生成的代码:
目前生成Java代码有两种方式,原始方式和JavaPoet。原始方式理解即可,咱们使用JavaPoet来解析注解、生成代码。
4.1 原始方式
原始方式就是通过流一行一行的手写代码。
优点:可读性高。
缺点:复用性差。
咱们看一下EventBus的源码就能更深刻的理解什么是原始方式:
// 截取EventBusAnnotationProcessor.java中的片段
private void createInfoIndexFile(String index) {
BufferedWriter writer = null;
try {
JavaFileObject sourceFile = processingEnv.getFiler().createSourceFile(index);
int period = index.lastIndexOf('.');
String myPackage = period > 0 ? index.substring(0, period) : null;
String clazz = index.substring(period + 1);
writer = new BufferedWriter(sourceFile.openWriter());
if (myPackage != null) {
writer.write("package " + myPackage + ";\n\n");
}
writer.write("import org.greenrobot.eventbus.meta.SimpleSubscriberInfo;\n");
writer.write("import org.greenrobot.eventbus.meta.SubscriberMethodInfo;\n");
writer.write("import org.greenrobot.eventbus.meta.SubscriberInfo;\n");
writer.write("import org.greenrobot.eventbus.meta.SubscriberInfoIndex;\n\n");
writer.write("import org.greenrobot.eventbus.ThreadMode;\n\n");
writer.write("import java.util.HashMap;\n");
writer.write("import java.util.Map;\n\n");
writer.write("/** This class is generated by EventBus, do not edit. */\n");
writer.write("public class " + clazz + " implements SubscriberInfoIndex {\n");
writer.write(" private static final Map<Class<?>, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");
writer.write(" static {\n");
writer.write(" SUBSCRIBER_INDEX = new HashMap<Class<?>, SubscriberInfo>();\n\n");
writeIndexLines(writer, myPackage);
writer.write(" }\n\n");
writer.write(" private static void putIndex(SubscriberInfo info) {\n");
writer.write(" SUBSCRIBER_INDEX.put(info.getSubscriberClass(), info);\n");
writer.write(" }\n\n");
writer.write(" @Override\n");
writer.write(" public SubscriberInfo getSubscriberInfo(Class<?> subscriberClass) {\n");
writer.write(" SubscriberInfo info = SUBSCRIBER_INDEX.get(subscriberClass);\n");
writer.write(" if (info != null) {\n");
writer.write(" return info;\n");
writer.write(" } else {\n");
writer.write(" return null;\n");
writer.write(" }\n");
writer.write(" }\n");
writer.write("}\n");
} catch (IOException e) {
throw new RuntimeException("Could not write source for " + index, e);
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
//Silent
}
}
}
}
4.2 JavaPoet
JavaPoet是使用Java的API和面向对象思想来生成.java文件的库。
优点:面向对象思想、复用性高。
缺点:学习成本高、可读性一般。
因为学习点比较多,咱们仅对用到的API进行说明,其他的可以参考GitHub地址,里面有相当全面的教程。
4.2.1 生成代码
我们先用JavaPoet生成一个HelloWorld类,下面是我们想要的Java代码:
package com.example.helloworld;
public final class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, JavaPoet!");
}
}
在JavaPoet中使用了面向对象的思想,万物皆对象,方法和类也变成了对象。在类中代码主要被分为了两块,一块是方法(MethodSpec),一块是类(TypeSpec)。
接下来我们使用JavaPoet生成这段代码,比较易懂。
① 先创建main方法的MethodSpec对象;
② 再创建HelloWorld类的TypeSpec对象,将main方法传入。
// 创建main方法的MethodSpec对象
MethodSpec main = MethodSpec.methodBuilder("main")// 方法名:main
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)// 方法修饰:public static
.returns(void.class)// 返回类型 void
.addParameter(String[].class, "args")// 参数:String[] args
.addStatement("$T.out.println($S)", System.class, "Hello, JavaPoet!")// 内容System.out.println("Hello, JavaPoet!");
.build();
// 创建HelloWorld类的TypeSpec对象,将main方法传入
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")// 类名:HelloWorld
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)// 类修饰:public final
.addMethod(main)// 添加方法main
.build();
// 构建生成文件,第一个参数为包名
JavaFile javaFile = JavaFile.builder("com.example.helloworld", helloWorld)
.build();
javaFile.writeTo(System.out);
经过以上步骤就可以生成HelloWorld类。
4.2.2 JavaPoet中的自定义类型
上面代码中会发现几个奇怪的类型写在字符串中,详细的讲解可以查看GitHub地址。
$L:值,可以放各种对象,比如int,Object等。
$S:字符串。
$T:类的引用,会自动导入该类的包,比如new Date()中的Date。
$N:定义好的Method方法名,可以调用代码中的其他方法。
4.2.3 各种案例
提供几个案例,更好的理解JavaPoet,详细的讲解可以查看GitHub地址。
① 循环
void main() {
int total = 0;
for (int i = 0; i < 10; i++) {
total += i;
}
}
// JavaPoet方式 1
MethodSpec main = MethodSpec.methodBuilder("main")
.addStatement("int total = 0")
.beginControlFlow("for (int i = 0; i < 10; i++)")
.addStatement("total += i")
.endControlFlow()
.build();
// JavaPoet方式 2
MethodSpec main = MethodSpec.methodBuilder("main")
.addCode(""
+ "int total = 0;\n"
+ "for (int i = 0; i < 10; i++) {\n"
+ " total += i;\n"
+ "}\n")
.build();
② ArrayList
package com.example.helloworld;
import com.mattel.Hoverboard;
import java.util.ArrayList;
import java.util.List;
public final class HelloWorld {
List<Hoverboard> beyond() {
List<Hoverboard> result = new ArrayList<>();
result.add(new Hoverboard());
result.add(new Hoverboard());
result.add(new Hoverboard());
return result;
}
}
// JavaPoet方式
ClassName hoverboard = ClassName.get("com.mattel", "Hoverboard");
ClassName list = ClassName.get("java.util", "List");
ClassName arrayList = ClassName.get("java.util", "ArrayList");
TypeName listOfHoverboards = ParameterizedTypeName.get(list, hoverboard);
MethodSpec beyond = MethodSpec.methodBuilder("beyond")
.returns(listOfHoverboards)
.addStatement("$T result = new $T<>()", listOfHoverboards, arrayList)
.addStatement("result.add(new $T())", hoverboard)
.addStatement("result.add(new $T())", hoverboard)
.addStatement("result.add(new $T())", hoverboard)
.addStatement("return result")
.build();
③ 属性
public class HelloWorld {
private final String android;
private final String robot;
}
// JavaPoet方式
FieldSpec android = FieldSpec.builder(String.class, "android")
.addModifiers(Modifier.PRIVATE, Modifier.FINAL)
.build();
TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld")
.addModifiers(Modifier.PUBLIC)
.addField(android)
.addField(String.class, "robot", Modifier.PRIVATE, Modifier.FINAL)
.build();
来源:https://blog.csdn.net/kuanggang_android/article/details/127685124


猜你喜欢
- 一,FileWritter写入文件FileWritter, 字符流写入字符到文件。默认情况下,它会使用新的内容取代所有现有的内容,然而,当指
- 本来就是基础知识,不能丢的太干净,今天竟然花了那么长的时间才写出来,记一下。有如下的一颗完全二叉树:先序遍历结果应该为:1 2&
- 环境:SpringFramework:4.3.5.RELEASEapollo-client:1.5.11.在项目的 resources/ME
- 本文实例为大家分享了六种Android常见控件的使用方法,供大家参考,具体内容如下1、TextView 主要用于界面上显示一段文本
- null与voidnull值用来表示数据类型未被赋予任何值,它是一种引用类型;void表示没有类型,或者说是没有任何值。null与void的
- 相信做 Java 开发的朋友,大多都是学习过或至少了解过 Java GUI 编程的,其中有大量的事件和控件的绑定,当我们需要在点击某个按钮实
- 时间处理相关类:1.java.util.Date:时间类2.java.text.DateFormat:时间格式化类(抽象类),实现类:jav
- MongoDBMongoDB作为一种NoSQL数据库产品,其实已经非常著名了。去年,由于MongoDB安全认证的薄弱,上万家公司中招。虽然是
- 前端网络访问,主流方案就是 Ajax,Vue 也不例外,在 Vue2.0 之前,网络访问较多的采用 vue-resources,Vue2.0
- 1)下载sqlite jdbc驱动http://www.xerial.org/maven/repository/artifact/org/x
- 写了一个java数组排序示例,这里分享给大家共同学习package com.yonyou.test;import java.util.Arr
- 这篇文章主要介绍了JavaWeb项目Servlet无法访问问题解决,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价
- 标识符和关键字标识符读音 biao zhi fu什么是标识符包、类、变量、方法…等等,只要是起名的地方,那个名字就是标
- 需求:应用A(通常有多个)和应用B(1个)进行 socket通讯,应用A必须知道应用B的ip地址(在应用A的配置文件中写死的),这个时候就必
- Mybatis是业界非常流行的持久层框架,轻量级、易用,在金融IT领域完全是领军地位,比Hibernate更受欢迎,优势非常多,也是非常值得
- 实践过程效果代码public partial class Form1 : Form{ public Form1()
- 引言MyBatis-Plus | 最优雅最简洁地完成数据库操作是对MyBatis-Plus的功能进行简单介绍,虽然是介绍,也让我们领略到他的
- 本文实例讲述了C#计算字符串哈希值(MD5、SHA)的方法。分享给大家供大家参考。具体如下:一、关于本文本文中是一个类库,包括下面几个函数:
- Jackson反序列化遇到的问题最近在项目中需要使用Jackson把前台转来的字符转为对象,转换过程中发生了错误,报错如下com.faste
- 代码一/// <summary> /// 截断字符串 /// </su