07-ftl-meta制作

直接通过工具来生成项目模板和配置文件

1. 需求分析

  • 当更改元信息数据模型配置,将模型参数进行分组后,之前已经编写的 FreeMarker 动态模板就无法正确生成内容了。这是因为使用的模型参数发生了变更,导致无法正确获得值
  • 动态模板和元信息配置是有很强的绑定关系的,稍有不慎,就有可能导致代码生成异常
  • 需求 —— 替换生成的代码包名

  • 对于 SpringBoot 项目模板这种相对复杂的项目,里面用到包名的 Java 文件太多了,如果每个文件都要手动“挖坑”来制作模板,不仅成本高、也容易出现遗漏
  • 虽然工具已经能够生成代码生成器了,但还是存在 2 大问题:
    1. 需要人工提前准备动态模板,项目文件越多,使用成本越高
    2. 需要根据动态模板编写对应的配置,参数越多,越容易出现和模板不一致的风险

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

2. 核心设计

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

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

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


分析出快速制作模板的 基本公式

  • 向工具输入:基本信息 + 输入文件 + 模型参数(+ 输出规则)
  • 由工具输出:模板文件 + 元信息配置
0bd8bb1bc13a4e0dac472abc14b1e5a0
  • 输入参数:
    • 基本信息:要制作的生成器的基本信息,对应元信息的名称、描述、版本号、作者等信息
    • 输入文件:要“挖坑”的原始文件。可能是一个文件、也可能是多个文件
    • 模型参数:要引导用户输入并填充到模板的模型参数,对应元信息的 modelConfig 模型配置
    • 输出规则:作为一个后续扩展功能的可选参数。eg:多次制作时是否覆盖旧的配置等
  • 输出参数:在指定目录下生成
    • FTL 模板文件
    • meta.json 元信息配置文件

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

3. 基础功能实现

maker 包下新建 template 包,所有和模板制作相关的代码都放到该包下,实现功能隔离

1. 基本流程实现

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

实现步骤:

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

  1. 输入信息
    • 要格外注意输入文件的路径(win 系统需要对路径进行转义)
  2. 使用字符串替换,生成模板文件
    • 使用 FileUtil.readUtf8String 快速读取文件内容,使用 StrUtil.replace 快速替换指定的内容,最后使用 FileUtil.writeUtf8String 将替换后的内容快速写入到文件
  3. 生成配置文件
    • 先构造 Meta 对象并填充属性,再使用 Hutool 工具库的 JSONUtil.toJsonPrettyStr() 将对象转为格式化后的 JSON 字符串,最后写入 meta.json 文件
package com.listao.maker.template.go;

public class Tmp1 {

    @Test
    public void test() {
        // 一、输入信息
        // 1.1. 输入项目基本信息
        String name = "01-local-generator";
        String description = "ACM 示例模板生成器";

        // 1.2. 输入文件信息
        String sourceRootPath = System.getProperty("user.dir") + "/origin/acm-template";

        // 注意 win 系统需要对路径进行转义
        sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");

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

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

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

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

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

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

        // 3.1. fileConfig
        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);

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

        // 3.3. 输出 `meta.json` 元信息文件
        FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);
        System.out.println("metaOutputPath = " + metaOutputPath);
    }

}





















 
 
 
 









 




 










 












 






 




b238e8dd409e424bbb1beb6e9fac4850

2. 工作空间隔离

  • 避免对原项目的污染
  • 约定将 maker 项目下的 .tmp 临时目录作为工作空间的根目录,并且在项目的 .gitignore 文件中忽略该目录

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

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

3. 分步制作能力

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

  • 工具要有分步制作、追加配置的能力,让制作工具 “有状态”,具体要做到以下 3 点:
    1. 输入过一次的信息,不用重复输入。eg:基本的项目信息
    2. 后续制作时,不用再次复制原始项目;而是可以在原有文件的基础上,多次追加或覆盖新的文件
    3. 后续制作时,可以在原有配置的基础上,多次追加或覆盖配置

1. 有状态和无状态

  • 有状态
    • 指程序或请求多次执行时,下一次执行保留对上一次执行的记忆
    • eg:用户登录后服务器会记住用户的信息,下一次请求就能正常使用系统
  • 无状态
    • 指每次程序或请求执行,都像是第一次执行一样,没有任何历史信息。很多 Restful API 会采用无状态的设计,能够节省服务器的资源占用

2. 有状态实现

2 个要素:唯一标识和存储

  • 在上一步 “工作空间隔离” 中,给每个工作空间分配了一个唯一的 id 作为工作空间的目录名称,相当于使用本地文件系统作为了 id 的存储
  • 那么只要在第一次制作时,生成唯一的 id;然后在后续制作时,使用相同的 id,就能找到之前的工作空间目录,从而追加文件或配置
/**
 * 制作模板
 */
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();
    }

3. 多次制作实现

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

  1. 非首次制作,不需要复制原始项目文件
    • 之前的 TemplateMaker 已经判断了某 id 对应的工作空间目录是否存在
  2. 非首次制作,可以在已有模板的基础上再次挖坑
    • 如果已有 .ftl 文件,表示不是第一次制作,可以在这个模板文件的基础上再去替换内容
  3. 非首次制作,不需要重复输入已有元信息,而是在此基础上覆盖和追加元信息配置
    • 通过是否存在 meta.json 判断是新增还是修改,将 fileInfo 对象的构造移到了前面,无论新增、修改元信息都能使用该对象

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

  • 文件信息根据输入路径 inputPath 去重,使用新值覆盖旧值
    • 用到了 Java 8 的 Stream API 和 Lambda 表达式来简化代码,其中 Collectors.toMap 表示将列表转换为 Map
      • 通过第一个参数(inputPath)作为 key 进行分组
      • 通过第二个参数作为 value 存储值(o -> o 表示使用原对象作为 value)
      • 最后的 (e, r) -> r 其实是 (exist, replacement) -> replacement 的缩写,表示遇到重复的值是保留新值,返回 exist 表示保留旧值
    • 相同 key 对应的文件信息只会保留一个,最后再取所有的 values 拿到所有的文件信息列表即可
  • 模型参数根据属性名称 fieldName 去重,使用新值覆盖旧值。和文件信息去重的实现方式完全一致

4. 抽象方法

多次传入不同的参数执行制作,可以先抽象出通用方法,将所有之前 main() 中硬编码的值都作为方法的参数

  • originProjectPath(原始项目路径)
  • inputFilePath(要制作模板的输入文件相对路径)
  • modelInfo(模型信息)
  • searchStr(要替换的模板内容)

  1. 把所有基本信息配置用 Meta 类封装,节约方法的参数个数
  2. 如果非首次制作。可以通过 BeanUtil.copyProperties 复制新对象的属性到老对象(如果属性为空则不复制)
package com.listao.maker.template.go;

public class Tmp2 {

    @Test
    public void test() {
        Meta meta = new Meta();
        meta.setName("01-local-generator");
        meta.setDescription("ACM 示例模板生成器");

        String originProjectPath = System.getProperty("user.dir") + "/origin/acm-template";
        String inputFilePath = "src/com/yupi/acm/MainTemplate.java";

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

        // 模型参数信息(第二次)
        // Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
        // modelInfo.setFieldName("className");
        // modelInfo.setType("String");

        // 替换变量(首次)
        String searchStr = "Sum: ";
        // 替换变量(第二次)
        // String searchStr = "MainTemplate";

        long id = makeTemplate(meta, originProjectPath, inputFilePath, modelInfo, searchStr, null);
        System.out.println(id);
    }

    /**
     * 制作模板
     */
    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 + ".tmp";
        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;   // .java
        String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath; // .ftl

        String fileContent;
        // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
        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;
    }

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

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






 
 
 




















 





















 


















 

 



















 

 









 
 
























 








 





第一次执行,成功制作模板并生成元信息,返回了一个新的 id

2a5bdf57226f4ee99d72f2128160c3c3

将得到的 id 作为 makeTemplate() 参数,修改传入的模型信息和替换变量

7f8af136350d44768d06b43e5b3d843e

然后再次执行,可以发现模板文件又多 “挖了一个坑”,并且元信息配置多加了一个模型参数

890bbedc9e52451cad1bb594b5c8bb94

4. 更多功能实现

1. 单次制作多个模板文件

  • 支持输入文件目录,同时处理该目录下的所有文件
  • 支持输入多个文件路径,同时处理这些文件

1. 支持输入文件目录

只要循环遍历 “单个文件制作模板” 的操作,就能轻松实现!!!

  1. 首先需要抽象出制作单个文件模板的方法 makeFileTemplate()
    • 接受单个文件、模型信息、替换文本、sourceRootPath 等参数,返回 FileInfo 文件信息
    • 由于之前的输入文件路径是相对路径,而之后要遍历文件目录下的所有文件时,传来的文件是绝对路径,将方法修改为 File 类型
    • 注意:在方法内部,要将绝对路径再转换为相对路径,以适配元信息文件的规则
  2. 如果输入的文件路径是目录,那么使用 Hutool 的 loopFiles 方法递归遍历并获取目录下的所有文件列表
    • 其中,使用 newFileInfoList 来存储所有文件的信息列表
    • 在生成配置文件时,之前使用 fileInfoList.add 添加一个文件信息对象,改为 fileInfoList.addAll 添加 newFileInfoList 文件信息列表
  3. 修改 main() 中传入的原始项目路径为 springboot-init 项目,输入文件路径改为 springbootinit 目录
    • 执行测试,目录下的所有文件都生成了模板
99cb373c8bb941b9b0fbc3dc9780fd52
  1. 优化下逻辑,如果某个文件内容没有被参数替换,那么就不生成模板,而是以静态生成的方式记录到元信息配置中
    • 修改 makeFileTemplate(),通过对比替换前后的内容是否一致来更改生成方式
    • 注意:如果是静态生成,文件输出路径(outputPath)要设置为和输入路径(inputPath)相同
  2. 更改 main() 中的 searchStrBaseResponse,然后再次执行测试
    • 效果符合预期,springbootinit 包中,包含该字符串的文件才生成了模板
    • 查看生成的元信息配置文件,生成了符合要求的静态和动态文件配置
d70eae2855b442f4a9eeeea2fe5a5f3d
3e36df7218514efdb1763b0ce54d621e

2. 支持输入多个文件

只要把 makeTemplate() 的入参 inputFilePath(单数)改为 inputFilePathList(复数),再多加一层循环处理即可

2648cca5e87842c6bb6b3f66a74e8342

3. 集成测试

package com.listao.maker.template.go;


public class Tmp3 {

    @Test
    public void test() {
        Meta meta = new Meta();
        meta.setName("springboot-generator");
        meta.setDescription("ACM 示例模板生成器");

        String originProjectPath = System.getProperty("user.dir") + "/origin/springboot-init";

        String inputFilePath1 = "src/main/java/com/yupi/springbootinit/common";
        String inputFilePath2 = "src/main/java/com/yupi/springbootinit/controller";
        List<String> inputFilePathList = Arrays.asList(inputFilePath1, inputFilePath2);

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

        // 模型参数信息(第二次)
        // Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
        // modelInfo.setFieldName("className");
        // modelInfo.setType("String");

        // 替换变量(首次)
        String searchStr = "BaseResponse";
        // 替换变量(第二次)
        // String searchStr = "BaseResponse";

        long id = makeTemplate(meta, originProjectPath, inputFilePathList, modelInfo, searchStr, null);
        System.out.println(id);
    }

    /**
     * 制作模板
     */
    public static long makeTemplate(Meta newMeta, String originProjectPath, List<String> inputFilePathList
            , 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 + ".tmp";
        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("\\\\", "/");

        // 二、生成文件模板
        // 遍历输入文件
        List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
        for (String inputFilePath : inputFilePathList) {
            String inputFileAbsolutePath = sourceRootPath + File.separator + inputFilePath;
            // 输入的是目录
            if (FileUtil.isDirectory(inputFileAbsolutePath)) {
                List<File> fileList = FileUtil.loopFiles(inputFileAbsolutePath);
                for (File file : fileList) {
                    Meta.FileConfig.FileInfo fileInfo = makeFileTemplate(modelInfo, searchStr, sourceRootPath, file);
                    newFileInfoList.add(fileInfo);
                }
            } else {
                // 输入的是文件
                Meta.FileConfig.FileInfo fileInfo = makeFileTemplate(modelInfo, searchStr, sourceRootPath,
                        new File(inputFileAbsolutePath));
                newFileInfoList.add(fileInfo);
            }
        }

        // 三、生成配置文件
        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.addAll(newFileInfoList);
            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.addAll(newFileInfoList);

            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;
    }


    /**
     * 制作文件模板
     */
    private static Meta.FileConfig.FileInfo makeFileTemplate(Meta.ModelConfig.ModelInfo modelInfo, String searchStr,
                                                             String sourceRootPath,
                                                             File inputFile) {

        // 要挖坑的文件绝对路径(用于制作模板)
        // 注意 win 系统需要对路径进行转义
        String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
        String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";

        // 文件输入输出相对路径(用于生成配置)
        String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
        String fileOutputPath = fileInputPath + ".ftl";

        // 使用字符串替换,生成模板文件
        String fileContent;
        // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
        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);

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

        // 和原文件一致,没有挖坑,则为静态生成
        if (newFileContent.equals(fileContent)) {
            // 输出路径 = 输入路径
            fileInfo.setOutputPath(fileInputPath);
            fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
        } else {
            // 生成模板文件
            fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
            FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
        }
        return fileInfo;
    }

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

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

}













 
 
 

















 




























 






 


 


 




 















 













 



















 
 













 





 



 
 



 






























2. 文件过滤

  • 需求:控制是否生成帖子相关功能
  • 实现思路:允许用户输入一个开关参数来控制帖子功能相关的文件是否生成(PostController、PostService、PostMapper、PostMapper.xml、Post 实体类等)
  • 通用能力:某个范围下的多个指定文件挖坑 => 绑定同个参数

给工具增加 文件过滤 功能,通过多种不同的过滤方式帮助用户选择文件,更灵活地完成批量模板制作

1. 文件过滤机制设计

  • 过滤范围:根据文件名称、或文件内容过滤
  • 过滤规则:包含 contains、前缀匹配 startsWith、后缀匹配 endsWith、正则 regex、相等 equals

由于工具已经支持输入多个文件 / 目录,所以其实每个文件 / 目录都可以指定自己的过滤规则,而且能同时指定多条过滤规则(必须同时满足才保留),进一步提高灵活性。参考文件过滤机制的 JSON 结构如下:

{
    "files": [
        {
            "path": "文件(目录)路径",
            "filters": [
                {
                    "range": "fileName",
                    "rule": "regex",
                    "value": ".*lala.*"
                },
                {
                    "range": "fileContent",
                    "rule": "contains",
                    "value": "haha"
                }
            ]
        }
    ],
}

通过这种设计,可以非常灵活地筛选文件。如果想使用 or 逻辑(有一个过滤条件符合要求就保留),可以定义多个重复的 file,并且每个 file 指定一个过滤条件来实现。哪怕同时满足了多个过滤器,去重逻辑也能搞定

2. 开发实现

1. 配置类

template.model 包下新建 FileFilterConfig 类,对应上面设计好的 JSON 结构

package com.listao.maker.template.model;

/**
 * 文件过滤配置
 */
@Data
@Builder
public class FileFilterConfig {

    /**
     * 过滤范围
     */
    private String range;

    /**
     * 过滤规则
     */
    private String rule;

    /**
     * 过滤值
     */
    private String value;

}
package com.listao.maker.template.model;

@Data
public class TemplateMakerFileConfig {

    private List<FileInfoConfig> files;

    @NoArgsConstructor
    @Data
    public static class FileInfoConfig {

        private String path;

        private List<FileFilterConfig> filterConfigList;
    }
}
2. 枚举类

针对过滤配置中的枚举值,编写对应的枚举类

package com.listao.maker.template.enums;

/**
 * 文件过滤范围枚举
 */
@Getter
public enum FileFilterRangeEnum {

    FILE_NAME("文件名称", "fileName"),
    FILE_CONTENT("文件内容", "fileContent");

    private final String text;

    private final String value;

    FileFilterRangeEnum(String text, String value) {
        this.text = text;
        this.value = value;
    }

    /**
     * 根据 value 获取枚举
     *
     * @param value
     * @return
     */
    public static FileFilterRangeEnum getEnumByValue(String value) {
        if (ObjectUtil.isEmpty(value)) {
            return null;
        }
        for (FileFilterRangeEnum anEnum : FileFilterRangeEnum.values()) {
            if (anEnum.value.equals(value)) {
                return anEnum;
            }
        }
        return null;
    }
}
package com.listao.maker.template.enums;

/**
 * 文件过滤规则枚举
 */
@Getter
public enum FileFilterRuleEnum {

    CONTAINS("包含", "contains"),
    STARTS_WITH("前缀匹配", "startsWith"),
    ENDS_WITH("后缀匹配", "endsWith"),
    REGEX("正则", "regex"),
    EQUALS("相等", "equals");

    private final String text;

    private final String value;

    FileFilterRuleEnum(String text, String value) {
        this.text = text;
        this.value = value;
    }

    /**
     * 根据 value 获取枚举
     *
     * @param value
     * @return
     */
    public static FileFilterRuleEnum getEnumByValue(String value) {
        if (ObjectUtil.isEmpty(value)) {
            return null;
        }
        for (FileFilterRuleEnum anEnum : FileFilterRuleEnum.values()) {
            if (anEnum.value.equals(value)) {
                return anEnum;
            }
        }
        return null;
    }
}
3. FileFilter
  • 首先开发针对单个文件过滤的方法 doSingleFileFilter。实现思路是遍历传入的文件过滤配置列表,并按照规则进行校验,如果有一个过滤配置不满足,就返回 false 表示不保留该文件,反之为 true 表示通过所有校验
  • 然后编写过滤器的主方法 doFilter,方法接受 filePath 文件路径参数,支持传入文件或目录,能够同时对多个文件进行过滤
package com.listao.maker.template;

/**
 * 文件过滤器
 */
public class FileFilter {

    /**
     * 对某个文件或目录进行过滤,返回文件列表
     */
    public static List<File> doFilter(String filePath, List<FileFilterConfig> fileFilterConfigList) {
        // 根据路径获取所有文件
        List<File> fileList = FileUtil.loopFiles(filePath);
        return fileList.stream().filter(file -> doSingleFileFilter(fileFilterConfigList, file)).collect(Collectors.toList());
    }

    /**
     * 单个文件过滤
     *
     * @param fileFilterConfigList 过滤规则
     * @param file                 单个文件
     * @return 是否保留
     */
    public static boolean doSingleFileFilter(List<FileFilterConfig> fileFilterConfigList, File file) {
        String fileName = file.getName();
        String fileContent = FileUtil.readUtf8String(file);

        // 所有过滤器校验结束的结果
        boolean result = true;

        if (CollUtil.isEmpty(fileFilterConfigList)) {
            return true;
        }

        for (FileFilterConfig fileFilterConfig : fileFilterConfigList) {
            String range = fileFilterConfig.getRange();
            String rule = fileFilterConfig.getRule();
            String value = fileFilterConfig.getValue();

            FileFilterRangeEnum fileFilterRangeEnum = FileFilterRangeEnum.getEnumByValue(range);
            if (fileFilterRangeEnum == null) {
                continue;
            }

            // 要过滤的原内容
            String content = fileName;
            switch (fileFilterRangeEnum) {
                case FILE_NAME:
                    content = fileName;
                    break;
                case FILE_CONTENT:
                    content = fileContent;
                    break;
                default:
            }

            FileFilterRuleEnum filterRuleEnum = FileFilterRuleEnum.getEnumByValue(rule);
            if (filterRuleEnum == null) {
                continue;
            }
            switch (filterRuleEnum) {
                case CONTAINS:
                    result = content.contains(value);
                    break;
                case STARTS_WITH:
                    result = content.startsWith(value);
                    break;
                case ENDS_WITH:
                    result = content.endsWith(value);
                    break;
                case REGEX:
                    result = content.matches(value);
                    break;
                case EQUALS:
                    result = content.equals(value);
                    break;
                default:
            }

            // 有一个不满足,就直接返回
            if (!result) {
                return false;
            }
        }

        // 都满足
        return true;
    }

}













 















































 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 













3. 集成测试

  1. 模板制作工具类使用过滤器
    • makeTemplate() 接受的 inputFilePathList 参数改为新封装的 TemplateMakerFileConfig,相当于同时传入了文件列表和过滤规则
    • 然后修改遍历输入文件的代码,改为遍历 fileConfigInfoList 获取文件信息
    • 应用过滤器。将文件信息配置中的 相对路径转化为绝对路径 作为调用过滤器的参数,并通过过滤器获取到所有文件列表(注意,这里不可能是目录),再遍历文件列表来制作模板
  2. main() 中编写文件过滤测试代码,只处理 common 包下文件名称包含 Base 的文件和 controller 包下的文件
    • 执行测试,只有 BaseResponse.java 生成了模板文件,符合预期
7a42e7f940164afd8bab4c96377cd431

生成的元信息配置也没有多余的文件:

4cdb228a23c649269e9b22325d0548c7
package com.listao.maker.template.go;

public class Tmp4 {

    @Test
    public void test() {
        Meta meta = new Meta();
        meta.setName("springboot-generator");
        meta.setDescription("ACM 示例模板生成器");

        String originProjectPath = System.getProperty("user.dir") + "/origin/springboot-init";

        String inputFilePath1 = "src/main/java/com/yupi/springbootinit/common";
        String inputFilePath2 = "src/main/java/com/yupi/springbootinit/controller";

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

        // 模型参数信息(第二次)
        // Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
        // modelInfo.setFieldName("className");
        // modelInfo.setType("String");

        // 替换变量(首次)
        String searchStr = "BaseResponse";
        // 替换变量(第二次)
        // String searchStr = "BaseResponse";

        // 文件过滤
        FileFilterConfig fileFilterConfig = FileFilterConfig.builder()
                .range(FileFilterRangeEnum.FILE_NAME.getValue())
                .rule(FileFilterRuleEnum.CONTAINS.getValue())
                .value("Base").build();

        List<FileFilterConfig> fileFilterConfigList = new ArrayList<>();
        fileFilterConfigList.add(fileFilterConfig);

        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig1 = new TemplateMakerFileConfig.FileInfoConfig();
        fileInfoConfig1.setPath(inputFilePath1);
        fileInfoConfig1.setFilterConfigList(fileFilterConfigList);

        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig2 = new TemplateMakerFileConfig.FileInfoConfig();
        fileInfoConfig2.setPath(inputFilePath2);

        TemplateMakerFileConfig templateMakerFileConfig = new TemplateMakerFileConfig();
        templateMakerFileConfig.setFiles(Arrays.asList(fileInfoConfig1, fileInfoConfig2));

        long id = makeTemplate(meta, originProjectPath, templateMakerFileConfig, modelInfo, searchStr, null);
        System.out.println(id);
    }

    /**
     * 制作模板
     */
    public static long makeTemplate(Meta newMeta, String originProjectPath,
                                    TemplateMakerFileConfig templateMakerFileConfig,
                                    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("\\\\", "/");
        List<TemplateMakerFileConfig.FileInfoConfig> fileConfigInfoList = templateMakerFileConfig.getFiles();

        // 二、生成文件模板
        // 遍历输入文件
        List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
        for (TemplateMakerFileConfig.FileInfoConfig fileInfoConfig : fileConfigInfoList) {
            String inputFilePath = fileInfoConfig.getPath();

            // 如果填的是相对路径,要改为绝对路径
            if (!inputFilePath.startsWith(sourceRootPath)) {
                inputFilePath = sourceRootPath + File.separator + inputFilePath;
            }

            // 获取过滤后的文件列表(不会存在目录)
            List<File> fileList = FileFilter.doFilter(inputFilePath, fileInfoConfig.getFilterConfigList());
            for (File file : fileList) {
                Meta.FileConfig.FileInfo fileInfo = makeFileTemplate(modelInfo, searchStr, sourceRootPath, file);
                newFileInfoList.add(fileInfo);
            }
        }

        // 三、生成配置文件
        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.addAll(newFileInfoList);
            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.addAll(newFileInfoList);

            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;
    }

    /**
     * 制作文件模板
     */
    private static Meta.FileConfig.FileInfo makeFileTemplate(Meta.ModelConfig.ModelInfo modelInfo, String searchStr
            , String sourceRootPath, File inputFile) {

        // 要挖坑的文件绝对路径(用于制作模板)
        // 注意 win 系统需要对路径进行转义
        String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
        String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";

        // 文件输入输出相对路径(用于生成配置)
        String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
        String fileOutputPath = fileInputPath + ".ftl";

        // 使用字符串替换,生成模板文件
        String fileContent;
        // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
        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);

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

        // 和原文件一致,没有挖坑,则为静态生成
        if (newFileContent.equals(fileContent)) {
            // 输出路径 = 输入路径
            fileInfo.setOutputPath(fileInputPath);
            fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
        } else {
            // 生成模板文件
            fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
            FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
        }
        return fileInfo;
    }

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

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

}
































 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 









 
























 




 
 







 













































































































3. 文件分组

  • 目前,生成器已经支持对文件进行分组,并且通过给组设置 condition 的方式,支持用单个模型参数同时控制一组文件
  • 工具也需要拥有快速生成文件组配置的能力

1. 实现思路

  • 现在已经能够单次制作多个文件了,而且根据用户习惯,同一次制作的多个文件更有可能属于同一组。那么其实不用让用户再手动配置如何分组了,可以自动分组

有 2 种分组策略:

  1. 一个文件信息配置(FileInfoConfig)对应一次分组。如果传入的 path 是目录,则目录下的所有文件为同组
  2. 一个完整的文件配置(TemplateMakerFileConfig)对应一次分组。即配置 files 列表中的所有文件都属于同组

选择第 2 种方案。从需求出发,对于 “要用一个参数控制帖子相关的文件是否生成” 需求,有可能要把跨目录下的文件设置为一个组

2. 开发实现

  1. 首先给 TemplateMakerFileConfig 增加分组配置,和之前 Meta 元信息实体类的分组字段一一对应
package com.listao.maker.template.model;

@Data
public class TemplateMakerFileConfig {

    private List<FileInfoConfig> files;

    private FileGroupConfig fileGroupConfig;

    @NoArgsConstructor
    @Data
    public static class FileInfoConfig {

        private String path;

        private List<FileFilterConfig> filterConfigList;
    }

    @Data
    public static class FileGroupConfig {

        private String condition;

        private String groupKey;

        private String groupName;
    }
}







 











 








  1. TemplateMakermakeTemplate() 中增加文件组相关代码,思路是将本次得到的所有文件信息都放到同一个分组下
    • 在 “生成配置文件” 前增加代码
c286ac19ffb045208b04a082cc7f46f3

3. 追加配置能力

  • 已经实现了文件分组配置的生成,但实际制作模板的过程中,可能没办法一步到位,而是希望能多次制作模板
  • 文件分组要能支持多次制作追加配置的能力,可以增加新的分组、也可以在同分组下新增文件
  • 两个分组的 groupKey 相同,视为同一个组,需要将第 2 次得到的分组内的所有文件和之前分组内的文件进行合并去重

文件分组去重 distinctFiles() 的实现流程,步骤:

  1. 将所有文件配置(fileInfo)分为有分组的和无分组的
  2. 对于有分组的文件配置,如果有相同的分组,同分组内的文件进行合并(merge),不同分组可同时保留
  3. 创建新的文件配置列表(结果列表),先将合并后的分组添加到结果列表
  4. 再将无分组的文件配置列表添加到结果列表

  • 修改 main() 中的 inputFilePath2,指定一个新的目录
String inputFilePath2 = "src/main/java/com/yupi/springbootinit/constant";
  • 基于之前制作好的模板再次执行,发现第 2 次新增的文件合并到了之前的分组配置中,符合预期
1f8292dffc2144e9943bdbfb094bccbe
  • 修改分组的名称、条件等配置,新的配置会覆盖之前的配置

4. 完整代码

package com.listao.maker.template.go;

public class Tmp5 {

    @Test
    public void test() {
        Meta meta = new Meta();
        meta.setName("springboot-generator");
        meta.setDescription("ACM 示例模板生成器");

        String originProjectPath = System.getProperty("user.dir") + "/origin/springboot-init";
        String inputFilePath1 = "src/main/java/com/yupi/springbootinit/common";
        String inputFilePath2 = "src/main/java/com/yupi/springbootinit/constant";

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

        // 模型参数信息(第二次)
        // Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
        // modelInfo.setFieldName("className");
        // modelInfo.setType("String");

        // 替换变量(首次)
        String searchStr = "BaseResponse";
        // 替换变量(第二次)
        // String searchStr = "BaseResponse";

        // 文件过滤
        FileFilterConfig fileFilterConfig = FileFilterConfig.builder()
                .range(FileFilterRangeEnum.FILE_NAME.getValue())
                .rule(FileFilterRuleEnum.CONTAINS.getValue())
                .value("Base")
                .build();

        List<FileFilterConfig> fileFilterConfigList = new ArrayList<>();
        fileFilterConfigList.add(fileFilterConfig);

        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig1 = new TemplateMakerFileConfig.FileInfoConfig();
        fileInfoConfig1.setPath(inputFilePath1);
        fileInfoConfig1.setFilterConfigList(fileFilterConfigList);

        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig2 = new TemplateMakerFileConfig.FileInfoConfig();
        fileInfoConfig2.setPath(inputFilePath2);

        // 分组配置
        TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = new TemplateMakerFileConfig.FileGroupConfig();
        fileGroupConfig.setCondition("outputText");
        fileGroupConfig.setGroupKey("test");
        fileGroupConfig.setGroupName("测试分组");

        TemplateMakerFileConfig templateMakerFileConfig = new TemplateMakerFileConfig();
        templateMakerFileConfig.setFiles(Arrays.asList(fileInfoConfig1, fileInfoConfig2));
        templateMakerFileConfig.setFileGroupConfig(fileGroupConfig);

        long id = makeTemplate(meta, originProjectPath, templateMakerFileConfig, modelInfo, searchStr, null);
        System.out.println(id);
    }

    /**
     * 制作模板
     */
    public static long makeTemplate(Meta newMeta, String originProjectPath, TemplateMakerFileConfig templateMakerFileConfig
            , 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("\\\\", "/");
        List<TemplateMakerFileConfig.FileInfoConfig> fileConfigInfoList = templateMakerFileConfig.getFiles();

        // 二、生成文件模板
        // 遍历输入文件
        List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
        for (TemplateMakerFileConfig.FileInfoConfig fileInfoConfig : fileConfigInfoList) {
            String inputFilePath = fileInfoConfig.getPath();

            // 如果填的是相对路径,要改为绝对路径
            if (!inputFilePath.startsWith(sourceRootPath)) {
                inputFilePath = sourceRootPath + File.separator + inputFilePath;
            }

            // 获取过滤后的文件列表(不会存在目录)
            List<File> fileList = FileFilter.doFilter(inputFilePath, fileInfoConfig.getFilterConfigList());
            for (File file : fileList) {
                Meta.FileConfig.FileInfo fileInfo = makeFileTemplate(modelInfo, searchStr, sourceRootPath, file);
                newFileInfoList.add(fileInfo);
            }
        }

        // 如果是文件组
        TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = templateMakerFileConfig.getFileGroupConfig();
        if (fileGroupConfig != null) {
            String condition = fileGroupConfig.getCondition();
            String groupKey = fileGroupConfig.getGroupKey();
            String groupName = fileGroupConfig.getGroupName();

            // 新增分组配置
            Meta.FileConfig.FileInfo groupFileInfo = new Meta.FileConfig.FileInfo();
            groupFileInfo.setType(FileTypeEnum.GROUP.getValue());
            groupFileInfo.setCondition(condition);
            groupFileInfo.setGroupKey(groupKey);
            groupFileInfo.setGroupName(groupName);
            // 文件全放到一个分组内
            groupFileInfo.setFiles(newFileInfoList);
            newFileInfoList = new ArrayList<>();
            newFileInfoList.add(groupFileInfo);
        }

        // 三、生成配置文件
        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.addAll(newFileInfoList);
            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.addAll(newFileInfoList);

            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;
    }

    /**
     * 制作文件模板
     */
    private static Meta.FileConfig.FileInfo makeFileTemplate(Meta.ModelConfig.ModelInfo modelInfo, String searchStr
            , String sourceRootPath, File inputFile) {

        // 要挖坑的文件绝对路径(用于制作模板)
        // 注意 win 系统需要对路径进行转义
        String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
        String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";

        // 文件输入输出相对路径(用于生成配置)
        String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
        String fileOutputPath = fileInputPath + ".ftl";

        // 使用字符串替换,生成模板文件
        String fileContent;
        // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
        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);

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

        // 和原文件一致,没有挖坑,则为静态生成
        if (newFileContent.equals(fileContent)) {
            // 输出路径 = 输入路径
            fileInfo.setOutputPath(fileInputPath);
            fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
        } else {
            // 生成模板文件
            fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
            FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
        }
        return fileInfo;
    }

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

    /**
     * 文件去重
     */
    private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
        // 策略:同分组内文件 merge,不同分组保留

        // 1. 有分组的,以组为单位划分
        Map<String, List<Meta.FileConfig.FileInfo>> groupKeyFileInfoListMap = fileInfoList.stream()
                .filter(fileInfo -> StrUtil.isNotBlank(fileInfo.getGroupKey()))
                .collect(
                        Collectors.groupingBy(Meta.FileConfig.FileInfo::getGroupKey)
                );

        // 2. 同组内的文件配置合并
        // 保存每个组对应的合并后的对象 map
        Map<String, Meta.FileConfig.FileInfo> groupKeyMergedFileInfoMap = new HashMap<>();
        for (Map.Entry<String, List<Meta.FileConfig.FileInfo>> entry : groupKeyFileInfoListMap.entrySet()) {
            List<Meta.FileConfig.FileInfo> tempFileInfoList = entry.getValue();
            List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(tempFileInfoList.stream()
                    .flatMap(fileInfo -> fileInfo.getFiles().stream())
                    .collect(
                            Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)
                    ).values());

            // 使用新的 group 配置
            Meta.FileConfig.FileInfo newFileInfo = CollUtil.getLast(tempFileInfoList);
            newFileInfo.setFiles(newFileInfoList);
            String groupKey = entry.getKey();
            groupKeyMergedFileInfoMap.put(groupKey, newFileInfo);
        }

        // 3. 将文件分组添加到结果列表
        List<Meta.FileConfig.FileInfo> resultList = new ArrayList<>(groupKeyMergedFileInfoMap.values());

        // 4. 将未分组的文件添加到结果列表
        List<Meta.FileConfig.FileInfo> noGroupFileInfoList = fileInfoList.stream()
                .filter(fileInfo -> StrUtil.isBlank(fileInfo.getGroupKey()))
                .collect(Collectors.toList());

        resultList.addAll(new ArrayList<>(noGroupFileInfoList.stream()
                .collect(
                        Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)
                ).values()));
        return resultList;
    }

}
















































 
 
 
 
 
 
 
 








 














































 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 












 




 








 






































































 











































4. 模型分组

生成器已经实现了模型分组的能力。工具也需要能够同时指定多个模型参数进行 “挖坑”,并生成模型分组配置

1. 实现思路

  • 注意:之前在测试工具时,传入的都是单个模型参数和要替换的字符串参数(searchStr)。但现在如果要一次性输入多个模型参数,也要传入多个要替换的字符串。准确地说,每个模型和要替换的字符串参数应该一一对应。所以需要用额外的类来封装这些参数

2. 开发实现

  1. 像文件配置类(TemplateMakerFileConfig)一样,封装所有模型参数、分组参数为模型配置类 TemplateMakerModelConfig
package com.listao.maker.template.model;

@Data
public class TemplateMakerModelConfig {

    private List<ModelInfoConfig> models;

    private ModelGroupConfig modelGroupConfig;

    @NoArgsConstructor
    @Data
    public static class ModelInfoConfig {

        private String fieldName;

        private String type;

        private String description;

        private Object defaultValue;

        private String abbr;

        // 用于替换哪些文本
        private String replaceText;
    }

    @Data
    public static class ModelGroupConfig {

        private String condition;

        private String groupKey;

        private String groupName;
    }
}













 










 












  1. 修改模型去重方法,和文件去重逻辑一致,实现同组模型信息的去重合并
  2. 修改 makeTemplate() 入参,使用封装好的模型配置类 TemplateMakerModelConfig 代替 modelInfosearchStr
    • 从模型配置中读出分组和模型列表信息,并转换为用于生成元信息配置的 newModelInfoList
    • 之前生成元信息配置,是增加了单个 ModelInfo 对象,现在需要改为增加 newModelInfoList 列表
  3. 修改 makeFileTemplate(),要能够支持使用多个模型参数对文件 “挖坑”
    • 实现思路是依次遍历模型参数,对文件内容进行替换,将上一轮替换后的结果作为新一轮要替换的内容,从而实现多轮替换
  4. 测试验证,定义一组能够替换 MySQL 配置的模型组参数,用来替换 application.yml 配置文件
    • 执行,成功制作出了有多个参数的模板文件:
2d8c2f2720c3401d90e7eed98cfcb1db
10a23ce1542c4a8ab2519d8ce28d2e87
  • 可以尝试更换模型参数组的 groupKey 或模型的 fieldName,测试能够正常追加模型配置

3. 完整代码

package com.listao.maker.template.go;


public class Tmp6 {

    @Test
    public void test() {
        Meta meta = new Meta();
        meta.setName("01-local-generator");
        meta.setDescription("ACM 示例模板生成器");

        String originProjectPath = System.getProperty("user.dir") + "/origin/springboot-init";
        String inputFilePath1 = "src/main/java/com/yupi/springbootinit/common";
        String inputFilePath2 = "src/main/resources/application.yml";

        // 模型参数配置
        TemplateMakerModelConfig templateMakerModelConfig = new TemplateMakerModelConfig();

        // - 模型组配置
        TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = new TemplateMakerModelConfig.ModelGroupConfig();
        modelGroupConfig.setGroupKey("mysql");
        modelGroupConfig.setGroupName("数据库配置");
        templateMakerModelConfig.setModelGroupConfig(modelGroupConfig);

        // - 模型配置
        TemplateMakerModelConfig.ModelInfoConfig modelInfoConfig1 = new TemplateMakerModelConfig.ModelInfoConfig();
        modelInfoConfig1.setFieldName("url");
        modelInfoConfig1.setType("String");
        modelInfoConfig1.setDefaultValue("jdbc:mysql://localhost:3306/my_db");
        modelInfoConfig1.setReplaceText("jdbc:mysql://localhost:3306/my_db");

        TemplateMakerModelConfig.ModelInfoConfig modelInfoConfig2 = new TemplateMakerModelConfig.ModelInfoConfig();
        modelInfoConfig2.setFieldName("username");
        modelInfoConfig2.setType("String");
        modelInfoConfig2.setDefaultValue("root");
        modelInfoConfig2.setReplaceText("root");

        List<TemplateMakerModelConfig.ModelInfoConfig> modelInfoConfigList = Arrays.asList(modelInfoConfig1, modelInfoConfig2);
        templateMakerModelConfig.setModels(modelInfoConfigList);

        // 文件过滤
        TemplateMakerFileConfig templateMakerFileConfig = new TemplateMakerFileConfig();
        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig1 = new TemplateMakerFileConfig.FileInfoConfig();
        fileInfoConfig1.setPath(inputFilePath1);
        List<FileFilterConfig> fileFilterConfigList = new ArrayList<>();

        FileFilterConfig fileFilterConfig = FileFilterConfig.builder()
                .range(FileFilterRangeEnum.FILE_NAME.getValue())
                .rule(FileFilterRuleEnum.CONTAINS.getValue())
                .value("Base")
                .build();

        fileFilterConfigList.add(fileFilterConfig);
        fileInfoConfig1.setFilterConfigList(fileFilterConfigList);

        TemplateMakerFileConfig.FileInfoConfig fileInfoConfig2 = new TemplateMakerFileConfig.FileInfoConfig();
        fileInfoConfig2.setPath(inputFilePath2);
        templateMakerFileConfig.setFiles(Arrays.asList(fileInfoConfig1, fileInfoConfig2));

        // 分组配置
        TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = new TemplateMakerFileConfig.FileGroupConfig();
        fileGroupConfig.setCondition("outputText");
        fileGroupConfig.setGroupKey("test");
        fileGroupConfig.setGroupName("测试分组");
        templateMakerFileConfig.setFileGroupConfig(fileGroupConfig);

        long id = makeTemplate(meta, originProjectPath, templateMakerFileConfig, templateMakerModelConfig, 1854123915333320704L);
        System.out.println(id);
    }

    /**
     * 制作模板
     */
    public static long makeTemplate(Meta newMeta, String originProjectPath,
                                    TemplateMakerFileConfig templateMakerFileConfig,
                                    TemplateMakerModelConfig templateMakerModelConfig,
                                    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("\\\\", "/");
        List<TemplateMakerFileConfig.FileInfoConfig> fileConfigInfoList = templateMakerFileConfig.getFiles();

        // 二、生成文件模板
        // 遍历输入文件
        List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
        for (TemplateMakerFileConfig.FileInfoConfig fileInfoConfig : fileConfigInfoList) {
            String inputFilePath = fileInfoConfig.getPath();

            // 如果填的是相对路径,要改为绝对路径
            if (!inputFilePath.startsWith(sourceRootPath)) {
                inputFilePath = sourceRootPath + File.separator + inputFilePath;
            }

            // 获取过滤后的文件列表(不会存在目录)
            List<File> fileList = FileFilter.doFilter(inputFilePath, fileInfoConfig.getFilterConfigList());
            for (File file : fileList) {
                Meta.FileConfig.FileInfo fileInfo = makeFileTemplate(templateMakerModelConfig, sourceRootPath, file);
                newFileInfoList.add(fileInfo);
            }
        }

        // 如果是文件组
        TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = templateMakerFileConfig.getFileGroupConfig();
        if (fileGroupConfig != null) {
            String condition = fileGroupConfig.getCondition();
            String groupKey = fileGroupConfig.getGroupKey();
            String groupName = fileGroupConfig.getGroupName();

            // 新增分组配置
            Meta.FileConfig.FileInfo groupFileInfo = new Meta.FileConfig.FileInfo();
            groupFileInfo.setType(FileTypeEnum.GROUP.getValue());
            groupFileInfo.setCondition(condition);
            groupFileInfo.setGroupKey(groupKey);
            groupFileInfo.setGroupName(groupName);
            // 文件全放到一个分组内
            groupFileInfo.setFiles(newFileInfoList);
            newFileInfoList = new ArrayList<>();
            newFileInfoList.add(groupFileInfo);
        }

        // 处理模型信息
        List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();
        // - 转换为配置接受的 ModelInfo 对象
        List<Meta.ModelConfig.ModelInfo> inputModelInfoList = models.stream().map(modelInfoConfig -> {
            Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
            BeanUtil.copyProperties(modelInfoConfig, modelInfo);
            return modelInfo;
        }).collect(Collectors.toList());

        // - 本次新增的模型配置列表
        List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>();

        // - 如果是模型组
        TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();
        if (modelGroupConfig != null) {
            String condition = modelGroupConfig.getCondition();
            String groupKey = modelGroupConfig.getGroupKey();
            String groupName = modelGroupConfig.getGroupName();
            Meta.ModelConfig.ModelInfo groupModelInfo = new Meta.ModelConfig.ModelInfo();
            groupModelInfo.setGroupKey(groupKey);
            groupModelInfo.setGroupName(groupName);
            groupModelInfo.setCondition(condition);

            // 模型全放到一个分组内
            groupModelInfo.setModels(inputModelInfoList);
            newModelInfoList.add(groupModelInfo);
        } else {
            // 不分组,添加所有的模型信息到列表
            newModelInfoList.addAll(inputModelInfoList);
        }

        // 三、生成配置文件
        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.addAll(newFileInfoList);
            List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
            modelInfoList.addAll(newModelInfoList);

            // 配置去重
            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.addAll(newFileInfoList);

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

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

    /**
     * 制作文件模板
     */
    private static Meta.FileConfig.FileInfo makeFileTemplate(
            TemplateMakerModelConfig templateMakerModelConfig,
            String sourceRootPath, File inputFile) {

        // 要挖坑的文件绝对路径(用于制作模板)
        // 注意 win 系统需要对路径进行转义
        String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
        String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";

        // 文件输入输出相对路径(用于生成配置)
        String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
        String fileOutputPath = fileInputPath + ".ftl";

        // 使用字符串替换,生成模板文件
        String fileContent;
        // 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
        if (FileUtil.exist(fileOutputAbsolutePath)) {
            fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
        } else {
            fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
        }

        // 支持多个模型:对同一个文件的内容,遍历模型进行多轮替换
        TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();
        String newFileContent = fileContent;
        String replacement;
        for (TemplateMakerModelConfig.ModelInfoConfig modelInfoConfig : templateMakerModelConfig.getModels()) {
            // 不是分组
            if (modelGroupConfig == null) {
                replacement = String.format("${%s}", modelInfoConfig.getFieldName());
            } else {
                // 是分组
                String groupKey = modelGroupConfig.getGroupKey();
                // 注意挖坑要多一个层级
                replacement = String.format("${%s.%s}", groupKey, modelInfoConfig.getFieldName());
            }
            // 多次替换
            newFileContent = StrUtil.replace(newFileContent, modelInfoConfig.getReplaceText(), replacement);
        }

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

        // 和原文件一致,没有挖坑,则为静态生成
        if (newFileContent.equals(fileContent)) {
            // 输出路径 = 输入路径
            fileInfo.setOutputPath(fileInputPath);
            fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
        } else {
            // 生成模板文件
            fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
            FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
        }
        return fileInfo;
    }

    /**
     * 模型分组去重
     */
    private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
        // 策略:同分组内模型 merge,不同分组保留

        // 1. 有分组的,以组为单位划分
        Map<String, List<Meta.ModelConfig.ModelInfo>> groupKeyModelInfoListMap = modelInfoList
                .stream()
                .filter(modelInfo -> StrUtil.isNotBlank(modelInfo.getGroupKey()))
                .collect(
                        Collectors.groupingBy(Meta.ModelConfig.ModelInfo::getGroupKey)
                );


        // 2. 同组内的模型配置合并
        // 保存每个组对应的合并后的对象 map
        Map<String, Meta.ModelConfig.ModelInfo> groupKeyMergedModelInfoMap = new HashMap<>();
        for (Map.Entry<String, List<Meta.ModelConfig.ModelInfo>> entry : groupKeyModelInfoListMap.entrySet()) {
            List<Meta.ModelConfig.ModelInfo> tempModelInfoList = entry.getValue();
            List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(tempModelInfoList.stream()
                    .flatMap(modelInfo -> modelInfo.getModels().stream())
                    .collect(
                            Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)
                    ).values());

            // 使用新的 group 配置
            Meta.ModelConfig.ModelInfo newModelInfo = CollUtil.getLast(tempModelInfoList);
            newModelInfo.setModels(newModelInfoList);
            String groupKey = entry.getKey();
            groupKeyMergedModelInfoMap.put(groupKey, newModelInfo);
        }

        // 3. 将模型分组添加到结果列表
        List<Meta.ModelConfig.ModelInfo> resultList = new ArrayList<>(groupKeyMergedModelInfoMap.values());

        // 4. 将未分组的模型添加到结果列表
        List<Meta.ModelConfig.ModelInfo> noGroupModelInfoList = modelInfoList.stream()
                .filter(modelInfo -> StrUtil.isBlank(modelInfo.getGroupKey()))
                .collect(Collectors.toList());
        resultList.addAll(new ArrayList<>(noGroupModelInfoList.stream()
                .collect(
                        Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)
                ).values()));
        return resultList;
    }

    /**
     * 文件分组去重
     */
    private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
        // 策略:同分组内文件 merge,不同分组保留

        // 1. 有分组的,以组为单位划分
        Map<String, List<Meta.FileConfig.FileInfo>> groupKeyFileInfoListMap = fileInfoList
                .stream()
                .filter(fileInfo -> StrUtil.isNotBlank(fileInfo.getGroupKey()))
                .collect(
                        Collectors.groupingBy(Meta.FileConfig.FileInfo::getGroupKey)
                );


        // 2. 同组内的文件配置合并
        // 保存每个组对应的合并后的对象 map
        Map<String, Meta.FileConfig.FileInfo> groupKeyMergedFileInfoMap = new HashMap<>();
        for (Map.Entry<String, List<Meta.FileConfig.FileInfo>> entry : groupKeyFileInfoListMap.entrySet()) {
            List<Meta.FileConfig.FileInfo> tempFileInfoList = entry.getValue();
            List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(tempFileInfoList.stream()
                    .flatMap(fileInfo -> fileInfo.getFiles().stream())
                    .collect(
                            Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)
                    ).values());

            // 使用新的 group 配置
            Meta.FileConfig.FileInfo newFileInfo = CollUtil.getLast(tempFileInfoList);
            newFileInfo.setFiles(newFileInfoList);
            String groupKey = entry.getKey();
            groupKeyMergedFileInfoMap.put(groupKey, newFileInfo);
        }

        // 3. 将文件分组添加到结果列表
        List<Meta.FileConfig.FileInfo> resultList = new ArrayList<>(groupKeyMergedFileInfoMap.values());

        // 4. 将未分组的文件添加到结果列表
        List<Meta.FileConfig.FileInfo> noGroupFileInfoList = fileInfoList.stream().filter(fileInfo -> StrUtil.isBlank(fileInfo.getGroupKey()))
                .collect(Collectors.toList());
        resultList.addAll(new ArrayList<>(noGroupFileInfoList.stream()
                .collect(
                        Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)
                ).values()));
        return resultList;
    }

}