当前位置:首页 > 移动互联 > 正文

什么是注解处理器?如何生成注解处理器?

2022-11-15 08:13:29    来源:世界网    

一、定义

注解处理器(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分为两种情况。

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(Setannotations, 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")

}

1

2

3

4

5

6

7

8

9

2.5 测试

经过上面的一系列操作,注解处理器已经注册完成,在其process方法中会接收到想要处理的注解。

① 在项目中使用@Test注解

@Test(id = 100, desc = "Person类", clazz = Person.class, array = {"111", "aaa", "bbb"})

public class Person {

}

1

2

3

4

② 在注解处理器的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(Setannotations, RoundEnvironment roundEnv) {

// 获取所有的被Test注解的对象,无论是类还是属性都会被封装成Element

for (Element element : roundEnv.getElementsAnnotatedWith(Test.class)) {

messager.printMessage(Diagnostic.Kind.NOTE, ">>>>>>>>>>>>>>>GetAnnotation:" + element.getSimpleName());

}

return false;

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

③ 构建项目,查看日志,成功获取到注解注释过的类名

三、解析注解

获取到了注解,我们看一下如何正确的拿到注解内的信息,在注解处理器中类、方法、属性都会被形容成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(Setannotations, 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;

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

四、生成代码

获取到了注解信息,下一步就是根据注解生成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, SubscriberInfo> SUBSCRIBER_INDEX;\n\n");

writer.write(" static {\n");

writer.write(" SUBSCRIBER_INDEX = new HashMap, 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(ClasssubscriberClass) {\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

}

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

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!");

}

}

1

2

3

4

5

6

7

在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);

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

经过以上步骤就可以生成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();

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

② ArrayList

package com.example.helloworld;

import com.mattel.Hoverboard;

import java.util.ArrayList;

import java.util.List;

public final class HelloWorld {

List beyond() {

List 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();

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

③ 属性

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();

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

总结

最后咱们再总结一下APT。

APT是JDK提供的工具,用于在编译阶段未生成class之前对源码中的注解进行扫描和处理。

获取到注解后可以使用原始方法与JavaPoet生成Java代码。

上一篇:​逍遥模拟器如何使用虚拟定位?逍遥模拟器使用虚拟定位的操作步骤
下一篇:最后一页