感谢大家和我一起,在Android世界打怪升级!

上一篇讲完注解,这篇咱们科普一下注解的其中一种用途——注解处理器(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 {@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);}@Overridepublic 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;@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);// 获取Messager对象messager = processingEnv.getMessager();}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {// 获取所有的被Test注解的对象,无论是类还是属性都会被封装成Elementfor (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;@Overridepublic synchronized void init(ProcessingEnvironment processingEnv) {super.init(processingEnv);messager = processingEnv.getMessager();elementTool = processingEnv.getElementUtils();}@Overridepublic boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {// 拿到被Test修饰的Element,因为我们只修饰类,所以拿到的Element都是类for (Element element : roundEnv.getElementsAnnotatedWith(Test.class)) {// ===============获取当前被修饰的类的信息===============// 获取包名,例:com.kproduce.androidstudy.testString packageName = elementTool.getPackageOf(element).getQualifiedName().toString();// 获取类名,例:PersonString className = element.getSimpleName().toString();// 拼装成文件名,例:com.kproduce.androidstudy.test.PersonString fileName = packageName + Const.DOT + className;// ===============解析注解===============// 获取注解Test card = element.getAnnotation(Test.class);// 注解中的int值int id = card.id();// 注解中的StringString 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();

总结

最后咱们再总结一下APT。

  1. APT是JDK提供的工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。
  2. 获取到注解后可以使用原始方法与JavaPoet生成Java代码。

这样APT的介绍就结束了,希望大家读完这篇文章,会对APT有一个更深入的了解。如果我的文章能给大家带来一点点的福利,那在下就足够开心了。

下次再见!

注解处理器(APT)是什么?相关推荐

  1. Android注解处理器APT技术简介

    Android注解处理器APT技术简介 APT是什么 例子 APT有什么用 (好处) APT原理 (为什么) APT实践 (怎么做) 参考 APT是什么 APT全称"Annotation P ...

  2. 注解和注解处理器APT

    目录 背景: 常见注解 元注解 自定义注解 1.创建自定义注解 2. 创建注解处理器 3. 注册apt 背景: 项目中xml是一种松耦合的配置文件,但是随着项目的越来越大,xml文件也会越来越复杂, ...

  3. Android 自定义注解处理器详解

    文章目录 AbstractProcessor 方法详细信息 ProcessingEnvironment 方法详细信息 1 新建 Java Library 1.1 新建 1.2 确定依赖关系 2 创建自 ...

  4. 注解提高篇:自定义注解处理器(APT)

    ## 0x01 继承AbstractProcessor抽象类 当定义好Annotation注解后,接下来就需要一个注解处理器来处理我们的自定义注解了.实现Java Annotation一般需要继承Ab ...

  5. 【Android APT】注解处理器 ( 根据注解生成 Java 代码 )

    文章目录 一.生成 Java 代码 二.实现 IButterKnife 接口 三.视图绑定主要操作 四.完整注解处理器代码 五.博客资源 Android APT 学习进阶路径 : 推荐按照顺序阅读 , ...

  6. 【Android APT】注解处理器 ( Element 注解节点相关操作 )

    文章目录 一.获取被 注解 标注的节点 二.Element 注解节点类型 三.VariableElement 注解节点相关操作 四.注解处理器 完整代码示例 五.博客资源 Android APT 学习 ...

  7. 【Android APT】注解处理器 ( 配置注解依赖、支持的注解类型、Java 版本支持 )

    文章目录 一.注解处理器 依赖 编译时注解 二.设置 注解处理器 支持的注解类型 三.设置 注解处理器 支持的 Java 版本 四.博客资源 Android APT 学习进阶路径 : 推荐按照顺序阅读 ...

  8. 【Android APT】注解处理器 ( 注解标注 与 初始化方法 )

    文章目录 一.注解处理器 AbstractProcessor 二.使用注解 @AutoService(Processor.class) 标注 注解处理器 三.注解处理器 init 初始化方法 四.注解 ...

  9. 【Android APT】编译时技术 ( 编译时注解 和 注解处理器 依赖库 )

    文章目录 一.编译时注解和注解处理器 二.创建 编译时注解 和 注解处理器 三.添加 编译时注解 和 注解处理器 依赖库依赖 四.博客资源 一.编译时注解和注解处理器 上一篇博客 [Android A ...

最新文章

  1. 使用Django开发REST 接口
  2. linux shell 把一个文件的前n行 拷贝到另一个文件中
  3. matlab xlsread参数_利用MATLAB批量完成科研数据处理
  4. Redis工作笔记-spring整合jedis
  5. 使用Google Custom Search打造站内搜索
  6. 数学趣题——渔夫抓鱼问题
  7. 【论文】Awesome Relation Extraction Paper(关系抽取)(PART III)
  8. java池模式_JAVA模式 对象池 简要代码示例
  9. OAuth2通过token访问资源服务器
  10. 使用谷歌(Google)TTS服务 – Java版开源gTTS及Python gTTS
  11. JavaEE与云服务知识概括
  12. 产品经理基础--04流程图与结构图
  13. 卷积法求解系统的零状态响应_利用卷积可以求解系统的零状态响应..ppt
  14. 【三次过】Lintcode 778. 太平洋和大西洋的水流
  15. 2023 磨水笔主页开源html源码
  16. 计算机网络安全第一课
  17. 基于steam的游戏销量预测 — PART 1 — 爬取steam游戏相关数据的爬虫
  18. ubuntu18.04合并pdf文件
  19. 通过对象实现圆柱体体积计算
  20. 图像分割-区域分裂合并算法

热门文章

  1. 3.进制——进制转换和计算机存储单位
  2. 互联网晚报 | 11/28日 星期一 | 蜜雪冰城客服回应喝出虫子店家态度消极;抖音回应世界杯直播时出现杂音;微信内测新功能...
  3. 怎样修改图片大小?如何编辑照片尺寸大小?
  4. 发包技术实现SEO快排原理解密
  5. 做一个新闻阅读类的app(1): 概要设计
  6. 鬣狗机器人_我的起源鬣狗的蛋窝位置与进入攻略
  7. 微信小程序布局display flex布局介绍
  8. 先给荣耀上鸿蒙试水,荣耀领战,华为鸿蒙HMS全球突进
  9. 惠普Envy4超极本促销 送1999元旅行箱
  10. UNIX传奇[转自酷壳]