深入理解Java虚拟机:(十五)自定义注解处理器

  • process(Set<? extends TypeElement> annoations, RoundEnvironment env) :这类似于每个处理器的main()方法。你可以在这个方法里面编码实现扫描,处理注解,生成 java 文件。使用RoundEnvironment 参数,你可以查询被特定注解标注的元素。

  • getSupportedAnnotationTypes():在这个方法里面你必须指定哪些注解应该被注解处理器注册。注意,它的返回值是一个String集合,包含了你的注解处理器想要处理的注解类型的全称。换句话说,你在这里定义你的注解处理器要处理哪些注解。

  • getSupportedSourceVersion() : 用来指定你使用的 java 版本。通常你应该返回SourceVersion.latestSupported() 。不过,如果你有足够的理由坚持用 java 8 的话,你也可以返回SourceVersion.RELEASE_8。我建议使用SourceVersion.latestSupported()

JDK 提供了一个实现Processor接口的抽象类AbstractProcessor。该抽象类实现了initgetSupportedAnnotationTypesgetSupportedSourceVersion方法。

它的子类可以通过@SupportedAnnotationTypes@SupportedSourceVersion注解来声明所支持的注解类型以及 Java 版本。

下面这段代码便是@CheckGetter注解处理器的实现。由于我使用了 Java 8 的编译器,因此将支持版本设置为SourceVersion.RELEASE_8

package com.jvm;

import javax.annotation.processing.AbstractProcessor;

import javax.annotation.processing.RoundEnvironment;

import javax.annotation.processing.SupportedAnnotationTypes;

import javax.annotation.processing.SupportedSourceVersion;

import javax.lang.model.SourceVersion;

import javax.lang.model.element.ExecutableElement;

import javax.lang.model.element.Modifier;

import javax.lang.model.element.TypeElement;

import javax.lang.model.element.VariableElement;

import javax.lang.model.util.ElementFilter;

import javax.tools.Diagnostic;

import java.util.Set;

@SupportedAnnotationTypes(“com.jvm.CheckGetter”)

@SupportedSourceVersion(SourceVersion.RELEASE_8)

public class CheckGetterProcessor extends AbstractProcessor {

@Override

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

// TODO: annotated ElementKind.FIELD

for (TypeElement annotatedClass : ElementFilter.typesIn(roundEnv.getElementsAnnotatedWith(CheckGetter.class))) {

for (VariableElement field : ElementFilter.fieldsIn(annotatedClass.getEnclosedElements())) {

if (!containsGetter(annotatedClass, field.getSimpleName().toString())) {

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,

String.format(“getter not found for ‘%s.%s’.”, annotatedClass.getSimpleName(), field.getSimpleName()));

}

}

}

return false;

}

private boolean containsGetter(TypeElement typeElement, String name) {

String getter = “get” + name.substring(0, 1).toUpperCase() + name.substring(1).toLowerCase();

for (ExecutableElement executableElement : ElementFilter.methodsIn(typeElement.getEnclosedElements())) {

if (!executableElement.getModifiers().contains(Modifier.STATIC)

&& executableElement.getSimpleName().toString().equals(getter)

&& executableElement.getParameters().isEmpty()) {

return true;

}

}

return false;

}

}

该注解处理器仅重写了process方法。这个方法将接收两个参数,分别代表该注解处理器所能处理的注解类型,以及囊括当前轮生成的抽象语法树的RoundEnvironment

由于该处理器针对的注解仅有@CheckGetter一个,而且我们并不会读取注解中的值,因此第一个参数并不重要。在代码中,我直接使用了 roundEnv.getElementsAnnotatedWith(CheckGetter.class) 来获取所有被@CheckGetter注解的类(以及字段)。

process方法涉及各种不同类型的Element,分别指代 Java 程序中的各个结构。如TypeElement指代类或者接口,VariableElement指代字段、局部变量、enum 常量等,ExecutableElement指代方法或者构造器。

package foo; // PackageElement

class Foo { // TypeElement

int a; // VariableElement

static int b; // VariableElement

Foo () {} // ExecutableElement

void setA ( // ExecutableElement

int newA // VariableElement

) {}

}

我们可以通过TypeElement.getEnclosedElements方法,获得上面这段代码中Foo类的字段、构造器以及方法。

我们也可以通过ExecutableElement.getParameters方法,获得setA方法的参数。具体这些Element类都有哪些 API,你可以参考它们的 Javado

在将该注解处理器编译成 class 文件后,我们便可以将其注册为 Java 编译器的插件,并用来处理其他源代码。

注册的方法主要有两种。第一种是直接使用 javac 命令的-processor参数,如下所示:

javac -cp /CLASSPATH/TO/CheckGetterProcessor -processor com.jvm.CheckGetterProcessor Foo.java

error: Class ‘Foo’ is annotated as @CheckGetter, but field ‘a’ is without getter

1 error

第二种则是将注解处理器编译生成的 class 文件压缩入 jar 包中,并在 jar 包的配置文件中记录该注解处理器的包名及类名,即com.jvm.CheckGetterProcessor

(具体路径及配置文件名为META-INF/services/javax.annotation.processing.Processor

当启动 Java 编译器时,它会寻找 classpath 路径上的 jar 包是否包含上述配置文件,并自动注册其中记录的注解处理器。

$ javac -cp /PATH/TO/CheckGetterProcessor.jar Foo.java

error: Class ‘Foo’ is annotated as @CheckGetter, but field ‘a’ is without getter

1 error

此外,我们还可以在 IDE 中配置注解处理器。这里我就不过多演示了,感兴趣的小伙伴可以自行搜索。

三、利用注解处理器生成源代码


前面提到,注解处理器可以用来修改已有源代码或者生成源代码。

确切地说,注解处理器并不能真正地修改已有源代码。这里指的是修改由 Java 源代码生成的抽象语法树,在其中修改已有树节点或者插入新的树节点,从而使生成的字节码发生变化。

对抽象语法树的修改涉及了 Java 编译器的内部 API,这部分很可能随着版本变更而失效。因此,我并不推荐这种修改方式。

我们用注解处理器来生成源代码则比较常用。我们以前介绍过的压力测试 jcstress,以及接下来即将介绍的 JMH 工具,都是依赖这种方式来生成测试代码的。

package com.jvm;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

@Target(ElementType.METHOD)

@Retention(RetentionPolicy.SOURCE)

public @interface Adapt {

Class<?> value();

}

在上面这段代码中,我定义了一个注解@Adapt。这个注解将接收一个Class类型的参数value(如果注解类仅包含一个名为value的参数时,那么在使用注解时,我们可以省略value=),具体用法如这段代码所示。

package com.jvm;

import java.util.function.IntBinaryOperator;

public class Bar {

@Adapt(IntBinaryOperator.class)

public static int add(int a, int b) {

return a + b;

}

}

接下来,我们来实现一个处理@Adapt注解的处理器。该处理器将生成一个新的源文件,实现参数value所指定的接口,并且调用至被该注解所标注的方法之中。

package com.jvm;

import javax.annotation.processing.*;

import javax.lang.model.SourceVersion;

import javax.lang.model.element.*;

import javax.lang.model.type.TypeMirror;

import javax.lang.model.util.ElementFilter;

import javax.tools.Diagnostic;

import javax.tools.JavaFileObject;

import java.io.IOException;

import java.io.PrintWriter;

import java.util.Set;

@SupportedAnnotationTypes(“com.jvm.Adapt”)

@SupportedSourceVersion(SourceVersion.RELEASE_8)

public class AdaptProcessor extends AbstractProcessor {

@Override

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

for (TypeElement annotation : annotations) {

if (!“com.jvm.Adapt”.equals(annotation.getQualifiedName().toString())) {

continue;

}

ExecutableElement targetAsKey = getExecutable(annotation, “value”);

for (ExecutableElement annotatedMethod : ElementFilter.methodsIn(roundEnv.getElementsAnnotatedWith(annotation))) {

if (!annotatedMethod.getModifiers().contains(Modifier.PUBLIC)) {

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, “@Adapt on non-public method”);

continue;

}

if (!annotatedMethod.getModifiers().contains(Modifier.STATIC)) {

// TODO support non-static methods

continue;

}

TypeElement targetInterface = getAnnotationValueAsTypeElement(annotatedMethod, annotation, targetAsKey);

if (targetInterface.getKind() != ElementKind.INTERFACE) {

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, “@Adapt with non-interface input”);

continue;

}

TypeElement enclosingType = getTopLevelEnclosingType(annotatedMethod);

createAdapter(enclosingType, annotatedMethod, targetInterface);

}

}

return true;

}

private void createAdapter(TypeElement enclosingClass, ExecutableElement annotatedMethod,

TypeElement targetInterface) {

PackageElement packageElement = (PackageElement) enclosingClass.getEnclosingElement();

String packageName = packageElement.getQualifiedName().toString();

String className = enclosingClass.getSimpleName().toString();

String methodName = annotatedMethod.getSimpleName().toString();

String adapterName = className + “_” + methodName + “Adapter”;

ExecutableElement overriddenMethod = getFirstNonDefaultExecutable(targetInterface);

try {

Filer filer = processingEnv.getFiler();

JavaFileObject sourceFile = filer.createSourceFile(packageName + “.” + adapterName, new Element[0]);

try (PrintWriter out = new PrintWriter(sourceFile.openWriter())) {

out.println("package " + packageName + “;”);

out.println("import " + targetInterface.getQualifiedName() + “;”);

out.println();

out.println(“public class " + adapterName + " implements " + targetInterface.getSimpleName() + " {”);

out.println(" @Override");

out.println(" public " + overriddenMethod.getReturnType() + " " + overriddenMethod.getSimpleName()

  • formatParameter(overriddenMethod, true) + " {");

out.println(" return " + className + “.” + methodName + formatParameter(overriddenMethod, false) + “;”);

out.println(" }");

out.println(“}”);

}

} catch (IOException e) {

throw new RuntimeException(e);

}

}

private ExecutableElement getExecutable(TypeElement annotation, String methodName) {

for (ExecutableElement method : ElementFilter.methodsIn(annotation.getEnclosedElements())) {

if (methodName.equals(method.getSimpleName().toString())) {

return method;

}

}

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, “Incompatible @Adapt.”);

return null;

}

private ExecutableElement getFirstNonDefaultExecutable(TypeElement annotation) {

for (ExecutableElement method : ElementFilter.methodsIn(annotation.getEnclosedElements())) {

if (!method.isDefault()) {

return method;

}

}

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,

“Target interface should declare at least one non-default method.”);

return null;

}

private TypeElement getAnnotationValueAsTypeElement(ExecutableElement annotatedMethod, TypeElement annotation,

ExecutableElement annotationFunction) {

TypeMirror annotationType = annotation.asType();

for (AnnotationMirror annotationMirror : annotatedMethod.getAnnotationMirrors()) {

if (processingEnv.getTypeUtils().isSameType(annotationMirror.getAnnotationType(), annotationType)) {

AnnotationValue value = annotationMirror.getElementValues().get(annotationFunction);

if (value == null) {

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, “Unknown @Adapt target”);

continue;

}

TypeMirror targetInterfaceTypeMirror = (TypeMirror) value.getValue();

return (TypeElement) processingEnv.getTypeUtils().asElement(targetInterfaceTypeMirror);

}

}

processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, “@Adapt should contain target()”);

return null;

}

private TypeElement getTopLevelEnclosingType(ExecutableElement annotatedMethod) {

TypeElement enclosingType = null;

Element enclosing = annotatedMethod.getEnclosingElement();

while (enclosing != null) {

if (enclosing.getKind() == ElementKind.CLASS) {

enclosingType = (TypeElement) enclosing;

} else if (enclosing.getKind() == ElementKind.PACKAGE) {

最后

一次偶然,从朋友那里得到一份“java高分面试指南”,里面涵盖了25个分类的面试题以及详细的解析:JavaOOP、Java集合/泛型、Java中的IO与NIO、Java反射、Java序列化、Java注解、多线程&并发、JVM、Mysql、Redis、Memcached、MongoDB、Spring、Spring Boot、Spring Cloud、RabbitMQ、Dubbo 、MyBatis 、ZooKeeper 、数据结构、算法、Elasticsearch 、Kafka 、微服务、Linux。

这不,马上就要到招聘季了,很多朋友又开始准备“金三银四”的春招啦,那我想这份“java高分面试指南”应该起到不小的作用,所以今天想给大家分享一下。

image

请注意:关于这份“java高分面试指南”,每一个方向专题(25个)的题目这里几乎都会列举,在不看答案的情况下,大家可以自行测试一下水平 且由于篇幅原因,这边无法展示所有完整的答案解析

singType = (TypeElement) enclosing;

} else if (enclosing.getKind() == ElementKind.PACKAGE) {

最后

一次偶然,从朋友那里得到一份“java高分面试指南”,里面涵盖了25个分类的面试题以及详细的解析:JavaOOP、Java集合/泛型、Java中的IO与NIO、Java反射、Java序列化、Java注解、多线程&并发、JVM、Mysql、Redis、Memcached、MongoDB、Spring、Spring Boot、Spring Cloud、RabbitMQ、Dubbo 、MyBatis 、ZooKeeper 、数据结构、算法、Elasticsearch 、Kafka 、微服务、Linux。

这不,马上就要到招聘季了,很多朋友又开始准备“金三银四”的春招啦,那我想这份“java高分面试指南”应该起到不小的作用,所以今天想给大家分享一下。

[外链图片转存中…(img-SbJ58MKd-1714204927272)]

请注意:关于这份“java高分面试指南”,每一个方向专题(25个)的题目这里几乎都会列举,在不看答案的情况下,大家可以自行测试一下水平 且由于篇幅原因,这边无法展示所有完整的答案解析

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值