前情回顾

在上期教程中,我们首先明确了第二阶段的目标 —— 通过代码生成器制作工具快速生成 Spring Boot 项目模板代码生成器,并且介绍了示例生成的 Spring Boot 项目模板。然后我们分析得出了 7 种生成器应具备的功能,并且从中梳理除了几个制作工具应具备的通用配置能力,比如支持用一个参数同时控制多个文件的生成、定义可选开启的参数组等等。

但光有这些能力还是不够的,想更快地制作代码生成器,我们还可以从“根源”去解决问题,直接通过制作工具来生成项目模板和配置文件。

本期教程,就让我们来进一步增加制作工具的功能,实现上述目标吧!

本节重点

本节教程属于项目的第二阶段 —— 开发代码生成器制作工具。

本节主要是开发模板制作工具,这一期的教程和代码甚至可以单独拿出来作为一个 快速挖坑工具 小项目了!

重点内容:

  1. 模板制作工具 - 需求分析
  2. 模板制作工具 - 核心设计
  3. 模板制作工具 - 基础功能实现
  4. 模板制作工具 - 更多功能实现

一、需求分析

还记得么?在上期教程的最后,我们遇到了一个问题:当我们更改元信息数据模型配置,将模型参数进行分组后,我们之前已经编写的 FreeMarker 动态模板就无法正确生成内容了。这是因为使用的模型参数发生了变更,导致无法正确获得值。

通过这个问题,我们会发现,动态模板和元信息配置是有很强的绑定关系的,稍有不慎,就有可能导致代码生成异常。

此外,我们上期教程中,还遗留了一个需求无法解决 —— 替换生成的代码包名。

因为对于 Spring Boot 项目模板这种相对复杂的项目,里面用到包名的 Java 文件太多了,如果每个文件都要自己“挖坑”来制作模板,不仅成本高、也容易出现遗漏。

也就是说,虽然制作工具已经能够生成代码生成器了,但还是存在 2 大问题:

  1. 需要人工提前准备动态模板,项目文件越多,使用成本越高
  2. 需要根据动态模板编写对应的配置,参数越多,越容易出现和模板不一致的风险

如何解决这个问题呢?

答案很简单。我们可以让制作工具根据我们的想法,自动给项目文件“挖坑”,并生成相互对应的动态模板文件和元信息配置。提高效率的同时,减少模型参数和模板不一致的风险。

这就是我们本期教程需要完成的需求。

不过需要明确一点: 制作工具的作用只是提高效率,无法覆盖所有的定制需求!

因为想要如何制作代码生成器,还是取决于开发者。

二、核心设计

我们先分析下如何实现上述需求。

经常跟大家提到这么一句话:程序的本质就是帮我们完成原本人工需要进行的操作。

所以想让程序自动制作模板和生成配置,我们就要先想一想:之前我们是怎么完成这些操作的?

在使用制作工具生成前,我们依次做了以下事情:

  1. 先指定一个原始的、待“挖坑”的输入文件
  2. 明确文件中需要被动态替换的内容和模型参数
  3. 自己编写 FreeMarker FTL 模板文件
  4. 自己编写生成器的元信息配置,包括基本信息、文件配置、模型参数配置

分析上面的步骤,第 1 - 2 步都是需要用户自主确认的内容,制作工具无法插手;而有了前两步的信息后,3 - 4 步就可以用制作工具来完成。

由此,我们可以分析出快速制作模板的 基本公式

  • 向制作工具输入:基本信息 + 输入文件 + 模型参数(+ 输出规则)
  • 由制作工具输出:模板文件 + 元信息配置

跟编写算法题目一样,先明确算法的输入和输出,再去设计实现算法

对应的算法流程图如下:

img

分别解释一下上述输入参数:

1)基本信息:要制作的代码生成器的基本信息,对应元信息的名称、描述、版本号、作者等信息。

2)输入文件:要“挖坑”的原始文件。可能是一个文件、也可能是多个文件。

3)模型参数:要引导用户输入并填充到模板的模型参数,对应元信息的 modelConfig 模型配置。

4)输出规则:作为一个后续扩展功能的可选参数,比如多次制作时是否覆盖旧的配置等。

输出参数就比较好理解了,在指定目录下生成 FTL 模板文件、以及 meta.json 元信息配置文件。

明确了程序的输入输出后,下面我们就先实现一个最基础的模板制作工具,然后再陆续给工具增加功能。

小技巧:开发复杂需求或新项目时,先一切从简,完成核心流程的开发。在这个过程中可以记录想法和扩展思路,后面再按需实现。

三、基础功能实现

首先打开制作工具(maker)项目,在 maker 包下新建 template 包,所有和模板制作相关的代码都放到该包下,实现功能隔离。

目前项目中的 maker.model 目录和 FileGeneratormain 方法是多余的,可以删除。

基本流程实现

下面我们就以第一阶段准备好的 ACM 示例模板项目为例,来编写模板制作工具的基本流程代码。

预期是以 ACM 示例模板项目为根目录,使用 outputText 模型参数来替换其 src/com/yupi/acm/MainTemplate.java 文件中的 Sum: 输出信息,并在同包下生成 “挖好坑” 的 MainTemplate.java.ftl 模板文件,以及在根目录下生成 meta.json 元信息文件。

实现步骤如下:

  1. 提供输入参数:包括生成器基本信息、原始项目目录、原始文件、模型参数
  2. 基于字符串替换算法,使用模型参数的字段名称来替换原始文件的指定内容,并使用替换后的内容来创建 FTL 动态模板文件
  3. 使用输入信息来创建 meta.json 元信息文件

template 包下新建 TemplateMaker.java 文件,并依次在 main 方法中实现上述步骤。

1)输入信息

要格外注意输入文件的路径(win 系统需要对路径进行转义),代码如下:

      // 一、输入信息
// 1. 输入项目基本信息
String name = "acm-template-generator";
String description = "ACM 示例模板生成器";

// 2. 输入文件信息
String projectPath = System.getProperty("user.dir");
String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";
// 注意 win 系统需要对路径进行转义
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");

String fileInputPath = "src/com/yupi/acm/MainTemplate.java";
String fileOutputPath = fileInputPath + ".ftl";

// 3. 输入模型参数信息
Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
modelInfo.setFieldName("outputText");
modelInfo.setType("String");
modelInfo.setDefaultValue("sum = ");
    

2)使用字符串替换,生成模板文件

代码如下:

      // 二、使用字符串替换,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newFileContent = StrUtil.replace(fileContent, "Sum: ", replacement);

// 输出模板文件
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
    

上述代码中,使用 FileUtil.readUtf8String 快速读取文件内容,使用 StrUtil.replace 快速替换指定的内容,最后使用 FileUtil.writeUtf8String 将替换后的内容快速写入到文件。

3)生成配置文件

思路是先构造 Meta 对象并填充属性,再使用 Hutool 工具库的 JSONUtil.toJsonPrettyStr 方法将对象转为格式化后的 JSON 字符串,最后再写入 meta.json 文件。

代码如下:

      // 三、生成配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";

// 1. 构造配置参数
Meta meta = new Meta();
meta.setName(name);
meta.setDescription(description);

Meta.FileConfig fileConfig = new Meta.FileConfig();
meta.setFileConfig(fileConfig);
fileConfig.setSourceRootPath(sourceRootPath);
List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
fileConfig.setFiles(fileInfoList);
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
fileInfoList.add(fileInfo);

Meta.ModelConfig modelConfig = new Meta.ModelConfig();
meta.setModelConfig(modelConfig);
List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
modelConfig.setModels(modelInfoList);
modelInfoList.add(modelInfo);

// 2. 输出元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);
    

组合以上几个步骤的代码,完整代码如下:

      package com.yupi.maker.template;

import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.yupi.maker.meta.Meta;
import com.yupi.maker.meta.enums.FileGenerateTypeEnum;
import com.yupi.maker.meta.enums.FileTypeEnum;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

public class TemplateMaker {

    public static void main(String[] args) {
        // 一、输入信息
        // 1. 输入项目基本信息
        String name = "acm-template-generator";
        String description = "ACM 示例模板生成器";

        // 2. 输入文件信息
        String projectPath = System.getProperty("user.dir");
        String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";
        // 注意 win 系统需要对路径进行转义
		sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
        
        String fileInputPath = "src/com/yupi/acm/MainTemplate.java";
        String fileOutputPath = fileInputPath + ".ftl";

        // 3. 输入模型参数信息
        Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
        modelInfo.setFieldName("outputText");
        modelInfo.setType("String");
        modelInfo.setDefaultValue("sum = ");

        // 二、使用字符串替换,生成模板文件
        String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
        String fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
        String replacement = String.format("${%s}", modelInfo.getFieldName());
        String newFileContent = StrUtil.replace(fileContent, "Sum: ", replacement);

        // 输出模板文件
        String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
        FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);

        // 三、生成配置文件
        String metaOutputPath = sourceRootPath + File.separator + "meta.json";

        // 1. 构造配置参数
        Meta meta = new Meta();
        meta.setName(name);
        meta.setDescription(description);

        Meta.FileConfig fileConfig = new Meta.FileConfig();
        meta.setFileConfig(fileConfig);
        fileConfig.setSourceRootPath(sourceRootPath);
        List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
        fileConfig.setFiles(fileInfoList);
        Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
        fileInfo.setInputPath(fileInputPath);
        fileInfo.setOutputPath(fileOutputPath);
        fileInfo.setType(FileTypeEnum.FILE.getValue());
        fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
        fileInfoList.add(fileInfo);

        Meta.ModelConfig modelConfig = new Meta.ModelConfig();
        meta.setModelConfig(modelConfig);
        List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
        modelConfig.setModels(modelInfoList);
        modelInfoList.add(modelInfo);

        // 2. 输出元信息文件
        FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);
    }
}
    

运行 main 方法,测试执行,成功生成了需要的模板和元信息文件:

image.png

虽然制作模板的流程是跑通了,但我们会发现一个问题:上述代码直接在原始项目内生成了模板和元信息配置,其实是对原项目的污染。如果我们想重新生成,就得一个个删除上次生成的文件。

工作空间隔离

想解决上面的问题,其实很简单。每次制作模板时,我们都不直接修改原始项目的任何文件,而是先复制原项目到一个临时的、专门用于制作模板的目录,然后在该目录下完成文件的生成和处理。

可以将上述临时目录称为 工作空间,每次模板制作应该属于不同的工作空间,互不影响。iZC7DB8PXu/GGlQ4cVha5zDNtNCK+ikSmWAXB1nDW5I=

我们约定将 maker 项目下的 .temp 临时目录作为工作空间的根目录,并且在项目的 .gitignore 文件中忽略该目录。

TemplateMaker 原有代码的基础上新增复制目录的逻辑:

  1. 需要用户传入 originProjectPath 变量代表原始项目路径
  2. 每次制作分配一个唯一 id(使用雪花算法),作为工作空间的名称,从而实现隔离
  3. 通过 FileUtil.copy 复制目录
  4. 修改变量 sourceRootPath 的值为复制后的工作空间内的项目根目录

对应修改的代码如下:

      public static void main(String[] args) {
    // 指定原始项目路径
    String projectPath = System.getProperty("user.dir");
    String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";

    // 复制目录
    long id = IdUtil.getSnowflakeNextId();
    String tempDirPath = projectPath + File.separator + ".temp";
    String templatePath = tempDirPath + File.separator + id;
    if (!FileUtil.exist(templatePath)) {
        FileUtil.mkdir(templatePath);
    }
    FileUtil.copy(originProjectPath, templatePath, true);

    // 一、输入信息
    // 1. 输入项目基本信息
    String name = "acm-template-generator";
    String description = "ACM 示例模板生成器";

    // 2. 输入文件信息
    String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
    String fileInputPath = "src/com/yupi/acm/MainTemplate.java";
    String fileOutputPath = fileInputPath + ".ftl";

    ...
}
    

再次测试执行,可以看到 maker 项目下新建了一个工作空间,并且生成了模板和元信息配置文件,如下图:

image.png

分步制作能力

一般来说,我们在制作模板时,不可能只 “挖一个坑”,只允许用户自定义输入一个参数;也不可能一次性 “挖完所有坑”。而是一步一步地替换参数、制作模板。

所以,我们的制作工具要有分步制作、追加配置的能力,具体要做到以下 3 点:

  1. 输入过一次的信息,不用重复输入,比如基本的项目信息
  2. 后续制作时,不用再次复制原始项目;而是可以在原有文件的基础上,多次追加或覆盖新的文件
  3. 后续制作时,可以在原有配置的基础上,多次追加或覆盖配置

想要实现这个能力,我们首先要让制作工具 “有状态”。

有状态和无状态

什么是有状态?

是指程序或请求多次执行时,下一次执行保留对上一次执行的记忆。比如用户登录后服务器会记住用户的信息,下一次请求就能正常使用系统。

与之相对的是无状态。是指每次程序或请求执行,都像是第一次执行一样,没有任何历史信息。很多 Restful API 会采用无状态的设计,能够节省服务器的资源占用。

有状态实现

想实现有状态,往往需要 2 个要素:唯一标识和存储。

其实在上一步 “工作空间隔离” 中,我们已经给每个工作空间分配了一个唯一的 id 作为标识;并且将 id 作为工作空间的目录名称,相当于使用本地文件系统作为了 id 的存储。

那么我们只要在第一次制作时,生成唯一的 id;然后在后续制作时,使用相同的 id,就能找到之前的工作空间目录,从而追加文件或配置。

修改 TemplateMaker 文件,新建 makeTemplate 方法,并将 id 作为参数,先实现 id 的生成逻辑。

代码如下:

      private static long makeTemplate(Long id) {
    // 没有 id 则生成
    if (id == null) {
        id = IdUtil.getSnowflakeNextId();
    }
    
    // 业务逻辑...
    
    return id;
}
    

多次制作实现

如果根据 id 判断出并非首次制作,我们又应该做哪些调整呢?应该如何追加配置和文件呢?

这里我考虑到 3 点:

  1. 非首次制作,不需要复制原始项目文件
  2. 非首次制作,可以在已有模板的基础上再次挖坑
  3. 非首次制作,不需要重复输入已有元信息,而是在此基础上覆盖和追加元信息配置

下面分别实现这几点。

1)非首次制作,不需要复制原始项目文件

之前的 TemplateMaker 代码已经判断了某 id 对应的工作空间目录是否存在,现在只需要把复制原始项目文件的逻辑移到 “首次制作” 的 if 条件中即可。

修改后的代码如下:

      // 复制目录
long id = IdUtil.getSnowflakeNextId();
String tempDirPath = projectPath + File.separator + ".temp";
String templatePath = tempDirPath + File.separator + id;

// 是否为首次制作模板
// 目录不存在,则是首次制作
if (!FileUtil.exist(templatePath)) {
    FileUtil.mkdir(templatePath);
    FileUtil.copy(originProjectPath, templatePath, true);
}
    

2)非首次制作,可以在已有模板的基础上再次挖坑

由于制作好的模板文件名称就是在原始文件名称后增加 .ftl 后缀,所以我们可以将输出文件路径(包含 .ftl 后缀的路径)作为文件是否重复的判断条件。如果已有 .ftl 文件,表示不是第一次制作,可以在这个模板文件的基础上再去替换内容。

修改后的代码如下:

      // 二、使用字符串替换,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;

String fileContent = null;
// 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
if (FileUtil.exist(fileOutputAbsolutePath)) {
    fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
} else {
    fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
}
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newFileContent = StrUtil.replace(fileContent, "Sum: ", replacement);

// 输出模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
    

3)非首次制作,不需要重复输入已有元信息,而是在此基础上覆盖和追加元信息配置。

和判断文件模板是否重复的逻辑一致,我们可以通过是否已存在 meta.json 配置文件来判断应该新增还是修改配置。

修改后的代码如下:

我们将 fileInfo 对象的构造移到了前面,使得无论是新增还是修改元信息都能使用该对象

      // 文件配置信息
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());

// 三、生成配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";

// 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
if (FileUtil.exist(metaOutputPath)) {
    Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
    // 1. 追加配置参数
    List<Meta.FileConfig.FileInfo> fileInfoList = oldMeta.getFileConfig().getFiles();
    fileInfoList.add(fileInfo);
    List<Meta.ModelConfig.ModelInfo> modelInfoList = oldMeta.getModelConfig().getModels();
    modelInfoList.add(modelInfo);

    // 2.更新元信息文件
    FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(oldMeta), metaOutputPath);
} else {
    // 1. 构造配置参数
    Meta meta = new Meta();
    meta.setName(name);
    meta.setDescription(description);

    Meta.FileConfig fileConfig = new Meta.FileConfig();
    meta.setFileConfig(fileConfig);
    fileConfig.setSourceRootPath(sourceRootPath);
    List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
    fileConfig.setFiles(fileInfoList);
    fileInfoList.add(fileInfo);

    Meta.ModelConfig modelConfig = new Meta.ModelConfig();
    meta.setModelConfig(modelConfig);
    List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
    modelConfig.setModels(modelInfoList);
    modelInfoList.add(modelInfo);

    // 2. 输出元信息文件
    FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);
}
    

一定要注意,追加完配置后,需要去重!否则可能出现多个一模一样的模型参数或文件信息。

文件信息根据输入路径 inputPath 去重,使用新值覆盖旧值。代码如下:

      /**
 * 文件去重
 *
 * @param fileInfoList
 * @return
 */
private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
    List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(
            fileInfoList.stream()
                    .collect(
                            Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)
                    ).values()
    );
    return newFileInfoList;
}
    

上述代码中,用到了 Java 8 的 Stream API 和 Lambda 表达式来简化代码,其中 Collectors.toMap 表示将列表转换为 Map,详细解释一下:

  • 通过第一个参数(inputPath)作为 key 进行分组
  • 通过第二个参数作为 value 存储值(o -> o 表示使用原对象作为 value)
  • 最后的 (e, r) -> r 其实是 (exist, replacement) -> replacement 的缩写,表示遇到重复的值是保留新值,返回 exist 表示保留旧值。

这样一来,相同 key 对应的文件信息只会保留一个,最后再取所有的 values 拿到所有的文件信息列表即可。

模型参数根据属性名称 fieldName 去重,使用新值覆盖旧值。和文件信息去重的实现方式完全一致,代码如下:

      /**
 * 模型去重
 *
 * @param modelInfoList
 * @return
 */
private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
    List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(
            modelInfoList.stream()
                    .collect(
                            Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)
                    ).values()
    );
    return newModelInfoList;
}
    

然后修改生成配置文件的代码,使用去重方法,并将去重后的配置更新到元信息中:

      // 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
if (FileUtil.exist(metaOutputPath)) {
    Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
    // 1. 追加配置参数
    List<Meta.FileConfig.FileInfo> fileInfoList = oldMeta.getFileConfig().getFiles();
    fileInfoList.add(fileInfo);
    List<Meta.ModelConfig.ModelInfo> modelInfoList = oldMeta.getModelConfig().getModels();
    modelInfoList.add(modelInfo);

    // 配置去重
    oldMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
    oldMeta.getModelConfig().setModels(distinctModels(modelInfoList));

    // 2.更新元信息文件
    FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(oldMeta), metaOutputPath);
}
    

抽象方法

由于我们在接下来的测试中,要多次传入不同的参数执行制作,所以可以先抽象出通用的方法,将所有之前我们在 main 方法中硬编码的值都作为方法的参数。包括 originProjectPath(原始项目路径)、inputFilePath(要制作模板的输入文件相对路径)、modelInfo(模型信息)、searchStr(要替换的模板内容)等。

我们还要把所有基本信息配置直接用 Meta 类封装,可以节约方法的参数个数,比如:vZvvLYw3j8nTpc1i6/0xN5tNL9Aw+ZOh8FzCTZZoWmo=

      String name = "acm-template-generator";
String description = "ACM 示例模板生成器";
    

改为:

      Meta meta = new Meta();
meta.setName("acm-template-generator");
meta.setDescription("ACM 示例模板生成器");
    

如果非首次制作,我们还要能使用最后传入的 meta 对象更新元信息的基本配置。可以通过 BeanUtil.copyProperties 复制新对象的属性到老对象(如果属性为空则不复制),从而实现新老 meta 对象的合并。

代码如下:

      Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
BeanUtil.copyProperties(meta, oldMeta, CopyOptions.create().ignoreNullValue());
    

抽象方法后的完整代码如下:

      /**
 * 制作模板
 *
 * @param newMeta
 * @param originProjectPath
 * @param inputFilePath
 * @param modelInfo
 * @param searchStr
 * @param id
 * @return
 */
public static long makeTemplate(Meta newMeta, String originProjectPath, String inputFilePath, Meta.ModelConfig.ModelInfo modelInfo, String searchStr, Long id) {
    // 没有 id 则生成
    if (id == null) {
        id = IdUtil.getSnowflakeNextId();
    }

    // 复制目录
    String projectPath = System.getProperty("user.dir");
    String tempDirPath = projectPath + File.separator + ".temp";
    String templatePath = tempDirPath + File.separator + id;

    // 是否为首次制作模板
    // 目录不存在,则是首次制作
    if (!FileUtil.exist(templatePath)) {
        FileUtil.mkdir(templatePath);
        FileUtil.copy(originProjectPath, templatePath, true);
    }

    // 一、输入信息
    // 输入文件信息
    String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
    // 注意 win 系统需要对路径进行转义
	sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
    
    String fileInputPath = inputFilePath;
    String fileOutputPath = fileInputPath + ".ftl";

    // 二、使用字符串替换,生成模板文件
    String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
    String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;

    String fileContent = null;
    // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
    if (FileUtil.exist(fileOutputAbsolutePath)) {
        fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
    } else {
        fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
    }
    String replacement = String.format("${%s}", modelInfo.getFieldName());
    String newFileContent = StrUtil.replace(fileContent, searchStr, replacement);

    // 输出模板文件
    FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);

    // 文件配置信息
    Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
    fileInfo.setInputPath(fileInputPath);
    fileInfo.setOutputPath(fileOutputPath);
    fileInfo.setType(FileTypeEnum.FILE.getValue());
    fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());

    // 三、生成配置文件
    String metaOutputPath = sourceRootPath + File.separator + "meta.json";

    // 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
    if (FileUtil.exist(metaOutputPath)) {
        Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
        BeanUtil.copyProperties(newMeta, oldMeta, CopyOptions.create().ignoreNullValue());
        newMeta = oldMeta;

        // 1. 追加配置参数
        List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
        fileInfoList.add(fileInfo);
        List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
        modelInfoList.add(modelInfo);

        // 配置去重
        newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
        newMeta.getModelConfig().setModels(distinctModels(modelInfoList));
    } else {
        // 1. 构造配置参数
        Meta.FileConfig fileConfig = new Meta.FileConfig();
        newMeta.setFileConfig(fileConfig);
        fileConfig.setSourceRootPath(sourceRootPath);
        List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
        fileConfig.setFiles(fileInfoList);
        fileInfoList.add(fileInfo);

        Meta.ModelConfig modelConfig = new Meta.ModelConfig();
        newMeta.setModelConfig(modelConfig);
        List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
        modelConfig.setModels(modelInfoList);
        modelInfoList.add(modelInfo);
    }

    // 2. 输出元信息文件
    FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);
    return id;
}
    

测试

最后,在 main 方法中指定 2 套不同的模型参数信息作为测试数据,并调用 makeTemplate 方法。

加载中...

声明

作者: liyao

版权:本博客所有文章除特别声明外,均采用CCBY-NC-SA4.O许可协议。转载请注明!

最后更新于 2026-02-18 18:15 history