07-ftl-meta制作
直接通过工具来生成项目模板和配置文件
1. 需求分析
- 当更改元信息数据模型配置,将模型参数进行分组后,之前已经编写的 FreeMarker 动态模板就无法正确生成内容了。这是因为使用的模型参数发生了变更,导致无法正确获得值
- 动态模板和元信息配置是有很强的绑定关系的,稍有不慎,就有可能导致代码生成异常
- 需求 —— 替换生成的代码包名
- 对于 SpringBoot 项目模板这种相对复杂的项目,里面用到包名的 Java 文件太多了,如果每个文件都要手动“挖坑”来制作模板,不仅成本高、也容易出现遗漏
- 虽然工具已经能够生成代码生成器了,但还是存在 2 大问题:
- 需要人工提前准备动态模板,项目文件越多,使用成本越高
- 需要根据动态模板编写对应的配置,参数越多,越容易出现和模板不一致的风险
需要明确一点:工具的作用只是提高效率,无法覆盖所有的定制需求!
2. 核心设计
在使用工具生成前,依次做了以下事情:
- 先指定一个原始的、待“挖坑”的输入文件
- 明确文件中需要被动态替换的内容和模型参数
- 手动编写 FTL 模板文件
- 手动编写生成器的元信息配置,包括基本信息、文件配置、模型参数配置
分析以上步骤,第 1 - 2 步都是需要用户自主确认的内容,制作工具无法插手;而有了前两步的信息后,3 - 4 步就可以用制作工具来完成
分析出快速制作模板的 基本公式:
- 向工具输入:基本信息 + 输入文件 + 模型参数(+ 输出规则)
- 由工具输出:模板文件 + 元信息配置
- 输入参数:
- 基本信息:要制作的生成器的基本信息,对应元信息的名称、描述、版本号、作者等信息
- 输入文件:要“挖坑”的原始文件。可能是一个文件、也可能是多个文件
- 模型参数:要引导用户输入并填充到模板的模型参数,对应元信息的
modelConfig
模型配置 - 输出规则:作为一个后续扩展功能的可选参数。eg:多次制作时是否覆盖旧的配置等
- 输出参数:在指定目录下生成
- FTL 模板文件
meta.json
元信息配置文件
小技巧:开发复杂需求或新项目时,先一切从简,完成核心流程的开发。在这个过程中可以记录想法和扩展思路,后面再按需实现
3. 基础功能实现
在 maker
包下新建 template
包,所有和模板制作相关的代码都放到该包下,实现功能隔离
1. 基本流程实现
- 预期是以 ACM 示例模板项目为根目录
- 使用
outputText
模型参数来替换其src/com/yupi/acm/MainTemplate.java
文件中的Sum:
输出信息 - 并在同包下生成 “挖好坑” 的
MainTemplate.java.ftl
模板文件 - 以及在根目录下生成
meta.json
元信息文件
- 使用
实现步骤:
- 提供输入参数:包括生成器基本信息、原始项目目录、原始文件、模型参数
- 基于字符串替换算法,使用模型参数的字段名称来替换原始文件的指定内容,并使用替换后的内容来创建 FTL 动态模板文件
- 使用输入信息来创建
meta.json
元信息文件
- 输入信息
- 要格外注意输入文件的路径(win 系统需要对路径进行转义)
- 使用字符串替换,生成模板文件
- 使用
FileUtil.readUtf8String
快速读取文件内容,使用StrUtil.replace
快速替换指定的内容,最后使用FileUtil.writeUtf8String
将替换后的内容快速写入到文件
- 使用
- 生成配置文件
- 先构造 Meta 对象并填充属性,再使用 Hutool 工具库的
JSONUtil.toJsonPrettyStr()
将对象转为格式化后的 JSON 字符串,最后写入meta.json
文件
- 先构造 Meta 对象并填充属性,再使用 Hutool 工具库的
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);
}
}
2. 工作空间隔离
- 避免对原项目的污染
- 约定将
maker
项目下的.tmp
临时目录作为工作空间的根目录,并且在项目的.gitignore
文件中忽略该目录
在 TemplateMaker
原有代码的基础上新增复制目录的逻辑:
- 需要用户传入
originProjectPath
变量代表原始项目路径 - 每次制作分配一个唯一 id(使用雪花算法),作为工作空间的名称,从而实现隔离
- 通过
FileUtil.copy
复制目录 - 修改变量
sourceRootPath
的值为复制后的工作空间内的项目根目录
3. 分步制作能力
一般来说,在制作模板时,不可能只 “挖一个坑”,只允许用户自定义输入一个参数;也不可能一次性 “挖完所有坑”。而是一步一步地替换参数、制作模板
- 工具要有分步制作、追加配置的能力,让制作工具 “有状态”,具体要做到以下 3 点:
- 输入过一次的信息,不用重复输入。eg:基本的项目信息
- 后续制作时,不用再次复制原始项目;而是可以在原有文件的基础上,多次追加或覆盖新的文件
- 后续制作时,可以在原有配置的基础上,多次追加或覆盖配置
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 判断出并非首次制作,又应该做哪些调整呢?应该如何追加配置和文件呢?
- 非首次制作,不需要复制原始项目文件
- 之前的
TemplateMaker
已经判断了某 id 对应的工作空间目录是否存在
- 之前的
- 非首次制作,可以在已有模板的基础上再次挖坑
- 如果已有
.ftl
文件,表示不是第一次制作,可以在这个模板文件的基础上再去替换内容
- 如果已有
- 非首次制作,不需要重复输入已有元信息,而是在此基础上覆盖和追加元信息配置
- 通过是否存在
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 拿到所有的文件信息列表即可
- 用到了 Java 8 的 Stream API 和 Lambda 表达式来简化代码,其中
- 模型参数根据属性名称
fieldName
去重,使用新值覆盖旧值。和文件信息去重的实现方式完全一致
4. 抽象方法
多次传入不同的参数执行制作,可以先抽象出通用方法,将所有之前 main()
中硬编码的值都作为方法的参数
originProjectPath
(原始项目路径)inputFilePath
(要制作模板的输入文件相对路径)modelInfo
(模型信息)searchStr
(要替换的模板内容)
- 把所有基本信息配置用
Meta
类封装,节约方法的参数个数 - 如果非首次制作。可以通过
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
将得到的 id 作为 makeTemplate()
参数,修改传入的模型信息和替换变量
然后再次执行,可以发现模板文件又多 “挖了一个坑”,并且元信息配置多加了一个模型参数
4. 更多功能实现
1. 单次制作多个模板文件
- 支持输入文件目录,同时处理该目录下的所有文件
- 支持输入多个文件路径,同时处理这些文件
1. 支持输入文件目录
只要循环遍历 “单个文件制作模板” 的操作,就能轻松实现!!!
- 首先需要抽象出制作单个文件模板的方法
makeFileTemplate()
- 接受单个文件、模型信息、替换文本、sourceRootPath 等参数,返回 FileInfo 文件信息
- 由于之前的输入文件路径是相对路径,而之后要遍历文件目录下的所有文件时,传来的文件是绝对路径,将方法修改为 File 类型
- 注意:在方法内部,要将绝对路径再转换为相对路径,以适配元信息文件的规则
- 如果输入的文件路径是目录,那么使用 Hutool 的
loopFiles
方法递归遍历并获取目录下的所有文件列表- 其中,使用
newFileInfoList
来存储所有文件的信息列表 - 在生成配置文件时,之前使用
fileInfoList.add
添加一个文件信息对象,改为fileInfoList.addAll
添加newFileInfoList
文件信息列表
- 其中,使用
- 修改
main()
中传入的原始项目路径为springboot-init
项目,输入文件路径改为springbootinit
目录- 执行测试,目录下的所有文件都生成了模板
- 优化下逻辑,如果某个文件内容没有被参数替换,那么就不生成模板,而是以静态生成的方式记录到元信息配置中
- 修改
makeFileTemplate()
,通过对比替换前后的内容是否一致来更改生成方式 - 注意:如果是静态生成,文件输出路径(outputPath)要设置为和输入路径(inputPath)相同
- 修改
- 更改
main()
中的searchStr
为BaseResponse
,然后再次执行测试- 效果符合预期,springbootinit 包中,包含该字符串的文件才生成了模板
- 查看生成的元信息配置文件,生成了符合要求的静态和动态文件配置
2. 支持输入多个文件
只要把 makeTemplate()
的入参 inputFilePath
(单数)改为 inputFilePathList
(复数),再多加一层循环处理即可
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. 集成测试
- 模板制作工具类使用过滤器
- 将
makeTemplate()
接受的inputFilePathList
参数改为新封装的TemplateMakerFileConfig
,相当于同时传入了文件列表和过滤规则 - 然后修改遍历输入文件的代码,改为遍历
fileConfigInfoList
获取文件信息 - 应用过滤器。将文件信息配置中的 相对路径转化为绝对路径 作为调用过滤器的参数,并通过过滤器获取到所有文件列表(注意,这里不可能是目录),再遍历文件列表来制作模板
- 将
- 在
main()
中编写文件过滤测试代码,只处理 common 包下文件名称包含Base
的文件和 controller 包下的文件- 执行测试,只有
BaseResponse.java
生成了模板文件,符合预期
- 执行测试,只有
生成的元信息配置也没有多余的文件:
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 种分组策略:
- 一个文件信息配置(
FileInfoConfig
)对应一次分组。如果传入的 path 是目录,则目录下的所有文件为同组 - 一个完整的文件配置(
TemplateMakerFileConfig
)对应一次分组。即配置 files 列表中的所有文件都属于同组
选择第 2 种方案。从需求出发,对于 “要用一个参数控制帖子相关的文件是否生成” 需求,有可能要把跨目录下的文件设置为一个组
2. 开发实现
- 首先给
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;
}
}
- 在
TemplateMaker
的makeTemplate()
中增加文件组相关代码,思路是将本次得到的所有文件信息都放到同一个分组下- 在 “生成配置文件” 前增加代码
3. 追加配置能力
- 已经实现了文件分组配置的生成,但实际制作模板的过程中,可能没办法一步到位,而是希望能多次制作模板
- 文件分组要能支持多次制作追加配置的能力,可以增加新的分组、也可以在同分组下新增文件
- 两个分组的
groupKey
相同,视为同一个组,需要将第 2 次得到的分组内的所有文件和之前分组内的文件进行合并去重
文件分组去重 distinctFiles()
的实现流程,步骤:
- 将所有文件配置(fileInfo)分为有分组的和无分组的
- 对于有分组的文件配置,如果有相同的分组,同分组内的文件进行合并(merge),不同分组可同时保留
- 创建新的文件配置列表(结果列表),先将合并后的分组添加到结果列表
- 再将无分组的文件配置列表添加到结果列表
- 修改
main()
中的inputFilePath2
,指定一个新的目录
String inputFilePath2 = "src/main/java/com/yupi/springbootinit/constant";
- 基于之前制作好的模板再次执行,发现第 2 次新增的文件合并到了之前的分组配置中,符合预期
- 修改分组的名称、条件等配置,新的配置会覆盖之前的配置
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. 开发实现
- 像文件配置类(
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;
}
}
- 修改模型去重方法,和文件去重逻辑一致,实现同组模型信息的去重合并
- 修改
makeTemplate()
入参,使用封装好的模型配置类TemplateMakerModelConfig
代替modelInfo
和searchStr
- 从模型配置中读出分组和模型列表信息,并转换为用于生成元信息配置的
newModelInfoList
- 之前生成元信息配置,是增加了单个
ModelInfo
对象,现在需要改为增加newModelInfoList
列表
- 从模型配置中读出分组和模型列表信息,并转换为用于生成元信息配置的
- 修改
makeFileTemplate()
,要能够支持使用多个模型参数对文件 “挖坑”- 实现思路是依次遍历模型参数,对文件内容进行替换,将上一轮替换后的结果作为新一轮要替换的内容,从而实现多轮替换
- 测试验证,定义一组能够替换 MySQL 配置的模型组参数,用来替换
application.yml
配置文件- 执行,成功制作出了有多个参数的模板文件:
- 可以尝试更换模型参数组的 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;
}
}