模板生成工具原创
# 前情回顾
在上期教程中,我们首先明确了第二阶段的目标 —— 通过代码生成器制作工具快速生成 Spring Boot 项目模板代码生成器,并且介绍了示例生成的 Spring Boot 项目模板。然后我们分析得出了 7 种生成器应具备的功能,并且从中梳理除了几个制作工具应具备的通用配置能力,比如支持用一个参数同时控制多个文件的生成、定义可选开启的参数组等等。
但光有这些能力还是不够的,想更快地制作代码生成器,我们还可以从“根源”去解决问题,直接通过制作工具来生成项目模板和配置文件。
本期教程,就让我们来进一步增加制作工具的功能,实现上述目标吧!
# 本节重点
本节教程属于项目的第二阶段 —— 开发代码生成器制作工具。
本节主要是开发模板制作工具,这一期的教程和代码甚至可以单独拿出来作为一个 快速挖坑工具
小项目了!
重点内容:
- 模板制作工具 - 需求分析
- 模板制作工具 - 核心设计
- 模板制作工具 - 基础功能实现
- 模板制作工具 - 更多功能实现
# 一、需求分析
还记得么?在上期教程的最后,我们遇到了一个问题:当我们更改元信息数据模型配置,将模型参数进行分组后,我们之前已经编写的 FreeMarker 动态模板就无法正确生成内容了。这是因为使用的模型参数发生了变更,导致无法正确获得值。
通过这个问题,我们会发现,动态模板和元信息配置是有很强的绑定关系的,稍有不慎,就有可能导致代码生成异常。
此外,我们上期教程中,还遗留了一个需求无法解决 —— 替换生成的代码包名。
因为对于 Spring Boot 项目模板这种相对复杂的项目,里面用到包名的 Java 文件太多了,如果每个文件都要自己“挖坑”来制作模板,不仅成本高、也容易出现遗漏。
也就是说,虽然制作工具已经能够生成代码生成器了,但还是存在 2 大问题:
- 需要人工提前准备动态模板,项目文件越多,使用成本越高
- 需要根据动态模板编写对应的配置,参数越多,越容易出现和模板不一致的风险
如何解决这个问题呢?
答案很简单。我们可以让制作工具根据我们的想法,自动给项目文件“挖坑”,并生成相互对应的动态模板文件和元信息配置。提高效率的同时,减少模型参数和模板不一致的风险。
这就是我们本期教程需要完成的需求。
不过需要明确一点: 制作工具的作用只是提高效率,无法覆盖所有的定制需求!
因为想要如何制作代码生成器,还是取决于开发者。
# 二、核心设计
我们先分析下如何实现上述需求。
经常跟大家提到这么一句话:程序的本质就是帮我们完成原本人工需要进行的操作。
所以想让程序自动制作模板和生成配置,我们就要先想一想:之前我们是怎么完成这些操作的?
在使用制作工具生成前,我们依次做了以下事情:
- 先指定一个原始的、待“挖坑”的输入文件
- 明确文件中需要被动态替换的内容和模型参数
- 自己编写 FreeMarker FTL 模板文件
- 自己编写生成器的元信息配置,包括基本信息、文件配置、模型参数配置
分析上面的步骤,第 1 - 2 步都是需要用户自主确认的内容,制作工具无法插手;而有了前两步的信息后,3 - 4 步就可以用制作工具来完成。
由此,我们可以分析出快速制作模板的 基本公式 :
- 向制作工具输入:基本信息 + 输入文件 + 模型参数(+ 输出规则)
- 由制作工具输出:模板文件 + 元信息配置
跟编写算法题目一样,先明确算法的输入和输出,再去设计实现算法
对应的算法流程图如下:
分别解释一下上述输入参数:
1)基本信息:要制作的代码生成器的基本信息,对应元信息的名称、描述、版本号、作者等信息。
2)输入文件:要“挖坑”的原始文件。可能是一个文件、也可能是多个文件。
3)模型参数:要引导用户输入并填充到模板的模型参数,对应元信息的 modelConfig 模型配置。
4)输出规则:作为一个后续扩展功能的可选参数,比如多次制作时是否覆盖旧的配置等。
输出参数就比较好理解了,在指定目录下生成 FTL 模板文件、以及 meta.json
元信息配置文件。
明确了程序的输入输出后,下面我们就先实现一个最基础的模板制作工具,然后再陆续给工具增加功能。
小技巧:开发复杂需求或新项目时,先一切从简,完成核心流程的开发。在这个过程中可以记录想法和扩展思路,后面再按需实现。
# 三、基础功能实现
首先打开制作工具(maker)项目,在 maker
包下新建 template
包,所有和模板制作相关的代码都放到该包下,实现功能隔离。
目前项目中的
maker.model
目录和FileGenerator
的main
方法是多余的,可以删除。
# 1、基本流程实现
下面我们就以第一阶段准备好的 ACM 示例模板项目为例,来编写模板制作工具的基本流程代码。
预期是以 ACM 示例模板项目为根目录,使用 outputText
模型参数来替换其 src/com/yupi/acm/MainTemplate.java
文件中的 Sum:
输出信息,并在同包下生成 “挖好坑” 的 MainTemplate.java.ftl
模板文件,以及在根目录下生成 meta.json
元信息文件。
实现步骤如下:
- 提供输入参数:包括生成器基本信息、原始项目目录、原始文件、模型参数
- 基于字符串替换算法,使用模型参数的字段名称来替换原始文件的指定内容,并使用替换后的内容来创建 FTL 动态模板文件
- 使用输入信息来创建
meta.json
元信息文件
在 template
包下新建 TemplateMaker.java
文件,并依次在 main 方法中实现上述步骤。
1)输入信息
要格外注意输入文件的路径(win 系统需要对路径进行转义),代码如下:
// 一、输入信息
// 1. 输入项目基本信息
String name = "acm-template-generator";
String description = "ACM 示例模板生成器";
// 2. 输入文件信息
String projectPath = System.getProperty("user.dir");
String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";
// 注意 win 系统需要对路径进行转义
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
String fileInputPath = "src/com/yupi/acm/MainTemplate.java";
String fileOutputPath = fileInputPath + ".ftl";
// 3. 输入模型参数信息
Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
modelInfo.setFieldName("outputText");
modelInfo.setType("String");
modelInfo.setDefaultValue("sum = ");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2)使用字符串替换,生成模板文件
代码如下:
// 二、使用字符串替换,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newFileContent = StrUtil.replace(fileContent, "Sum: ", replacement);
// 输出模板文件
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
2
3
4
5
6
7
8
9
上述代码中,使用 FileUtil.readUtf8String
快速读取文件内容,使用 StrUtil.replace
快速替换指定的内容,最后使用 FileUtil.writeUtf8String
将替换后的内容快速写入到文件。
3)生成配置文件
思路是先构造 Meta 对象并填充属性,再使用 Hutool 工具库的 JSONUtil.toJsonPrettyStr
方法将对象转为格式化后的 JSON 字符串,最后再写入 meta.json
文件。
代码如下:
// 三、生成配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";
// 1. 构造配置参数
Meta meta = new Meta();
meta.setName(name);
meta.setDescription(description);
Meta.FileConfig fileConfig = new Meta.FileConfig();
meta.setFileConfig(fileConfig);
fileConfig.setSourceRootPath(sourceRootPath);
List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
fileConfig.setFiles(fileInfoList);
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
fileInfoList.add(fileInfo);
Meta.ModelConfig modelConfig = new Meta.ModelConfig();
meta.setModelConfig(modelConfig);
List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
modelConfig.setModels(modelInfoList);
modelInfoList.add(modelInfo);
// 2. 输出元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
组合以上几个步骤的代码,完整代码如下:
package com.yupi.maker.template;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.yupi.maker.meta.Meta;
import com.yupi.maker.meta.enums.FileGenerateTypeEnum;
import com.yupi.maker.meta.enums.FileTypeEnum;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
public class TemplateMaker {
public static void main(String[] args) {
// 一、输入信息
// 1. 输入项目基本信息
String name = "acm-template-generator";
String description = "ACM 示例模板生成器";
// 2. 输入文件信息
String projectPath = System.getProperty("user.dir");
String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";
// 注意 win 系统需要对路径进行转义
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
String fileInputPath = "src/com/yupi/acm/MainTemplate.java";
String fileOutputPath = fileInputPath + ".ftl";
// 3. 输入模型参数信息
Meta.ModelConfig.ModelInfo modelInfo = new Meta.ModelConfig.ModelInfo();
modelInfo.setFieldName("outputText");
modelInfo.setType("String");
modelInfo.setDefaultValue("sum = ");
// 二、使用字符串替换,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newFileContent = StrUtil.replace(fileContent, "Sum: ", replacement);
// 输出模板文件
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
// 三、生成配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";
// 1. 构造配置参数
Meta meta = new Meta();
meta.setName(name);
meta.setDescription(description);
Meta.FileConfig fileConfig = new Meta.FileConfig();
meta.setFileConfig(fileConfig);
fileConfig.setSourceRootPath(sourceRootPath);
List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
fileConfig.setFiles(fileInfoList);
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
fileInfoList.add(fileInfo);
Meta.ModelConfig modelConfig = new Meta.ModelConfig();
meta.setModelConfig(modelConfig);
List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
modelConfig.setModels(modelInfoList);
modelInfoList.add(modelInfo);
// 2. 输出元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
运行 main 方法,测试执行,成功生成了需要的模板和元信息文件:
虽然制作模板的流程是跑通了,但我们会发现一个问题:上述代码直接在原始项目内生成了模板和元信息配置,其实是对原项目的污染。如果我们想重新生成,就得一个个删除上次生成的文件。
# 2、工作空间隔离
想解决上面的问题,其实很简单。每次制作模板时,我们都不直接修改原始项目的任何文件,而是先复制原项目到一个临时的、专门用于制作模板的目录,然后在该目录下完成文件的生成和处理。
可以将上述临时目录称为 工作空间
,每次模板制作应该属于不同的工作空间,互不影响。iZC7DB8PXu/GGlQ4cVha5zDNtNCK+ikSmWAXB1nDW5I=
我们约定将 maker
项目下的 .temp
临时目录作为工作空间的根目录,并且在项目的 .gitignore
文件中忽略该目录。
在 TemplateMaker
原有代码的基础上新增复制目录的逻辑:
- 需要用户传入
originProjectPath
变量代表原始项目路径 - 每次制作分配一个唯一 id(使用雪花算法),作为工作空间的名称,从而实现隔离
- 通过
FileUtil.copy
复制目录 - 修改变量
sourceRootPath
的值为复制后的工作空间内的项目根目录
对应修改的代码如下:
public static void main(String[] args) {
// 指定原始项目路径
String projectPath = System.getProperty("user.dir");
String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";
// 复制目录
long id = IdUtil.getSnowflakeNextId();
String tempDirPath = projectPath + File.separator + ".temp";
String templatePath = tempDirPath + File.separator + id;
if (!FileUtil.exist(templatePath)) {
FileUtil.mkdir(templatePath);
}
FileUtil.copy(originProjectPath, templatePath, true);
// 一、输入信息
// 1. 输入项目基本信息
String name = "acm-template-generator";
String description = "ACM 示例模板生成器";
// 2. 输入文件信息
String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
String fileInputPath = "src/com/yupi/acm/MainTemplate.java";
String fileOutputPath = fileInputPath + ".ftl";
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
再次测试执行,可以看到 maker 项目下新建了一个工作空间,并且生成了模板和元信息配置文件,如下图:
# 3、分步制作能力
一般来说,我们在制作模板时,不可能只 “挖一个坑”,只允许用户自定义输入一个参数;也不可能一次性 “挖完所有坑”。而是一步一步地替换参数、制作模板。
所以,我们的制作工具要有分步制作、追加配置的能力,具体要做到以下 3 点:
- 输入过一次的信息,不用重复输入,比如基本的项目信息
- 后续制作时,不用再次复制原始项目;而是可以在原有文件的基础上,多次追加或覆盖新的文件
- 后续制作时,可以在原有配置的基础上,多次追加或覆盖配置
想要实现这个能力,我们首先要让制作工具 “有状态”。
# 有状态和无状态
什么是有状态?
是指程序或请求多次执行时,下一次执行保留对上一次执行的记忆。比如用户登录后服务器会记住用户的信息,下一次请求就能正常使用系统。
与之相对的是无状态。是指每次程序或请求执行,都像是第一次执行一样,没有任何历史信息。很多 Restful API 会采用无状态的设计,能够节省服务器的资源占用。
# 有状态实现
想实现有状态,往往需要 2 个要素:唯一标识和存储。
其实在上一步 “工作空间隔离” 中,我们已经给每个工作空间分配了一个唯一的 id 作为标识;并且将 id 作为工作空间的目录名称,相当于使用本地文件系统作为了 id 的存储。
那么我们只要在第一次制作时,生成唯一的 id;然后在后续制作时,使用相同的 id,就能找到之前的工作空间目录,从而追加文件或配置。
修改 TemplateMaker
文件,新建 makeTemplate
方法,并将 id 作为参数,先实现 id 的生成逻辑。
代码如下:
private static long makeTemplate(Long id) {
// 没有 id 则生成
if (id == null) {
id = IdUtil.getSnowflakeNextId();
}
// 业务逻辑...
return id;
}
2
3
4
5
6
7
8
9
10
# 多次制作实现
如果根据 id 判断出并非首次制作,我们又应该做哪些调整呢?应该如何追加配置和文件呢?
这里我考虑到 3 点:
- 非首次制作,不需要复制原始项目文件
- 非首次制作,可以在已有模板的基础上再次挖坑
- 非首次制作,不需要重复输入已有元信息,而是在此基础上覆盖和追加元信息配置
下面分别实现这几点。
1)非首次制作,不需要复制原始项目文件
之前的 TemplateMaker
代码已经判断了某 id 对应的工作空间目录是否存在,现在只需要把复制原始项目文件的逻辑移到 “首次制作” 的 if 条件中即可。
修改后的代码如下:
// 复制目录
long id = IdUtil.getSnowflakeNextId();
String tempDirPath = projectPath + File.separator + ".temp";
String templatePath = tempDirPath + File.separator + id;
// 是否为首次制作模板
// 目录不存在,则是首次制作
if (!FileUtil.exist(templatePath)) {
FileUtil.mkdir(templatePath);
FileUtil.copy(originProjectPath, templatePath, true);
}
2
3
4
5
6
7
8
9
10
11
2)非首次制作,可以在已有模板的基础上再次挖坑
由于制作好的模板文件名称就是在原始文件名称后增加 .ftl
后缀,所以我们可以将输出文件路径(包含 .ftl
后缀的路径)作为文件是否重复的判断条件。如果已有 .ftl
文件,表示不是第一次制作,可以在这个模板文件的基础上再去替换内容。
修改后的代码如下:
// 二、使用字符串替换,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
String fileContent = null;
// 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
if (FileUtil.exist(fileOutputAbsolutePath)) {
fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
} else {
fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
}
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newFileContent = StrUtil.replace(fileContent, "Sum: ", replacement);
// 输出模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3)非首次制作,不需要重复输入已有元信息,而是在此基础上覆盖和追加元信息配置。
和判断文件模板是否重复的逻辑一致,我们可以通过是否已存在 meta.json
配置文件来判断应该新增还是修改配置。
修改后的代码如下:
我们将 fileInfo 对象的构造移到了前面,使得无论是新增还是修改元信息都能使用该对象
// 文件配置信息
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
// 三、生成配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";
// 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
if (FileUtil.exist(metaOutputPath)) {
Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
// 1. 追加配置参数
List<Meta.FileConfig.FileInfo> fileInfoList = oldMeta.getFileConfig().getFiles();
fileInfoList.add(fileInfo);
List<Meta.ModelConfig.ModelInfo> modelInfoList = oldMeta.getModelConfig().getModels();
modelInfoList.add(modelInfo);
// 2.更新元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(oldMeta), metaOutputPath);
} else {
// 1. 构造配置参数
Meta meta = new Meta();
meta.setName(name);
meta.setDescription(description);
Meta.FileConfig fileConfig = new Meta.FileConfig();
meta.setFileConfig(fileConfig);
fileConfig.setSourceRootPath(sourceRootPath);
List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
fileConfig.setFiles(fileInfoList);
fileInfoList.add(fileInfo);
Meta.ModelConfig modelConfig = new Meta.ModelConfig();
meta.setModelConfig(modelConfig);
List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
modelConfig.setModels(modelInfoList);
modelInfoList.add(modelInfo);
// 2. 输出元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
一定要注意,追加完配置后,需要去重!否则可能出现多个一模一样的模型参数或文件信息。
文件信息根据输入路径 inputPath
去重,使用新值覆盖旧值。代码如下:
/**
* 文件去重
*
* @param fileInfoList
* @return
*/
private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(
fileInfoList.stream()
.collect(
Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)
).values()
);
return newFileInfoList;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上述代码中,用到了 Java 8 的 Stream API 和 Lambda 表达式来简化代码,其中 Collectors.toMap
表示将列表转换为 Map,详细解释一下:
- 通过第一个参数(inputPath)作为 key 进行分组
- 通过第二个参数作为 value 存储值(
o -> o
表示使用原对象作为 value) - 最后的
(e, r) -> r
其实是(exist, replacement) -> replacement
的缩写,表示遇到重复的值是保留新值,返回 exist 表示保留旧值。
这样一来,相同 key 对应的文件信息只会保留一个,最后再取所有的 values 拿到所有的文件信息列表即可。
模型参数根据属性名称 fieldName
去重,使用新值覆盖旧值。和文件信息去重的实现方式完全一致,代码如下:
/**
* 模型去重
*
* @param modelInfoList
* @return
*/
private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(
modelInfoList.stream()
.collect(
Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)
).values()
);
return newModelInfoList;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
然后修改生成配置文件的代码,使用去重方法,并将去重后的配置更新到元信息中:
// 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
if (FileUtil.exist(metaOutputPath)) {
Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
// 1. 追加配置参数
List<Meta.FileConfig.FileInfo> fileInfoList = oldMeta.getFileConfig().getFiles();
fileInfoList.add(fileInfo);
List<Meta.ModelConfig.ModelInfo> modelInfoList = oldMeta.getModelConfig().getModels();
modelInfoList.add(modelInfo);
// 配置去重
oldMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
oldMeta.getModelConfig().setModels(distinctModels(modelInfoList));
// 2.更新元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(oldMeta), metaOutputPath);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 抽象方法
由于我们在接下来的测试中,要多次传入不同的参数执行制作,所以可以先抽象出通用的方法,将所有之前我们在 main 方法中硬编码的值都作为方法的参数。包括 originProjectPath(原始项目路径)、inputFilePath(要制作模板的输入文件相对路径)、modelInfo(模型信息)、searchStr(要替换的模板内容)等。
我们还要把所有基本信息配置直接用 Meta 类封装,可以节约方法的参数个数,比如:vZvvLYw3j8nTpc1i6/0xN5tNL9Aw+ZOh8FzCTZZoWmo=
String name = "acm-template-generator";
String description = "ACM 示例模板生成器";
2
改为:
Meta meta = new Meta();
meta.setName("acm-template-generator");
meta.setDescription("ACM 示例模板生成器");
2
3
如果非首次制作,我们还要能使用最后传入的 meta 对象更新元信息的基本配置。可以通过 BeanUtil.copyProperties
复制新对象的属性到老对象(如果属性为空则不复制),从而实现新老 meta 对象的合并。
代码如下:
Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
BeanUtil.copyProperties(meta, oldMeta, CopyOptions.create().ignoreNullValue());
2
抽象方法后的完整代码如下:
/**
* 制作模板
*
* @param newMeta
* @param originProjectPath
* @param inputFilePath
* @param modelInfo
* @param searchStr
* @param id
* @return
*/
public static long makeTemplate(Meta newMeta, String originProjectPath, String inputFilePath, Meta.ModelConfig.ModelInfo modelInfo, String searchStr, Long id) {
// 没有 id 则生成
if (id == null) {
id = IdUtil.getSnowflakeNextId();
}
// 复制目录
String projectPath = System.getProperty("user.dir");
String tempDirPath = projectPath + File.separator + ".temp";
String templatePath = tempDirPath + File.separator + id;
// 是否为首次制作模板
// 目录不存在,则是首次制作
if (!FileUtil.exist(templatePath)) {
FileUtil.mkdir(templatePath);
FileUtil.copy(originProjectPath, templatePath, true);
}
// 一、输入信息
// 输入文件信息
String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
// 注意 win 系统需要对路径进行转义
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
String fileInputPath = inputFilePath;
String fileOutputPath = fileInputPath + ".ftl";
// 二、使用字符串替换,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
String fileContent = null;
// 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
if (FileUtil.exist(fileOutputAbsolutePath)) {
fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
} else {
fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
}
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newFileContent = StrUtil.replace(fileContent, searchStr, replacement);
// 输出模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
// 文件配置信息
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
// 三、生成配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";
// 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
if (FileUtil.exist(metaOutputPath)) {
Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
BeanUtil.copyProperties(newMeta, oldMeta, CopyOptions.create().ignoreNullValue());
newMeta = oldMeta;
// 1. 追加配置参数
List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
fileInfoList.add(fileInfo);
List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
modelInfoList.add(modelInfo);
// 配置去重
newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
newMeta.getModelConfig().setModels(distinctModels(modelInfoList));
} else {
// 1. 构造配置参数
Meta.FileConfig fileConfig = new Meta.FileConfig();
newMeta.setFileConfig(fileConfig);
fileConfig.setSourceRootPath(sourceRootPath);
List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
fileConfig.setFiles(fileInfoList);
fileInfoList.add(fileInfo);
Meta.ModelConfig modelConfig = new Meta.ModelConfig();
newMeta.setModelConfig(modelConfig);
List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
modelConfig.setModels(modelInfoList);
modelInfoList.add(modelInfo);
}
// 2. 输出元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);
return id;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# 测试
最后,在 main 方法中指定 2 套不同的模型参数信息作为测试数据,并调用 makeTemplate
方法。
代码如下:
public static void main(String[] args) {
Meta meta = new Meta();
meta.setName("acm-template-generator");
meta.setDescription("ACM 示例模板生成器");
String projectPath = System.getProperty("user.dir");
String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
第一次执行,成功制作模板并生成元信息,返回了一个新的 id:
将得到的 id 作为 makeTemplate
方法的参数,修改传入的模型信息和替换变量:
然后再次执行,可以发现模板文件又多 “挖了一个坑”,并且元信息配置多加了一个模型参数:
# 完整代码
TemplateMaker
的完整代码如下:
package com.yupi.maker.template;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.yupi.maker.meta.Meta;
import com.yupi.maker.meta.enums.FileGenerateTypeEnum;
import com.yupi.maker.meta.enums.FileTypeEnum;
import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class TemplateMaker {
/**
* 制作模板
*
* @param newMeta
* @param originProjectPath
* @param inputFilePath
* @param modelInfo
* @param searchStr
* @param id
* @return
*/
public static long makeTemplate(Meta newMeta, String originProjectPath, String inputFilePath, Meta.ModelConfig.ModelInfo modelInfo, String searchStr, Long id) {
// 没有 id 则生成
if (id == null) {
id = IdUtil.getSnowflakeNextId();
}
// 复制目录
String projectPath = System.getProperty("user.dir");
String tempDirPath = projectPath + File.separator + ".temp";
String templatePath = tempDirPath + File.separator + id;
// 是否为首次制作模板
// 目录不存在,则是首次制作
if (!FileUtil.exist(templatePath)) {
FileUtil.mkdir(templatePath);
FileUtil.copy(originProjectPath, templatePath, true);
}
// 一、输入信息
// 输入文件信息
String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
// 注意 win 系统需要对路径进行转义
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
String fileInputPath = inputFilePath;
String fileOutputPath = fileInputPath + ".ftl";
// 二、使用字符串替换,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
String fileContent = null;
// 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
if (FileUtil.exist(fileOutputAbsolutePath)) {
fileContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
} else {
fileContent = FileUtil.readUtf8String(fileInputAbsolutePath);
}
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newFileContent = StrUtil.replace(fileContent, searchStr, replacement);
// 输出模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
// 文件配置信息
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
// 三、生成配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";
// 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
if (FileUtil.exist(metaOutputPath)) {
Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
BeanUtil.copyProperties(newMeta, oldMeta, CopyOptions.create().ignoreNullValue());
newMeta = oldMeta;
// 1. 追加配置参数
List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
fileInfoList.add(fileInfo);
List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
modelInfoList.add(modelInfo);
// 配置去重
newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
newMeta.getModelConfig().setModels(distinctModels(modelInfoList));
} else {
// 1. 构造配置参数
Meta.FileConfig fileConfig = new Meta.FileConfig();
newMeta.setFileConfig(fileConfig);
fileConfig.setSourceRootPath(sourceRootPath);
List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
fileConfig.setFiles(fileInfoList);
fileInfoList.add(fileInfo);
Meta.ModelConfig modelConfig = new Meta.ModelConfig();
newMeta.setModelConfig(modelConfig);
List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
modelConfig.setModels(modelInfoList);
modelInfoList.add(modelInfo);
}
// 2. 输出元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);
return id;
}
public static void main(String[] args) {
Meta meta = new Meta();
meta.setName("acm-template-generator");
meta.setDescription("ACM 示例模板生成器");
String projectPath = System.getProperty("user.dir");
String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/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, 1735281524670181376L);
System.out.println(id);
}
/**
* 模型去重
*
* @param modelInfoList
* @return
*/
private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(modelInfoList.stream().collect(Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)).values());
return newModelInfoList;
}
/**
* 文件去重
*
* @param fileInfoList
* @return
*/
private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(fileInfoList.stream().collect(Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)).values());
return newFileInfoList;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
至此,分步制作能力开发完成。你还可以试着多次修改 meta 的基本信息,也是可以正确更新的。
实现了这个能力后,我们不仅可以依次使用多个模型参数对同一个文件 “挖坑”,也可以使用同一个模型参数依次对多个不同的文件挖坑。已经能够满足像 ACM 示例模板这种简单的项目了。
接下来,我们要给模板制作工具增加更多的功能,进一步提高使用的效率、并尽力满足 Spring Boot 模板项目代码生成器所需的能力。
# 四、更多功能实现
# 1、单次制作多个模板文件
虽然现在模板制作工具可以通过多次执行来制作多个模板文件,但还是比较麻烦。对于我们之前提到的 “批量替换项目下所有文件包名” 的需求,可能需要制作几十个模板文件,难道要执行几十次么?
所以我们需要实现多模板文件同时制作的能力。
有 2 种方法:
- 支持输入文件目录,同时处理该目录下的所有文件
- 支持输入多个文件路径,同时处理这些文件
下面依次实现这两种方法。
# 支持输入文件目录
实现思路很简单,之前我们已经能针对单个文件制作模板了。那么对于文件目录下的多个文件,只要循环遍历 “单个文件制作模板” 的操作,不就能轻松实现了么?
1)所以我们首先需要抽象出制作单个文件模板的方法 makeFileTemplate
,接受单个文件、模型信息、替换文本、sourceRootPath 等参数,返回 FileInfo 文件信息。
由于之前的输入文件路径是相对路径,而之后我们要遍历文件目录下的所有文件时,传来的文件是绝对路径。所以最好将方法接受的参数,由 String 类型的输入文件路径,修改为 File 类型的输入文件对象。但还要注意一点,在方法内部,要将绝对路径再转换为相对路径,以适配元信息文件的规则。
完整代码如下:
/**
* 制作文件模板
*
* @param modelInfo
* @param searchStr
* @param sourceRootPath
* @param inputFile
* @return
*/
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);
// 输出模板文件
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());
return fileInfo;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2)如果输入的文件路径是目录,那么使用 Hutool 的 loopFiles
方法递归遍历并获取目录下的所有文件列表。
修改 makeTemplate
方法中生成文件的相关代码,如下:
// 一、输入信息
// 输入文件信息
String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
// 二、生成文件模板
// 输入文件为目录
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
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";
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
其中,使用 newFileInfoList
来存储所有文件的信息列表。
在生成配置文件时,以前是使用 fileInfoList.add
添加一个文件信息对象,现在要改为 fileInfoList.addAll
添加 newFileInfoList
文件信息列表。
修改后的代码如下:
// 三、生成配置文件
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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
3)修改 main 方法中传入的原始项目路径为 springboot-init
项目,输入文件路径改为 springbootinit
目录:
String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/springboot-init";
String inputFilePath = "src/main/java/com/yupi/springbootinit";
2
然后执行测试,目录下的所有文件都生成了模板:
但是貌似生成的模板有点太多了。。。明明这些文件内没有任何内容被模型参数替换了,但也生成了模板?
4)所以这里我们需要优化下逻辑,如果某个文件内容没有被参数替换,那么就不生成模板,而是以静态生成的方式记录到元信息配置中。
修改 makeFileTemplate
方法,通过对比替换前后的内容是否一致来更改生成方式。
注意,如果是静态生成,文件输出路径(outputPath)要设置为和输入路径(inputPath)相同。
修改后的方法代码如下(主要修改了后半段):
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
5)更改 main 方法中的 searchStr
为 BaseResponse
,然后再次执行测试。
效果符合预期,springbootinit 包中,包含该字符串的文件才生成了模板:
查看生成的元信息配置文件,生成了符合要求的静态和动态文件配置:
# 支持输入多个文件
如果想在单次制作时支持输入多个文件,其实只要把 makeTemplate
方法的输入参数 inputFilePath
(单数)改为 inputFilePathList
(复数),再多加一层循环处理即可。
修改 makeTemplate
方法中生成文件模板的代码如下:vZvvLYw3j8nTpc1i6/0xN5tNL9Aw+ZOh8FzCTZZoWmo=
// 二、生成文件模板
// 遍历输入文件
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
然后修改 main 方法中的测试条件为文件列表,验证效果:
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);
2
3
结果符合预期,同时将多个指定目录下的文件制作成了模板:
# 完整代码
至此,已经实现了单次制作多个模板文件的功能。
TemplateMaker
的完整代码如下:
package com.yupi.maker.template;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.yupi.maker.meta.Meta;
import com.yupi.maker.meta.enums.FileGenerateTypeEnum;
import com.yupi.maker.meta.enums.FileTypeEnum;
import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class TemplateMaker {
/**
* 制作模板
*
* @param newMeta
* @param originProjectPath
* @param inputFilePathList
* @param modelInfo
* @param searchStr
* @param id
* @return
*/
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 + ".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<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;
}
/**
* 制作文件模板
*
* @param modelInfo
* @param searchStr
* @param sourceRootPath
* @param inputFile
* @return
*/
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;
}
public static void main(String[] args) {
Meta meta = new Meta();
meta.setName("acm-template-generator");
meta.setDescription("ACM 示例模板生成器");
String projectPath = System.getProperty("user.dir");
String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/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 = "Sum: ";
// 替换变量(第二次)
String searchStr = "BaseResponse";
long id = makeTemplate(meta, originProjectPath, inputFilePathList, modelInfo, searchStr, 1735281524670181376L);
System.out.println(id);
}
/**
* 模型去重
*
* @param modelInfoList
* @return
*/
private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(modelInfoList.stream().collect(Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)).values());
return newModelInfoList;
}
/**
* 文件去重
*
* @param fileInfoList
* @return
*/
private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(fileInfoList.stream().collect(Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)).values());
return newFileInfoList;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
# 2、文件过滤
回忆下我们之前梳理的 Spring Boot 模板生成器需要的一个功能:
需求:控制是否生成帖子相关功能 实现思路:允许用户输入一个开关参数来控制帖子功能相关的文件是否生成,比如 PostController、PostService、PostMapper、PostMapper.xml、Post 实体类等。 通用能力:某个范围下的多个指定文件挖坑 => 绑定同个参数
为了实现这个需求,我们要同时对多个名称包含 Post
的文件进行处理。由于这些文件分散在不同的目录中,我们没有办法通过直接指定一个目录完成制作。单次制作时输入多个文件是可行的提效方式,但如果文件数量再多一些,依次去写文件的路径也会成为一种麻烦。
有没有更优雅的方式呢?
答案是肯定的,想想我们平时查找文件时会怎么办?肯定是输入一些关键词来过滤文件对吧。
没错,我们可以给模板制作工具增加 文件过滤 功能,通过多种不同的过滤方式帮助用户选择文件,更灵活地完成批量模板制作。
# 文件过滤机制设计
文件过滤可以有很多种不同的配置方式,梳理归纳了 2 类配置:
- 过滤范围:根据文件名称、或者文件内容过滤。
- 过滤规则:包含 contains、前缀匹配 startsWith、后缀匹配 endsWith、正则 regex、相等 equals。
由于制作工具已经支持输入多个文件 / 目录,所以其实每个文件 / 目录都可以指定自己的过滤规则,而且能同时指定多条过滤规则(必须同时满足才保留),进一步提高灵活性。
参考文件过滤机制的 JSON 结构如下:
{
"files": [
{
"path": "文件(目录)路径",
"filters": [
{
"range": "fileName",
"rule": "regex",
"value": ".*lala.*"
},
{
"range": "fileContent",
"rule": "contains",
"value": "haha"
}
]
}
],
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
上面的 JSON 表示:该文件的名称必须符合正则并且内容必须包含 haha
。
通过这种设计,可以非常灵活地筛选文件。如果想使用 or 逻辑(有一个过滤条件符合要求就保留),可以定义多个重复的 file,并且每个 file 指定一个过滤条件来实现。哪怕同时满足了多个过滤器,我们的去重逻辑也能搞定。
# 开发实现
1)设计好了机制后,我们可以编写对应的配置类。
在 template.model
包下新建 FileFilterConfig
类,对应上面设计好的 JSON 结构。
代码如下:
package com.yupi.maker.template.model;
import lombok.Builder;
import lombok.Data;
/**
* 文件过滤配置
*/
@Data
@Builder
public class FileFilterConfig {
/**
* 过滤范围
*/
private String range;
/**
* 过滤规则
*/
private String rule;
/**
* 过滤值
*/
private String value;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在 template.model
包下新建 TemplateMakerFileConfig
类,用来封装所有和文件相关的配置。
代码如下:
package com.yupi.maker.template.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@Data
public class TemplateMakerFileConfig {
private List<FileInfoConfig> files;
@NoArgsConstructor
@Data
public static class FileInfoConfig {
private String path;
private List<FileFilterConfig> filterConfigList;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2)针对过滤配置中的枚举值,编写对应的枚举类
在 template.enums
包下新建 FileFilterRangeEnum
枚举类,表示文件过滤范围枚举。需要提供根据 value 获取枚举的方法,便于根据字符串获取对应的枚举。
代码如下:
package com.yupi.maker.template.enums;
import cn.hutool.core.util.ObjectUtil;
import lombok.Getter;
/**
* 文件过滤范围枚举
*/
@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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
在 template.enums
包下再新建 FileFilterRuleEnum
枚举类,表示文件过滤规则枚举。
代码如下:
package com.yupi.maker.template.enums;
import cn.hutool.core.util.ObjectUtil;
import lombok.Getter;
/**
* 文件过滤规则枚举
*/
@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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
3)有了配置类后,我们来开发文件过滤功能
在 template
包下新建 FileFilter
类,首先开发针对单个文件过滤的方法 doSingleFileFilter
。实现思路是遍历传入的文件过滤配置列表,并按照规则进行校验,如果有一个过滤配置不满足,就返回 false 表示不保留该文件,反之为 true 表示通过所有校验。
代码如下:
/**
* 单个文件过滤
*
* @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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
然后编写过滤器的主方法 doFilter
,方法接受 filePath
文件路径参数,支持传入文件或目录,能够同时对多个文件进行过滤。
实现思路很简单,通过 Hutool 的 FileUtil.loopFiles
获取到所有文件列表,再依次调用过滤单个文件的方法即可。
即使传入的是单个文件,也能通过该方法获取到列表,保持一致
代码如下:
/**
* 对某个文件或目录进行过滤,返回文件列表
*
* @param filePath
* @param fileFilterConfigList
* @return
*/
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());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
FileFilter
的完整代码如下:
package com.yupi.maker.template;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import com.yupi.maker.template.enums.FileFilterRangeEnum;
import com.yupi.maker.template.enums.FileFilterRuleEnum;
import com.yupi.maker.template.model.FileFilterConfig;
import java.io.File;
import java.util.List;
import java.util.stream.Collectors;
/**
* 文件过滤器
*/
public class FileFilter {
/**
* 对某个文件或目录进行过滤,返回文件列表
*
* @param filePath
* @param fileFilterConfigList
* @return
*/
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
4)模板制作工具类使用过滤器
将 makeTemplate
方法接受的 inputFilePathList
参数改为我们新封装的 TemplateMakerFileConfig
类,相当于同时传入了文件列表和过滤规则。
public static long makeTemplate(Meta newMeta,
String originProjectPath,
TemplateMakerFileConfig templateMakerFileConfig,
Meta.ModelConfig.ModelInfo modelInfo,
String searchStr,
Long id) {}
2
3
4
5
6
然后修改遍历输入文件的代码,改为遍历 fileConfigInfoList
获取文件信息:
// 输入文件信息
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();
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
应用过滤器。将文件信息配置中的 相对路径转化为绝对路径 作为调用过滤器的参数,并通过过滤器获取到所有文件列表(注意,这里不可能是目录),再遍历文件列表来制作模板。
修改生成文件模板的部分代码,如下:
// 二、生成文件模板
// 遍历输入文件
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);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
5)在 main 方法中编写文件过滤测试代码,只处理 common 包下文件名称包含 Base
的文件和 controller 包下的文件。
在 main 方法的最后追加代码,如下:
// 文件过滤
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));
long id = makeTemplate(meta, originProjectPath, templateMakerFileConfig, modelInfo, searchStr, 1735281524670181376L);
System.out.println(id);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
执行测试,只有 BaseResponse.java
生成了模板文件,符合预期:
生成的元信息配置也没有多余的文件:
# 完整代码
TemplateMaker
的完整代码如下:
package com.yupi.maker.template;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.yupi.maker.meta.Meta;
import com.yupi.maker.meta.enums.FileGenerateTypeEnum;
import com.yupi.maker.meta.enums.FileTypeEnum;
import com.yupi.maker.template.enums.FileFilterRangeEnum;
import com.yupi.maker.template.enums.FileFilterRuleEnum;
import com.yupi.maker.template.model.FileFilterConfig;
import com.yupi.maker.template.model.TemplateMakerFileConfig;
import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class TemplateMaker {
/**
* 制作模板
*
* @param newMeta
* @param originProjectPath
* @param templateMakerFileConfig
* @param modelInfo
* @param searchStr
* @param id
* @return
*/
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;
}
/**
* 制作文件模板
*
* @param modelInfo
* @param searchStr
* @param sourceRootPath
* @param inputFile
* @return
*/
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;
}
public static void main(String[] args) {
Meta meta = new Meta();
meta.setName("acm-template-generator");
meta.setDescription("ACM 示例模板生成器");
String projectPath = System.getProperty("user.dir");
String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/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 = "Sum: ";
// 替换变量(第二次)
String searchStr = "BaseResponse";
// 文件过滤
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));
long id = makeTemplate(meta, originProjectPath, templateMakerFileConfig, modelInfo, searchStr, 1735281524670181376L);
System.out.println(id);
}
/**
* 模型去重
*
* @param modelInfoList
* @return
*/
private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(modelInfoList.stream().collect(Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)).values());
return newModelInfoList;
}
/**
* 文件去重
*
* @param fileInfoList
* @return
*/
private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(fileInfoList.stream().collect(Collectors.toMap(Meta.FileConfig.FileInfo::getInputPath, o -> o, (e, r) -> r)).values());
return newFileInfoList;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# 3、文件分组
目前,我们的制作工具已经支持对文件进行分组,并且通过给组设置 condition 的方式,支持用单个模型参数同时控制一组文件。
同样的,我们的模板制作工具也需要拥有快速生成文件组配置的能力。
# 实现思路
怎么实现呢?
我们现在已经能够单次制作多个文件了,而且根据用户习惯,同一次制作的多个文件更有可能属于同一组。那么其实我们不用让用户再手动配置如何分组了,可以自动分组。
有 2 种分组策略
- 一个文件信息配置(FileInfoConfig)对应一次分组。如果传入的 path 是目录,则目录下的所有文件为同组。
- 一个完整的文件配置(TemplateMakerFileConfig)对应一次分组。即配置 files 列表中的所有文件都属于同组。
这里选择第 2 种方案。原因是从需求出发,对于 “要用一个参数控制帖子相关的文件是否生成” 的需求,有可能要把跨目录下的文件设置为一个组。第 2 种方案会更灵活。
# 开发实现
下面我们来实现文件分组功能。
1)首先给 TemplateMakerFileConfig
增加分组配置,和之前 Meta
元信息实体类的分组字段一一对应。
代码如下:
package com.yupi.maker.template.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2)在 TemplateMaker
的 makeTemplate
方法中增加文件组相关代码,思路是将本次得到的所有文件信息都放到同一个分组下。
在 “生成配置文件” 前增加如下代码:
// 如果是文件组
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
3)在 main 方法中增加分组测试数据,代码如下:
// 分组配置
TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = new TemplateMakerFileConfig.FileGroupConfig();
fileGroupConfig.setCondition("outputText");
fileGroupConfig.setGroupKey("test");
fileGroupConfig.setGroupName("测试分组");
templateMakerFileConfig.setFileGroupConfig(fileGroupConfig);
2
3
4
5
6
执行,成功生成分组配置:
# 追加配置能力
虽然已经实现了文件分组配置的生成,但是实际制作模板的过程中,我们可能没办法一步到位,而是希望能多次制作模板,并将文件追加到之前已有的分组下。
也就是说,文件分组要能支持多次制作时追加配置的能力,可以增加新的分组、也可以在同分组下新增文件。
1)让我们先来明确下需求。
举个例子,假设第 1 次制作得到的分组配置如下:
{
"files": [
{
"type": "group",
"condition": "outputText",
"groupKey": "test",
"groupName": "测试分组",
"files": [
{
"inputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java",
"outputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java.ftl",
"type": "file",
"generateType": "dynamic"
},
{
"inputPath": "src/main/java/com/yupi/springbootinit/controller/PostController.java",
"outputPath": "src/main/java/com/yupi/springbootinit/controller/PostController.java.ftl",
"type": "file",
"generateType": "dynamic"
},
{
"inputPath": "src/main/java/com/yupi/springbootinit/controller/UserController.java",
"outputPath": "src/main/java/com/yupi/springbootinit/controller/UserController.java.ftl",
"type": "file",
"generateType": "dynamic"
}
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
第 2 次制作得到的分组配置如下,新增了 ResultUtils.java
文件:
{
"files": [
{
"type": "group",
"condition": "outputText",
"groupKey": "test",
"groupName": "测试分组",
"files": [
{
"inputPath": "src/main/java/com/yupi/springbootinit/common/ResultUtils.java",
"outputPath": "src/main/java/com/yupi/springbootinit/common/ResultUtils.java.ftl",
"type": "file",
"generateType": "dynamic"
},
{
"inputPath": "src/main/java/com/yupi/springbootinit/controller/PostController.java",
"outputPath": "src/main/java/com/yupi/springbootinit/controller/PostController.java.ftl",
"type": "file",
"generateType": "dynamic"
}
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
由于两个分组的 groupKey
相同,视为同一个组,需要将第 2 次得到的分组内的所有文件和之前分组内的文件进行合并去重。得到如下配置:
{
"files": [
{
"type": "group",
"condition": "outputText",
"groupKey": "test",
"groupName": "测试分组",
"files": [
{
"inputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java",
"outputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java.ftl",
"type": "file",
"generateType": "dynamic"
},
{
"inputPath": "src/main/java/com/yupi/springbootinit/controller/PostController.java",
"outputPath": "src/main/java/com/yupi/springbootinit/controller/PostController.java.ftl",
"type": "file",
"generateType": "dynamic"
},
{
"inputPath": "src/main/java/com/yupi/springbootinit/controller/UserController.java",
"outputPath": "src/main/java/com/yupi/springbootinit/controller/UserController.java.ftl",
"type": "file",
"generateType": "dynamic"
},
{
"inputPath": "src/main/java/com/yupi/springbootinit/common/ResultUtils.java",
"outputPath": "src/main/java/com/yupi/springbootinit/common/ResultUtils.java.ftl",
"type": "file",
"generateType": "dynamic"
}
]
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2)明确需求后,让我们来梳理下文件分组去重的实现流程,分为以下步骤:
- 将所有文件配置(fileInfo)分为有分组的和无分组的
- 对于有分组的文件配置,如果有相同的分组,同分组内的文件进行合并(merge),不同分组可同时保留
- 创建新的文件配置列表(结果列表),先将合并后的分组添加到结果列表
- 再将无分组的文件配置列表添加到结果列表
3)根据上述步骤,编写代码实现。
修改后的 distinctFiles
方法代码如下:
/**
* 文件去重
*
* @param fileInfoList
* @return
*/
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
4)测试验证
修改 main 方法中的 inputFilePath2
,指定一个新的目录:
String inputFilePath2 = "src/main/java/com/yupi/springbootinit/constant";
基于之前制作好的模板再次执行,发现第 2 次新增的文件合并到了之前的分组配置中,符合预期:8J8aZQ9pRnWasqusHGea5EeJjD5lqENi9OUTo+mr6wM=
你还可以试着修改分组的名称、条件等配置,新的配置会覆盖之前的配置。
# 完整代码
TemplateMaker
的完整代码如下:
package com.yupi.maker.template;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.yupi.maker.meta.Meta;
import com.yupi.maker.meta.enums.FileGenerateTypeEnum;
import com.yupi.maker.meta.enums.FileTypeEnum;
import com.yupi.maker.template.enums.FileFilterRangeEnum;
import com.yupi.maker.template.enums.FileFilterRuleEnum;
import com.yupi.maker.template.model.FileFilterConfig;
import com.yupi.maker.template.model.TemplateMakerFileConfig;
import java.io.File;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
public class TemplateMaker {
/**
* 制作模板
*
* @param newMeta
* @param originProjectPath
* @param templateMakerFileConfig
* @param modelInfo
* @param searchStr
* @param id
* @return
*/
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;
}
/**
* 制作文件模板
*
* @param modelInfo
* @param searchStr
* @param sourceRootPath
* @param inputFile
* @return
*/
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;
}
public static void main(String[] args) {
Meta meta = new Meta();
meta.setName("acm-template-generator");
meta.setDescription("ACM 示例模板生成器");
String projectPath = System.getProperty("user.dir");
String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/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 = "Sum: ";
// 替换变量(第二次)
String searchStr = "BaseResponse";
// 文件过滤
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, modelInfo, searchStr, 1735281524670181376L);
System.out.println(id);
}
/**
* 模型去重
*
* @param modelInfoList
* @return
*/
private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(modelInfoList.stream().collect(Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)).values());
return newModelInfoList;
}
/**
* 文件去重
*
* @param fileInfoList
* @return
*/
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# 4、模型分组
和文件分组一样,之前我们的制作工具已经实现了模型分组的能力。那现在我们的模板制作工具也需要能够同时指定多个模型参数进行 “挖坑”,并生成模型分组配置。
# 实现思路
模型分组的实现思路和文件分组逻辑几乎一致,此处不再赘述。
但有一点需要注意,之前我们在测试模板制作工具时,传入的都是单个模型参数和要替换的字符串参数(searchStr)。但现在如果要一次性输入多个模型参数,也要传入多个要替换的字符串。准确地说,每个模型和要替换的字符串参数应该一一对应。所以我们需要用额外的类来封装这些参数。
# 开发实现
1)首先像封装文件配置类(TemplateMakerFileConfig)一样,封装所有模型参数、分组参数为模型配置类 TemplateMakerModelConfig
,放到 template.model
目录下。
代码如下:
package com.yupi.maker.template.model;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2)修改模型去重方法,和文件去重逻辑一致,实现同组模型信息的去重合并。
模型去重方法的代码如下:
/**
* 模型去重
*
* @param modelInfoList
* @return
*/
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;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
3)修改 makeTemplate
的输入参数,使用封装好的模型配置类 TemplateMakerModelConfig
代替 modelInfo 和 searchStr。8J8aZQ9pRnWasqusHGea5EeJjD5lqENi9OUTo+mr6wM=
修改后的方法参数如下:
public static long makeTemplate(Meta newMeta,
String originProjectPath,
TemplateMakerFileConfig templateMakerFileConfig,
TemplateMakerModelConfig templateMakerModelConfig,
Long id) {}
2
3
4
5
由于参数修改了,我们也要像从文件配置中读取信息一样,从模型配置中读出分组和模型列表信息,并转换为用于生成元信息配置的 newModelInfoList
。
代码如下:
// 处理模型信息
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
之前生成元信息配置,是增加了单个 ModelInfo 对象,现在需要改为增加 newModelInfoList
列表:
// 1. 追加配置参数
List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
fileInfoList.addAll(newFileInfoList);
List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
modelInfoList.addAll(newModelInfoList);
2
3
4
5
4)修改 makeFileTemplate
方法,要能够支持使用多个模型参数对文件 “挖坑”。
实现思路是依次遍历模型参数,对文件内容进行替换,将上一轮替换后的结果作为新一轮要替换的内容,从而实现多轮替换。
先调整方法的输入参数,以及调用该方法时传入的参数:
private static Meta.FileConfig.FileInfo makeFileTemplate(
TemplateMakerModelConfig templateMakerModelConfig,
String sourceRootPath,
File inputFile) {}
2
3
4
然后实现多轮替换逻辑,修改的关键代码如下:
// 支持多个模型:对同一个文件的内容,遍历模型进行多轮替换
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);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
5)测试验证,定义一组能够替换 MySQL 配置的模型组参数,用来替换 application.yml
配置文件。
代码如下:
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);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
执行,成功制作出了有多个参数的模板文件:
也成功生成了元信息文件:
可以尝试更换模型参数组的 groupKey 或模型的 fieldName,测试能够正常追加模型配置。
至此,模型分组能力开发完成。
# 完整代码
TemplateMaker
的完整代码如下:
package com.yupi.maker.template;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.yupi.maker.meta.Meta;
import com.yupi.maker.meta.enums.FileGenerateTypeEnum;
import com.yupi.maker.meta.enums.FileTypeEnum;
import com.yupi.maker.template.enums.FileFilterRangeEnum;
import com.yupi.maker.template.enums.FileFilterRuleEnum;
import com.yupi.maker.template.model.FileFilterConfig;
import com.yupi.maker.template.model.TemplateMakerFileConfig;
import com.yupi.maker.template.model.TemplateMakerModelConfig;
import java.io.File;
import java.nio.file.Paths;
import java.util.*;
import java.util.stream.Collectors;
public class TemplateMaker {
/**
* 制作模板
*
* @param newMeta
* @param originProjectPath
* @param templateMakerFileConfig
* @param templateMakerModelConfig
* @param id
* @return
*/
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;
}
/**
* 制作文件模板
*
* @param templateMakerModelConfig
* @param sourceRootPath
* @param inputFile
* @return
*/
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;
}
public static void main(String[] args) {
Meta meta = new Meta();
meta.setName("acm-template-generator");
meta.setDescription("ACM 示例模板生成器");
String projectPath = System.getProperty("user.dir");
String originProjectPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/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);
// 替换变量(首次)
// String searchStr = "Sum: ";
// 替换变量(第二次)
String searchStr = "BaseResponse";
// 文件过滤
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, 1735281524670181376L);
System.out.println(id);
}
/**
* 模型去重
*
* @param modelInfoList
* @return
*/
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;
}
/**
* 文件去重
*
* @param fileInfoList
* @return
*/
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;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# 最后
以上就是本节教程,我们给制作工具项目增加了模板制作功能,能够根据用户指定的文件和模型参数快速生成模板和元信息配置。并且支持了单次制作多个模板、文件过滤、文件分组、模型分组等多个功能。
本节教程涉及到大量的编码,在编码中,其实蕴含了很多小技巧,比如 Lambda 表达式编程、复用变量、抽象封装方法等。建议大家自己实现这些代码,锻炼一下自己的逻辑思维能力。
在下节教程中,我们将进一步完善模板制作工具,并且使用本阶段开发的制作工具,来快速制作 Spring Boot 项目模板代码生成器。
- 01
- RTC实时时钟 原创02-12
- 02
- keil调试 原创01-21
- 03
- GPIO概述与配置 原创01-20
- 04
- element-plus多文件手动上传 原创11-03
- 05
- TrueLicense 创建及安装证书 原创10-25