在JDK6的时候引入了JSR269的标准,即APT(Annotation Processing Tool),用于在编译时处理源代码中的注解,从而生成额外的代码、配置文件或其他资源。与传统的运行时反射相比,APT在编译时进行处理,可以提高性能并在编译阶段捕获一些问题,减少运行时错误。
Java 编译器的工作流程
在介绍注解处理器工作原理之前,我们先来了解一下 Java 编译器的工作流程。
如上图所示,Java 源代码的编译过程可分为三个步骤:
如果在第 2 步调用注解处理器过程中生成了新的源文件,那么编译器将重复第 1、2 步,解析并且处理新生成的源文件。每次重复我们称之为一轮(Round)。也就是说,第一轮解析、处理的是输入至编译器中的已有源文件。如果注解处理器生成了新的源文件,则开始第二轮、第三轮,解析并且处理这些新生成的源文件。当注解处理器不再生成新的源文件,编译进入最后一轮,并最终进入生成字节码的第 3 步。
APT的工作流程包括以下阶段:
AnnotationProcessor
,如果没有则整个流程直接结束,否则继续Processor
们进行认领APT可以应用于许多场景,包括:
lombok
、MapStruct
优势包括:
要使用APT,需要在项目中配置注解处理器,通常是通过Maven或Gradle来实现。在编译时,注解处理器会自动触发,对带有指定注解的元素进行处理。下面是一个简单的使用APT的示例:
假设我们要实现自动序列化功能,可以使用注解@Serializable
标记需要序列化的类,然后通过APT生成相应的序列化和反序列化代码。
首先,定义注解和注解处理器:Serializable
,通过APT可以生成与之相关的代码。
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Serializable {
}
编写自定义的注解处理器需要实现javax.annotation.processing.AbstractProcessor
类,并重写process
方法。在process
方法中,可以获取被注解标记的元素,并进行相应的处理。
package com.demo.bytecode.apt;
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.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.Writer;
import java.util.Set;
@SupportedAnnotationTypes("com.demo.bytecode.apt.Serializable")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class SerializableProcessor extends AbstractProcessor {
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
if (element.getKind() == ElementKind.CLASS) {
String className = element.getSimpleName().toString();
String packageName = processingEnv.getElementUtils().getPackageOf(element).toString();
generateSerializerClass(packageName, className);
generateDeserializerClass(packageName, className);
}
}
}
return true;
}
private void generateSerializerClass(String packageName, String className) {
String serializerClassName = className + "Serializer";
StringBuilder serializerClassCode = new StringBuilder();
serializerClassCode.append("package ").append(packageName).append(";\n\n");
serializerClassCode.append("import java.io.Serializable;\n");
serializerClassCode.append("import java.io.ObjectOutputStream;\n");
serializerClassCode.append("import java.io.IOException;\n\n");
serializerClassCode.append("public class ").append(serializerClassName)
.append(" implements Serializable {\n\n");
serializerClassCode.append(" private static final long serialVersionUID = 1L;\n\n");
serializerClassCode.append(" public static void serialize(").append(className)
.append(" obj, ObjectOutputStream out) throws IOException {\n");
serializerClassCode.append(" out.writeObject(obj);\n");
serializerClassCode.append(" }\n");
serializerClassCode.append("}\n");
try {
JavaFileObject serializerFile = processingEnv.getFiler().createSourceFile(packageName + "." + serializerClassName);
try (Writer writer = serializerFile.openWriter()) {
writer.write(serializerClassCode.toString());
}
} catch (IOException e) {
e.printStackTrace();
}
}
private void generateDeserializerClass(String packageName, String className) {
String deserializerClassName = className + "Deserializer";
StringBuilder deserializerClassCode = new StringBuilder();
deserializerClassCode.append("package ").append(packageName).append(";\n\n");
deserializerClassCode.append("import java.io.Serializable;\n");
deserializerClassCode.append("import java.io.ObjectInputStream;\n");
deserializerClassCode.append("import java.io.IOException;\n\n");
deserializerClassCode.append("public class ").append(deserializerClassName)
.append(" implements Serializable {\n\n");
deserializerClassCode.append(" private static final long serialVersionUID = 1L;\n\n");
deserializerClassCode.append(" public static ").append(className)
.append(" deserialize(ObjectInputStream in) throws IOException, ClassNotFoundException {\n");
deserializerClassCode.append(" return (").append(className).append(") in.readObject();\n");
deserializerClassCode.append(" }\n");
deserializerClassCode.append("}\n");
try {
JavaFileObject deserializerFile = processingEnv.getFiler().createSourceFile(packageName + "." + deserializerClassName);
try (Writer writer = deserializerFile.openWriter()) {
writer.write(deserializerClassCode.toString());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
resources
下新建一个 META-INF/services
的目录;services
下新建一个 javax.annotation.processing.Processor
的文件,并将要注册的 Annotation Processor
的全路径写入。javax.annotation.processing.Processor
内容如下: com.demo.bytecode.apt.SerializableProcessor
上述配置后Maven编译会报如下错误
服务配置文件不正确, 或构造处理程序对象javax.annotation.processing.Processor: Provider com.demo.bytecode.apt.SerializableProcessor not found时抛出异常错误
通过Maven的编译插件的配置指定如下:
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
-proc:none
将上述apt代码打包,这样在我们项目中就可以使用了。 在项目中引入依赖
com.demo
bytecode-apt
1.0-SNAPSHOT
项目编译引入jar
org.apache.maven.plugins
maven-compiler-plugin
3.8.1
1.8
1.8
UTF-8
${java.home}/lib/rt.jar:${java.home}/lib/jce.jar:${java.home}/lib/jsse.jar
com.demo
bytecode-apt
1.0-SNAPSHOT
定义一个方法
@Serializable
public class Product {
private Long id;
private String name;
private double price;
// get set 略
}
Maven编译后在目录下生成了序列化及反序列化类
以上示例中的代码仅为示范,实际项目中可能需要更多的处理和逻辑。这些示例演示了如何使用APT来生成代码,以及如何编写自定义的注解处理器来自动化生成和处理代码。
通过本文的详细介绍,读者对APT的概念、原理和应用应该有了更深入的理解。APT作为一个强大的编译时工具,可以帮助开发者实现自动化、提高代码质量和性能,并在项目开发中发挥重要作用。随着技术的不断演进,APT有望在Java开发中扮演更加重要的角色。