java注解报错_Java 注解预处理 Annotation Processing & 代码生成

  • Post author:
  • Post category:java


关于 Java 的注解预处理的资料实在是过于稀少,连stackoverflow上都没多少人研究,以致于我这个萌新在尝试使用注解预处理来生成代码时踩了不少坑,正好博客也快长草了,遂决定留一篇文章,希望能够对后来者有所帮助。

本文章同时对一般 Java 项目和 Android 项目适用。

为何使用 Java 注解预处理

诚然,用反射处理注解来替代代码的复制粘贴可以让代码更加简洁、易懂(优雅),但是,反射实在是太 慢

了。

啥?反射不慢?来来来,一个 Activity 就用几十次反射,要不要和复制粘贴做一下对比?(手动阴险)

那反射这么慢,有没有什么办法?当然就是今天的主题了——代码生成: 让编译器来给你“复制粘贴”,既优雅,又高效(反正生成的代码你也不看)。

如何使用 Java 注解预处理

关于注解预处理的基本使用方法的资料还是很多的,这里就不细说了,概括一下就是:

javax.annotation.processing.AbstractProcessor

META-INF.services.javax.annotation.processing.Processor

注意:对于 Android 项目,你需要单独建立一个 “Java 类” 项目,不可以直接在原 Android 项目中使用 注解预处理,否则你会发现没有 javax 这个包。

然后,在 Android 项目的 build.gradle

中的 dependencies

添加 annotationProcessor project(‘:项目名’)

处理我们的注解

假定我们要处理的注解名为 ViewAutoLoad

,定义为:

@Retention(RetentionPolicy.CLASS) //保留此注解到编译期

@Target(ElementType.FIELD) //此注解只适用于“字段”

public @interface ViewAutoLoad {

}

本文通过介绍对字段注解的处理来讲述如何实现注解预处理,对于方法,用法其实没啥区别。

然后,重写 process

方法:

@Override

public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {

return true;

}

为啥要留个 return true

? true表示这个注解已经被我们处理过了,编译器不用再调用其他注解处理器了。

然后开始写我们的处理代码,这里就有两种处理注解的办法了:

办法1:一次性全局处理注解

这种方法 不能

知道这个字段(方法)到底是哪个类的,自然也不能获取除了你正在处理的字段(方法)所在类的其他信息,但是用起来方便一些。

获取 全局所有

具有此注解的字段,然后用 processAnnotation

方法逐一处理它们:

roundEnv.getElementsAnnotatedWith(ViewAutoLoad.class).forEach(this::processAnnotation);

这里先讲一些常用操作,假定我们现在在实现上文的 processAnnotation

方法,它的方法签名为:

private void processFormNotEmpty(Element annotatedElement)

获取字段的类型

如果你将来要生成代码或者将注解用作编译时检查,十有八九要用到这个字段的类型。

TypeMirror fieldType = annotatedElement.asType();

获取这个字段的注解,或者注解的值

ViewAutoLoad annotation = annotatedElement.getAnnotation(ViewAutoLoad.class);

现在,你可以直接使用你在注解接口定义的方法了,虽然作为示例的 ViewAutoLoad

没定义任何方法。

假装定义了 value()

: annotation.value()

获取这个字段(方法)的名字

我觉得这个肯定会用吧

Name fieldVarName = annotatedElement.getSimpleName();

//string: fieldVarName.toString();

获取这个方法的修饰符

annotatedElement.getModifiers()

返回一个集合,这个集合装着 javax.lang.model.element.Modifier

这个枚举

办法2:逐类处理注解

虽然麻烦了点,但是这个办法让我们可以知道我们在处理哪个类了。

我们回到 process

方法:

Set extends Element> rootElements = roundEnv.getRootElements();

这次我们直接拿到所有编译器处理的类的基础信息了,嗯,没有过滤器。

现在我们得手撸过滤器了,既然是 Set,先遍历走起。

然后怎么过滤呢?这里有一些思路:

给字段(方法)上注解的时候就指定好这个类的名称,比如 @Example(“com.kenvix.test.TestClass”)

注意:不要指定成 TestClass.class

,在编译期无法这样读取类名,因为类尚未编译。

遍历所有类,通过字段(方法)的一些特征查找这个类

第一种思路

第一种可以是十分简单粗暴了。

String targetName = “com.kenvix.test.TestClass”;

Element targetClass = null;

for (Element element : rootElements) {

if(element.toString().equals(targetName)) {

targetClass = element;

break;

}

}

//这里只拿到了类,注解处理方法暂时省略,见下文。

第二种思路

显然,第一种实在是不怎么优雅,第二种方法又有这些思路:

android.*

Map> tasks = new HashMap<>();

for (Element classElement : rootElements) {

if(classElement.toString().startsWith(Environment.TargetAppPackage)) {

List extends Element> enclosedElements = classElement.getEnclosedElements();

for(Element enclosedElement : enclosedElements) {

List extends AnnotationMirror> annotationMirrors = enclosedElement.getAnnotationMirrors();

for (AnnotationMirror annotationMirror : annotationMirrors) {

if(ViewAutoLoad.class.getName().equals(annotationMirror.getAnnotationType().toString())) { //好像没有其他办法在这里判断是否是目标注解了

if(!tasks.containsKey(classElement))

tasks.put(classElement, new LinkedList<>());

tasks.get(classElement).add(enclosedElement);

}

}

}

}

}

这样,这个 Map<> 中就包含了我们需要的类和这个类持有的字段了,接下来进行处理即可

嗯?效率低?这是编译期,加钱换CPU或用第一种,请(手动滑稽)

生成代码

这里需要用到 javapoet 这个依赖,编辑gradle配置,加入依赖:

implementation ‘com.squareup:javapoet:1.8.0’

然后重写 init 方法:

protected Types typeUtil;

protected Elements elementUtil;

protected Filer filer;

protected Messager messager;

protected ProcessingEnvironment processingEnv;

@Override

public synchronized final void init(ProcessingEnvironment processingEnv) {

super.init(processingEnv);

this.processingEnv = processingEnv;

typeUtil = processingEnv.getTypeUtils();

elementUtil = processingEnv.getElementUtils();

filer = processingEnv.getFiler();

messager = processingEnv.getMessager();

onPreprocessorInit();

messager.printMessage(Diagnostic.Kind.NOTE, “Preprocessor: ” + this.getClass().getSimpleName() + ” Initialized”);

}

回到 process 方法,刚才我们已经拿到了要处理的注解,接下来开始处理这些注解:

JavaPoet 资料到处都是啊,要写还不容易?

我咋取一个不可能导入的包的类型?

这问题还是很常见的,比如我们没法在一个 Java 项目中用 Android 包的东西,但是却需要生成相关的代码.

例如,我们需要用到一个类 AppCompatActivity,它在 android.support.v7.app

这个包,则可以这样写:

ClassName appCompatClass = ClassName.get(“android.support.v7.app”, “AppCompatActivity”);

我咋表示类型通配符、泛型限定?

接上,我们还想表示 ? extends AppCompatActivity

,可以这样写:

MethodSpec.Builder builder = code; //这里是你的方法builder

builder.addTypeVariable(TypeVariableName.get(“T”, appCompatClass)).addParameter(TypeVariableName.get(“T”), “target”)

保存我们的生成的代码,并在下一步编译生成的代码

回到 process 方法,加上:

if(roundEnv.processingOver()) {

//创建FormChecker这个类

TypeSpec formChecker = TypeSpec.classBuilder(“FormChecker”)

.addModifiers(Modifier.PUBLIC, Modifier.FINAL)

.addMethods(methods)

.build();

//创建类文件

JavaFile javaFile = JavaFile.builder(“com.kenvix.eg.generated”, formChecker)

.addFileComment(getFileHeader())

.build();

try {

javaFile.writeTo(filer);

} catch (IOException ex) {

throw new IllegalStateException(ex.toString());

}

}

对同一个 javaFile

, javaFile.writeTo(filer)

只能调用一次,故需要判断是否为最后一轮注解预处理。

其他的可以看看 这篇文章

,虽然标题挺扯的(够你:horse:)

其他小问题

我咋调试啊

显然这个时候按 IDE 的断点按钮是莫得了。

直接 System.out

或 Logger

也不太好,分分钟被一堆垃圾编译消息淹没。用着还麻烦。

好吧,其实有个简单粗暴的方法,抛个运行时异常嘛,这样就能直接停止编译然后让 IDE 显示我们想要的东西了。

throw new IllegalStateException(“something”);

IDEA 对 addModifiers(), javaFile.writeTo(filer) 报错

IDEA bug

别理他,编译就行了



版权声明:本文为weixin_39608613原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。