利用 Annotation Processor 自动生成 Java 类

程序的作用之一,是将重复的工作自动化。

在上篇文章 《基于 Spring Data JPA 实现简单的分表功能》中,我们介绍了一个简单的分表方案。但有一点我不满意:当一张大表被分成 N 个子表时,需要手动创建 N实体类NRepository 类。直觉告诉我,手动干这种事有点傻!

我们知道,像项目中用到的 LombokMapStructQueryDSL 等库,它们能根据配置,帮我们自动生成一些 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();
}

它的作用是:

  1. 设置一个实体要分成几张表;
  2. 方便编译器去定位用到了该注解的实体类,以让自己的 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 $Entityextends $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 Processor 自动生成 Java 类

可以看到相关的类都自动生成出来了。

再看类的内容是否符合要求(长按查看原图):

利用 Annotation Processor 自动生成 Java 类

正是我们要的效果!


Annotation Processing 提供了在编译期生成额外文件的功能,这使自动化的代码生成成为可能。读完本文,说不定你会发现你们项目中也有可以用到它们的地方。试试?

– END –


原文始发于微信公众号(背井):利用 Annotation Processor 自动生成 Java 类

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/246710.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!