起因
简单的说就是我不想手写 model 里边的 builder 了,就问度娘类似 lombok 里边 @Builder 功能实现的原理,打算自己实现一个。于是便有了此文,此文质量好全靠度娘,质量差全赖我,反正我懒。。。
介绍
APIJSR 269 提供一套标准 API(Pluggable Annotation Processing)来处理 Annotations JSR 175,JSR 269 用 Annotation Processor 在编译期间而不是运行期间处理 Annotation,Annotation Processor 相当于编译器的一个插件,所以称为插件式注解处理
如果 Annotation Processor 处理 Annotation 时(执行process方法)产生了新的 Java 代码,编译器会再调用一次 Annotation Processor,如果第二次处理还有新代码产生,就会接着调用 Annotation Processor,直到没有新代码产生为止。每执行一次 process() 方法被称为一个”round”,这样整个 Annotation processing 过程可以看作是一个 round 的序列。JSR 269 主要被设计成为针对 Tools 或者容器的 API。这个特性虽然在 JavaSE 6 已经存在,但是很少人知道它的存在
作用
这是在编译阶段生成附加源文件的便捷技术。源文件不必是 Java 文件-您可以根据源代码中的注释生成任何类型的描述,元数据,文档,资源或任何其他类型的文件。注释处理已在许多无处不在的 Java 库中积极使用,例如,在 QueryDSL 和 JPA 中生成元类,以在 Lombok 库中使用样板代码扩展类,lombok 就是使用这个特性实现编译期的代码插入的。另外,如果没有猜错,像 IDEA 在编写代码时候的标记语法错误的红色下划线也是通过这个特性实现的。KAPT(Annotation Processing for Kotlin),也就是 Kotlin 的编译也是通过此特性的
使用步骤
插件化注解处理 API 的使用步骤大概如下:
- 自定义一个 Annotation Processor,需要继承 javax.annotation.processing.AbstractProcessor,并重写 process 方法
- 自定义一个注解,注解的元注解需要指定 @Retention(RetentionPolicy.SOURCE)
- 需要在声明的自定义 Annotation Processor 中使用 javax.annotation.processing.SupportedAnnotationTypes 指定在第 2 步创建的注解类型的名称(注意需要全类名,“包名.注解类型名称”,否则会不生效)
- 需要在声明的自定义 Annotation Processor 中使用 javax.annotation.processing.SupportedSourceVersion 指定编译版本
- 可选操作,可以通在声明的自定义 Annotation Processor 中使用 javax.annotation.processing.SupportedOptions 指定编译参数
需要注意的是: - 插件化注解处理 API 的局限性-它只能用于生成新文件,而不能更改现有文件
- 老外的观点:值得注意的例外是 Lombok 库,该库使用注释处理作为一种引导机制,将其自身包含在编译过程中,并通过一些内部编译器 API 修改 AST。这种骇人听闻的技术与注解处理的预期目的无关,因此本文不予讨论
实践
一:定义对象
package com.just.demo.demo.builder;
import com.just.common.service.builder.Builder;
/**
* @author lbh
* @date 2020/7/23
*/
public class TestA {
private String username;
private String password;
private String school;
public TestA() {
}
public TestA(String username, String password, String school) {
this.username = username;
this.password = password;
this.school = school;
}
public TestA(String userName) {
this.username = userName;
}
public String getSchool() {
return school;
}
@Builder
public void setSchool(String school) {
this.school = school;
}
public String getUsername() {
return username;
}
@Builder
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
@Builder
public void setPassword(String password) {
this.password = password;
}
public TestA(String username, String password) {
this.username = username;
this.password = password;
}
@Override
public String toString() {
return "TestA{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", school='" + school + '\'' +
'}';
}
}
二:定义注解
package com.just.common.service.builder;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author lbh
* @date 2021/2/1 10:35
* @describe
*/
@Target({ElementType.METHOD})
// 必须是SOURCE级别
@Retention(RetentionPolicy.SOURCE)
public @interface Builder {
}
三:定义注解的实现
package com.just.common.service.builder;
import com.google.auto.service.AutoService;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.ExecutableType;
import javax.tools.Diagnostic;
import javax.tools.JavaFileObject;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author lbh
* @date 2021/2/1 10:36
* @describe 其实就是扫描属性,然后输出成文件
*/
@SupportedAnnotationTypes(value = {"com.just.common.service.builder.Builder"})
@SupportedSourceVersion(value = SourceVersion.RELEASE_8)
// @AutoService是谷歌的一个包,该处理器生成包含 BuilderProcessor 类名称的META-INF/services/javax.annotation.processing.Processor 文件
// <dependency>
// <groupId>com.google.auto.service</groupId>
// <artifactId>auto-service</artifactId>
// <version>1.0-rc2</version>
// </dependency>
@AutoService(Processor.class)
public class BuilderProcess extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement typeElement : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(typeElement);
Map<Boolean, List<Element>> annotatedMethods = annotatedElements.stream().collect(Collectors.partitioningBy(element -> ((ExecutableType) element.asType() .getParameterTypes().size() == 1 && element.getSimpleName().toString().startsWith("set")));
List<Element> setters = annotatedMethods.get(true);
List<Element> otherMethods = annotatedMethods.get(false);
otherMethods.forEach(element -> processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@Builder must be applied to a setXxx method with a single argument", element));
Map<String, String> setterMap = setters.stream().collect(Collectors.toMap(setter -> setter.getSimpleName().toString(),setter -> ((ExecutableType) setter.asType()).getParameterTypes().get(0).toString()));
String className = ((TypeElement) setters.get(0).getEnclosingElement()).getQualifiedName().toString();
try {
writeBuilderFile(className, setterMap);
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private void writeBuilderFile(String className, Map<String, String> setterMap)throws IOException {
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = className.substring(lastDot + 1);
String builderClassName = className + "Builder";
String builderSimpleClassName = builderClassName.substring(lastDot + 1);
JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
if (packageName != null) {
out.print("package ");
out.print(packageName);
out.println(";");
out.println();
}
out.print("public class ");
out.print(builderSimpleClassName);
out.println(" {");
out.println();
out.print(" private ");
out.print(simpleClassName);
out.print(" object = new ");
out.print(simpleClassName);
out.println("();");
out.println();
out.print(" public ");
out.print(simpleClassName);
out.println(" build() {");
out.println(" return object;");
out.println(" }");
out.println();
setterMap.forEach((methodName, argumentType) -> {
out.print(" public ");
out.print(builderSimpleClassName);
out.print(" ");
out.print(methodName);
out.print("(");
out.print(argumentType);
out.println(" value) {");
out.print(" object.");
out.print(methodName);
out.println("(value);");
out.println(" return this;");
out.println(" }");
out.println();
});
out.println("}");
}
}
}
四:运行示例
这里我是项目部署在两个模块里了,注解和注解的实现都在 common 包里,使用是在 demo 包里。必须先编译 common 包,然后再编译 demo 包
demo 包的 pom 需要有额外配置,即你的注解实现以及生成地址
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
<generatedSourcesDirectory>${project.build.directory}/generated-sources/</generatedSourcesDirectory>
<annotationProcessors>
<annotationProcessor>com.just.common.service.builder.BuilderProcess</annotationProcessor>
</annotationProcessors>
</configuration>
</plugin>
</plugins>
</build>
编译完 demo 包既可看到生成的新文件
// target/generated-sources/com/just/demo/demo/builder/TestABuilder
package com.just.demo.demo.builder;
public class TestABuilder {
private TestA object = new TestA();
public TestA build() {
return object;
}
public TestABuilder setPassword(java.lang.String value) {
object.setPassword(value);
return this;
}
public TestABuilder setUsername(java.lang.String value) {
object.setUsername(value);
return this;
}
public TestABuilder setSchool(java.lang.String value) {
object.setSchool(value);
return this;
}
}
五:应用
package com.just.demo.demo.builder;
import com.just.demo.test.vo.model.TestB;
/**
* @author lbh
* @date 2021/2/1 10:44
* @describe
*/
public class BuilderTest {
public static void main(String[] args) {
TestA testA = new TestABuilder().setSchool("1").setUsername("1").setPassword("1").build();
System.out.println(testA.toString());
}
}
六:额外方式
第四步有很多种方法可以实现 BuilderProcess 的创建,我使用了比较简单的通过 maven 加 @AutoService(Processor.class) 的方式。本质其实就是开始讲解的,循环生成需要的 Annotation Processor,对于 model 比较多的情况这种方式可能比较好一点
其他方式看参考吧