程序的作用之一,是将重复的工作自动化。
在上篇文章 《基于 Spring Data JPA 实现简单的分表功能》中,我们介绍了一个简单的分表方案。但有一点我不满意:当一张大表被分成 N 个子表时,需要手动创建 N 个 实体类 和 N 个 Repository 类。直觉告诉我,手动干这种事有点傻!
我们知道,像项目中用到的 Lombok、MapStruct、QueryDSL 等库,它们能根据配置,帮我们自动生成一些 Java 类。如果能模仿它们,自己写一套自动生成分表的 实体类和 Repository 类,那分表操作将更加简单且智能了。
其实 Java 为我们提供了一个完整的代码生成方案 —— Annotation Processing 机制。我们要做的,是实现 javax.annotation.processing.Processor 这个接口,或者更简单的,继承 javax.annotation.processing.AbstractProcessor 这个抽象类,在其 process() 方法中编写具体的生成逻辑即可。
下面说说我使用它来自动生成 实体类 和 Repository 类 的步骤。
1. 先创建一个注解,用来设置分表参数:
/**
* 分表配置
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface TableSplit {
/**
* 分成几张表
*/
int value();
}
它的作用是:
-
设置一个实体要分成几张表; -
方便编译器去定位用到了该注解的实体类,以让自己的 Processor 做后续的解析处理;
2. 编写一个 AbstractProcessor
的子类,用来处理使用了上述注解的类:
// 指定该 Processor 要处理的注解类
@SupportedAnnotationTypes("example.processor.TableSplit")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class TableSplitProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// 源码生成逻辑在这里
return true;
}
}
process() 方法是该类的核心,它的入参提供了使用了指定注解的相关元素的信息。具体实现会在后面给出。
3. 为了源码生成方便,在 resources 目录下分别创建 实体类 和 Repositoty类 的模板文件:
EntityTemplate.java 实体的模板文件:
package $package$;
import javax.persistence.*;
@Entity
public class $Entity$ extends $Base$ {
}
RepositoryTemplate.java Repository 类的模板文件:
package $package$;
import org.springframework.data.jpa.repository.JpaRepository;
public interface $Entity$Repository extends JpaRepository<$Entity$, Long> {
}
在生成具体的源码文件时,上面模板中的 $xxx$
会被替换为真实的类名。
有了以上基础,我们再来看看 TableSplitProcessor.java 的全部代码,重要的部分都加了注释:
package example.processor;
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.tools.JavaFileObject;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.Scanner;
import java.util.Set;
@SupportedAnnotationTypes("example.processor.TableSplit")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class TableSplitProcessor extends AbstractProcessor {
private final String entityTemplate;
private final String repositoryTemplate;
public TableSplitProcessor() {
super();
try {
entityTemplate = loadTemplate("EntityTemplate.java");
repositoryTemplate = loadTemplate("RepositoryTemplate.java");
} catch (IOException e) {
throw new RuntimeException("加载实体模板文档出错.", e);
}
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
try {
for (Element annotatedElement : annotatedElements) {
// 读取被注解的类名
String entityName = annotatedElement.getSimpleName().toString();
// 读取被注解的类所在的包名
String entityPackage = annotatedElement.getEnclosingElement().toString();
// 读取注解配置
TableSplit tableSplit = annotatedElement.getAnnotation(TableSplit.class);
int split = tableSplit.value();
for (int i = 0; i < split; i++) {
// 为每张子表生成 实体类文件 和 Repository 类文件
writeEntityFile(entityName, entityPackage, i);
writeRepositoryFile(entityName, entityPackage, i);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
return true;
}
private void writeEntityFile(String baseEntityName, String entityPackage, int sn) throws IOException {
String className = baseEntityName + sn;
String fullClassName = entityPackage + "." + className;
String source = entityTemplate.replaceFirst("\$package\$", entityPackage)
.replaceFirst("\$Entity\$", className)
.replaceFirst("\$Base\$", baseEntityName);
JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(fullClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
out.println(source);
}
}
private void writeRepositoryFile(String baseEntityName, String entityPackage, int sn) throws IOException {
String className = baseEntityName + sn;
String fullClassName = entityPackage + "." + className + "Repository";
String source = repositoryTemplate.replaceFirst("\$package\$", entityPackage)
.replaceAll("\$Entity\$", className);
JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(fullClassName);
try (PrintWriter out = new PrintWriter(builderFile.openWriter())) {
out.println(source);
}
}
/**
* 加载模板文件
*/
private String loadTemplate(String template) throws IOException {
try (InputStream in = this.getClass().getResourceAsStream("/" + template)) {
try (Scanner scanner = new Scanner(in).useDelimiter("\A")) {
return scanner.hasNext() ? scanner.next() : "";
}
}
}
}
需要注意的是 @AutoService 注解是为了给模块自动生成 META-INF/services/javax.annotation.processing.Processor 文件,这样引用该模块的项目不用任何配置,就能自动应用上 TableSplitProcessor 了。有趣的是,@AutoService 也是由另一个 Annotation Processor 来生成的。
以防你需要,@AutoService 的 maven 声明是:
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>${auto-service.version}</version>
<scope>provided</scope>
</dependency>
如何使用
在一个 maven 构建的项目中引入上面 Processor 的模块依赖;编写两个实体类,假设这两个实体都需要进行分表操作:
Message 实体需要分 2 张表:
@TableSplit(2)
@MappedSuperclass
public class Message {
@Id
@Column(columnDefinition = "bigint(20) COMMENT 'ID,自增'")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String userNo;
@Column
private String message;
}
User 实体需要分 4 张表:
@TableSplit(4)
@MappedSuperclass
public class User {
private int age;
private String name;
}
编译该项目,并查看 target/generated-sources/annotations 目录,看到有如下文件:

可以看到相关的类都自动生成出来了。
再看类的内容是否符合要求(长按查看原图):
正是我们要的效果!
Annotation Processing 提供了在编译期生成额外文件的功能,这使自动化的代码生成成为可能。读完本文,说不定你会发现你们项目中也有可以用到它们的地方。试试?
– END –
原文始发于微信公众号(背井):利用 Annotation Processor 自动生成 Java 类
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/246710.html