第七章 模板制作工具
第七章 模板制作工具
创建时间: 2025年4月21日 09:32
状态: Done
前言
代码地址:https://github.com/Liucc-123/yuzi-generator.git
项目教程:https://www.codefather.cn/course/1790980795074654209
上一章节内容:https://blog.csdn.net/weixin_45284646/article/details/147668585?spm=1011.2415.3001.5331
本节重点
本节教程属于项目的第二阶段 —— 开发代码生成器制作工具。
本节主要是开发模板制作工具,这一期的教程和代码甚至可以单独拿出来作为一个 快速挖坑工具
小项目了!
重点内容:
- 模板制作工具 - 需求分析
- 模板制作工具 - 核心设计
- 模板制作工具 - 基础功能实现
- 模板制作工具 - 更多功能实现
一、需求分析
上一章节中我们遇到了一个问题:当元信息数据模型属性发生变更后,我们之前编写的FreeMarker动态模板就无法正确生成内容了。因为模型参数发生更改,模板就无法正确获取模型参数的值了。也就是模板文件和数据模型是强绑定的,稍有遗漏,就会导致代码生成器无法正常工作。
此外,上一章节中还以留了一个需求:替换生成的代码包名。
对于SpringBoot项目,里面用到包名的地方实在是太多了,如果每个文件都是自己去“挖坑”来制作模板文件,成本就太大了,也很容易出现遗漏。
也就是说,现在的代码生成器仍存在2大问题:
- 需要根据动态模板编写对应的配置,参数越多,出现和模板不一致的风险就越大
- 需要人工提前准备好动态模板,项目文件越多,制作成本就越大
如何解决呢?
可以让制作工具根据我们的想法,自动给项目文件“挖坑”,并生成相互对应的动态模板文件和元信息配置文件。提高效率的同时,还能减少模板文件和模型参数不一致的风险。
这也是本章节要完成的需求。
注意:制作工具的作用只是提高效率,并不能覆盖所有定制需求!
二、核心设计
先思考如何实现上面的需求?
程序的本质就是代替人工完成所要操作的事情。因此想让程序自动制作模板文件和生成配置,我们先回忆一下,我们都做了哪些事情:
1、先指定一个原始、待“挖坑”的输入文件
2、明确文件中需要动态替换的内容和模型参数
3、自己编写FreeMarker FTL动态模板文件
4、自己编写生成器的元信息配置文件,包括基本信息、文件配置、模型参数配置
分析上面的步骤,其中1-2步都是需要用户自主确认的内容,制作工具无法插手,但是有了1-2步的确认后,3-4步就可以交给制作工具来完成了。
由此,我们就可以快速制作模板的基本公式:
- 向制作工具输入:基本信息、输入文件、模型参数(+输出规则)
- 由制作工具输出:模板文件、元信息配置
跟编写算法题目一样,先确定算法的输入输出,再去设计实现算法
对应的算法流程图如下:
参数解释:
- 基础信息:代码生成器的基本信息,比如项目名称、描述、版本号、作者等基础信息
- 输入文件:待“挖坑”的原始模板文件。可以是多个文件。
- 模型参数:引导用户输入并填充到模板文件中的参数,对应的就是元信息配置里的modelConfig
- 输出规则:这是一个可扩展项,比如多次制作时采取覆盖还是追加策略等
输出参数就好理解了:在指定目录位置生成FTL模板文件和元信息配置文件。
明确了核心设计后,就可以着手开始开发实现了,我们先从一个最基础的模板制作工具实现开始,然后再陆续给工具增加功能。
小技巧:开发复杂需求或新项目时,先一切从简,完成核心流程的开发。在这个过程中可以记录想法和扩展思路,后面再按需实现。
三、基础功能实现
首先打开制作工具项目(maker),在maker包下新建template
包,所有和模板制作相关的代码后面都会放到这个包下,实现功能隔离。
目前项目中maker.model
目录和FileGenerator
中的main
方法是多余的,可以删掉
1、基本流程实现
现在继续以ACM示例模板为例,编写模板制作工具的基本流程代码。
我们的期望是:以ACM示例模板项目为根目录,使用模型参数outputText
替换MainTemplate
文件中的Sum:
输出信息,并且在同包下生成FTL模板文件,以及在项目根目录下生成元信息配置文件meta.json。
实现步骤如下:
- 提供输入参数:包括有项目基础信息、原始项目目录、输入模型参数信息
- 基于字符串替换算法,使用模型参数提供的字段信息替换原始文件
MainTemplate
中的输出信息 - 基于输入信息,构造Meta实体类,通过hutool工具将其转为json,并写入到指定文件中
在template
包下新建TemplateMaker
类,在其main
方法中逐步实现以上步骤:
1)输入信息
示例代码如下(注意Windows的文件路径需要转义问题):
// 一、输入信息
// 1、输入项目基础信息
String projectName = "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";
// windows系统需要对文件路径进行转移
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
String fileInputPath = "src/com/liucc/acm/MainTemplate.java";
String fileOutputPath = fileInputPath + ".ftl";
// 3、输入模型参数信息
Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();
modelInfo.setFieldName("outputText");
modelInfo.setDefaultValue("Sum: ");
2)使用字符串替换算法,生成模板文件
// 二、使用字符串替换算法,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newContent = StrUtil.replace(originalContent, "Sum: ", replacement);
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);
3)生成配置文件
思路是先基于输入信息构造一个Meta实体类,再通过hutool API将其转为json字符串,最后再写入到meta.json文件中。
// 三、生成元信息配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";
// 1、构造配置参数
Meta meta = new Meta();
meta.setName(projectName);
meta.setDescription(description);Meta.FileConfigDTO fileConfig = new Meta.FileConfigDTO();
meta.setFileConfig(fileConfig);
fileConfig.setSourceRootPath(sourceRootPath);
fileConfig.setType("dir");
List<Meta.FileConfigDTO.FileInfo> fileInfoList = new ArrayList<>();
Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getType());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());
fileInfoList.add(fileInfo);
fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();
List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();
modelInfoList.add(modelInfo);
modelConfig.setModels(modelInfoList);
meta.setModelConfig(modelConfig);
// 2、输出元信息配置文件
String metaJson = JSONUtil.toJsonPrettyStr(meta);
FileUtil.writeUtf8String(metaJson, metaOutputPath);
最终的完整代码如下:
package com.liucc.maker.template;import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.liucc.maker.meta.Meta;
import com.liucc.maker.meta.enums.FileGenerateTypeEnum;
import com.liucc.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) {System.out.println("模板生成器工作中...");// 一、输入信息// 1、输入项目基础信息String projectName = "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";// windows系统需要对文件路径进行转移sourceRootPath = sourceRootPath.replace("\\", "/");String fileInputPath = "src/com/liucc/acm/MainTemplate.java";String fileOutputPath = fileInputPath + ".ftl";// 3、输入模型参数信息Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();modelInfo.setFieldName("outputText");modelInfo.setDefaultValue("Sum: ");// 二、使用字符串替换算法,生成模板文件String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;String originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);String replacement = String.format("${%s}", modelInfo.getFieldName());String newContent = StrUtil.replace(originalContent, "Sum: ", replacement);String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);// 三、生成元信息配置文件String metaOutputPath = sourceRootPath + File.separator + "meta.json";// 1、构造配置参数Meta meta = new Meta();meta.setName(projectName);meta.setDescription(description);Meta.FileConfigDTO fileConfig = new Meta.FileConfigDTO();meta.setFileConfig(fileConfig);fileConfig.setSourceRootPath(sourceRootPath);fileConfig.setType("dir");List<Meta.FileConfigDTO.FileInfo> fileInfoList = new ArrayList<>();Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();fileInfo.setInputPath(fileInputPath);fileInfo.setOutputPath(fileOutputPath);fileInfo.setType(FileTypeEnum.FILE.getType());fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());fileInfoList.add(fileInfo);fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();modelInfoList.add(modelInfo);modelConfig.setModels(modelInfoList);meta.setModelConfig(modelConfig);// 2、输出元信息配置文件String metaJson = JSONUtil.toJsonPrettyStr(meta);FileUtil.writeUtf8String(metaJson, metaOutputPath);System.out.println("模板生成器制作完成~");}
}
运行main方法,测试执行,成功生成所需要的文件:
2、工作空间隔离
目前制作工具生成的代码是直接生成到原始项目下的,这种方式可能会对原项目造成污染。一种解决方案就是通过工作空间进行隔离,这也是很多知名中间件都会采用的方式,比如FreeMarker、Nacos等。
因此我们每次制作模板时,不直接修改原始项目的任何文件,而是先复制原项目到一个临时目录下,然后在该临时目录下完成文件的生成和处理。
我们把这个临时目录成为工作空间
,每次模板制作都应该属于不同的工作空间,相互隔离、互不影响。
约定将maker
项目下的.temp
目录作为工作空间,在.gitignore文件中忽略该目录。
在TemplateMaker
添加新逻辑:
- 拷贝原始示例模板项目到工作空间中
- 使用hutool的雪花算法生成唯一id表示工作空间进行隔离
- 修改变量
sourceRootPath
的值为复制后工作空间内的项目根目录
修改后的代码如下:
public static void main(String[] args) {System.out.println("模板生成器工作中...");// 拷贝原始项目到临时目录(工作空间)中String projectPath = System.getProperty("user.dir");String sourceRootPath = new File(projectPath).getParent() +File.separator + "yuzi-generator-demo-projects/acm-template";long id = IdUtil.getSnowflakeNextId();String copyDestPath = projectPath + File.separator + ".temp/" + id;if (!FileUtil.exist(copyDestPath)){FileUtil.mkdir(copyDestPath);}FileUtil.copy(sourceRootPath, copyDestPath, true);// 一、输入信息// 1、输入项目基础信息String projectName = "acm-template-generator";String description = "ACM模板生成器";// 2、输入文件信息(相对路径)sourceRootPath = copyDestPath + File.separator + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).toString();// windows系统需要对文件路径进行转移sourceRootPath = sourceRootPath.replace("\\", "/");String fileInputPath = "src/com/liucc/acm/MainTemplate.java";String fileOutputPath = fileInputPath + ".ftl";// 3、输入模型参数信息Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();modelInfo.setFieldName("outputText");modelInfo.setDefaultValue("Sum: ");// 二、使用字符串替换算法,生成模板文件String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;String originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);String replacement = String.format("${%s}", modelInfo.getFieldName());String newContent = StrUtil.replace(originalContent, "Sum: ", replacement);String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);// 三、生成元信息配置文件String metaOutputPath = sourceRootPath + File.separator + "meta.json";// 1、构造配置参数Meta meta = new Meta();meta.setName(projectName);meta.setDescription(description);Meta.FileConfigDTO fileConfig = new Meta.FileConfigDTO();meta.setFileConfig(fileConfig);fileConfig.setSourceRootPath(sourceRootPath);fileConfig.setType("dir");List<Meta.FileConfigDTO.FileInfo> fileInfoList = new ArrayList<>();Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();fileInfo.setInputPath(fileInputPath);fileInfo.setOutputPath(fileOutputPath);fileInfo.setType(FileTypeEnum.FILE.getType());fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());fileInfoList.add(fileInfo);fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();modelInfoList.add(modelInfo);modelConfig.setModels(modelInfoList);meta.setModelConfig(modelConfig);// 2、输出元信息配置文件String metaJson = JSONUtil.toJsonPrettyStr(meta);FileUtil.writeUtf8String(metaJson, metaOutputPath);System.out.println("模板生成器制作完成~");}
测试执行,成功在工作空间内生成了模板文件和配置文件。
3、分布制作能力
一般来说,在制作模板时,不能只挖一个坑,只允许用户填写一个参数,也不会一次性挖所有坑,而是类似于填写模板,一步一步地替换参数、制作模板。
因此,我们的制作工具要具备分布制作、追加配置的能力,具体要做到以下三点:
- 输入过的信息,不用重复输入,比如项目的基础信息
- 后续制作时,不用重复复制原始项目;可以在原有文件的基础上覆盖或追加新的文件
- 后续制作时,可以在原有配置的基础上,覆盖或追加新的配置
想要实现这个能力,需要让我们的制作工具“有状态”
有状态和无状态
有状态:服务器端在多次会话中会记录客户端的状态。比如每次请求都会保存用户上下文、用户登录信息等;
无状态:服务器端不会保存客户端状态,不管用户发送来多少次请求,都和第一次请求是一样的。
有状态实现
要实现有状态,需要2个因素:唯一标识和存储。
在上一步“工作空间隔离”中我们使用id进行隔离,这个id已经具备唯一性了;而我们使用id作为目录,这个文件系统也就充当了存储的作用。因此我们的制作工具已经具备有状态了。
我们只需要在第一次制作工具时,生成唯一的id,后续制作时,将id作为参数传递进来,就能找到同一个工作空间,从而进行追加配置或文件。
修改template.TemplateMaker
文件,添加新方法makeTemplate,id作为参数,id不存在,表示首次制作模板;id存在,更新模板。示例代码如下:
/*** 制作模板** @param id id不存在,表示首次制作模板;id存在,更新模板* @return*/
private static Long makeTemplate(Long id) {if (id == null) {id = IdUtil.getSnowflakeNextId();}
}
分布制作实现
如果判断出是非首次制作,我们该做哪些处理呢?
- 非首次制作,不需要重复拷贝原始项目文件
- 非首次制作,可以在已有模板文件的基础上再次挖坑
- 非首次制作,不需要重复输入已有的元信息,而是在此基础上,可以覆盖或追加新的元信息配置
代码实现
1)非首次制作,不需要重复拷贝原始项目文件
if (id == null) {id = IdUtil.getSnowflakeNextId();
}
System.out.println("模板生成器工作中...");
// 拷贝原始项目到临时目录(工作空间)中
String projectPath = System.getProperty("user.dir");
String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";
String copyDestPath = projectPath + File.separator + ".temp/" + id;
// 非首次制作,不需要重复拷贝原始项目文件
if (!FileUtil.exist(copyDestPath)) {FileUtil.mkdir(copyDestPath);FileUtil.copy(sourceRootPath, copyDestPath, true);
}
2)非首次制作,可以在已有模板文件的基础上再次挖坑
判断ftl文件是否存在,如果已经存在,说明不是第一次制作,就可以将ftl文件的内容作为originalContent
// 二、使用字符串替换算法,生成模板文件
String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
String originalContent;
// 非首次制作,可以在已有模板文件的基础上再次挖坑
if (FileUtil.exist(fileOutputAbsolutePath)) {originalContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
} else {originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);
}
String replacement = String.format("${%s}", modelInfo.getFieldName());
String newContent = StrUtil.replace(originalContent, "Sum: ", replacement);
FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);
3)非首次制作,不需要重复输入已有的元信息,而是在此基础上,可以覆盖或追加新的元信息配置
和判断文件模板是否重复一样,这里我们通过判断meta.json是否存在来区分是不是第一次制作。
// 三、生成元信息配置文件
String metaOutputPath = sourceRootPath + File.separator + "meta.json";
Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();
fileInfo.setInputPath(fileInputPath);
fileInfo.setOutputPath(fileOutputPath);
fileInfo.setType(FileTypeEnum.FILE.getType());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());
// 非首次制作,不需要重复输入已有的元信息,而是在此基础上,可以覆盖或追加元信息配置
if (FileUtil.exist(metaOutputPath)) {// 1、构造配置参数Meta meta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);// 文件配置List<Meta.FileConfigDTO.FileInfo> fileInfoList = meta.getFileConfig().getFiles();fileInfoList.add(fileInfo);// 模型配置List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = meta.getModelConfig().getModels();// 2、输出元信息配置文件FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);System.out.println("模板生成器制作完成~");
} else {// 1、构造配置参数Meta meta = new Meta();meta.setName(projectName);meta.setDescription(description);Meta.FileConfigDTO fileConfig = new Meta.FileConfigDTO();meta.setFileConfig(fileConfig);fileConfig.setSourceRootPath(sourceRootPath);fileConfig.setType("dir");List<Meta.FileConfigDTO.FileInfo> fileInfoList = new ArrayList<>();fileInfoList.add(fileInfo);fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();modelInfoList.add(modelInfo);modelConfig.setModels(modelInfoList);meta.setModelConfig(modelConfig);// 2、输出元信息配置文件String metaJson = JSONUtil.toJsonPrettyStr(meta);FileUtil.writeUtf8String(metaJson, metaOutputPath);System.out.println("模板生成器制作完成~");
}
这里实际运行后会发现一个问题:meta.json
文件中modelConfig
和fileConfig
会出现重复。因此我们需要修改原先代码,对fileInfoList和modelInfoList 进行去重,再添加到meta中。
文件去重
文件信息根据inputPath去重,采取新值覆盖旧值策略。代码如下:
/*** 文件去重* @param fileInfoList* @return*/
private static List<Meta.FileConfigDTO.FileInfo> distinctFiles(List<Meta.FileConfigDTO.FileInfo> fileInfoList){if (CollUtil.isEmpty(fileInfoList)){return fileInfoList;}Collection<Meta.FileConfigDTO.FileInfo> values = fileInfoList.stream().collect(Collectors.toMap(Meta.FileConfigDTO.FileInfo::getInputPath,Function.identity(), (existing, replacement) -> replacement)).values();List<Meta.FileConfigDTO.FileInfo> distinctList = new ArrayList<>(values);return distinctList;
}
这里使用Java8 Stream API和lambda表达式简化代码,Collectors.toMap方法将收集来的数据转化为一个map,这里解释一下这个方法
- 第一个参数(
Meta.FileConfigDTO.FileInfo::getInputPath
):是将FileInfo对象里的inputPath字段值作为map的key - 第二个参数(
Function.identity()
):表示FileInfo对象本身,也就是将FileInfo对象本身作为map的value - 第三个参数(
(existing, replacement) -> replacement)
):这里表示当发生map的key冲突时采取的合并策略,existing
是旧值,replacement
是新值,→右边的参数表示最后到底是返回旧值还是新值。
模型配置去重
和文件去重几乎一样,示例代码如下:
/*** 配置去重* @param modelInfoList* @return*/
private static List<Meta.ModelConfigDTO.ModelInfo> distinctModels(List<Meta.ModelConfigDTO.ModelInfo> modelInfoList){if (CollUtil.isEmpty(modelInfoList)){return modelInfoList;}Collection<Meta.ModelConfigDTO.ModelInfo> values = modelInfoList.stream().collect(Collectors.toMap(Meta.ModelConfigDTO.ModelInfo::getFieldName,Function.identity(), (existing, replacement) -> replacement)).values();List<Meta.ModelConfigDTO.ModelInfo> distinctList = new ArrayList<>(values);return distinctList;
}
最后修改makeTemplate方法,在生成元信息配置文件之前,进行去重,示例代码如下:
// 非首次制作,不需要重复输入已有的元信息,而是在此基础上,可以覆盖或追加元信息配置
if (FileUtil.exist(metaOutputPath)) {// 1、构造配置参数Meta meta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);// 文件配置List<Meta.FileConfigDTO.FileInfo> fileInfoList = meta.getFileConfig().getFiles();fileInfoList.add(fileInfo);// 模型配置List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = meta.getModelConfig().getModels();modelInfoList.add(modelInfo);// 文件去重meta.getFileConfig().setFiles(distinctFiles(fileInfoList));// 配置去重meta.getModelConfig().setModels(distinctModels(modelInfoList));// 2、输出元信息配置文件FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(meta), metaOutputPath);System.out.println("模板生成器制作完成~");
}
抽象方法
由于在接下来的测试中,我们需要多次测试第一次使用制作工具、第二次使用制作工具进行追加,因此一些参数我们需要从方法中提取出来,放到main方法中定义。将makeTemplate
方法中的硬编码进行重构。
将所有基本信息配置直接用Meta类封装:
String name = "acm-template-generator";
String description = "ACM 示例模板生成器";
改为:
// 项目基础信息
Meta meta = new Meta();
String projectName = "acm-template-generator";
String description = "ACM模板生成器";
meta.setName(projectName);
meta.setDescription(description);
最后抽取方法后的完整代码如下:
private static Long makeTemplate(Long id, String sourceRootPath, Meta meta, String fileInputPath, Meta.ModelConfigDTO.ModelInfo modelInfo, String searchStr) {if (id == null) {id = IdUtil.getSnowflakeNextId();}// 拷贝原始项目到临时目录(工作空间)中String projectPath = System.getProperty("user.dir");String copyDestPath = projectPath + File.separator + ".temp/" + id;// 非首次制作,不需要重复拷贝原始项目文件if (!FileUtil.exist(copyDestPath)) {FileUtil.mkdir(copyDestPath);FileUtil.copy(sourceRootPath, copyDestPath, true);}// 一、输入信息// 2、输入文件信息(相对路径)sourceRootPath = copyDestPath + File.separator + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).toString();// windows系统需要对文件路径进行转移sourceRootPath = sourceRootPath.replace("\\", "/");String fileOutputPath = fileInputPath + ".ftl";// 二、使用字符串替换算法,生成模板文件String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;String originalContent;// 非首次制作,可以在已有模板文件的基础上再次挖坑if (FileUtil.exist(fileOutputAbsolutePath)) {originalContent = FileUtil.readUtf8String(fileOutputAbsolutePath);} else {originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);}String replacement = String.format("${%s}", modelInfo.getFieldName());String newContent = StrUtil.replace(originalContent, searchStr, replacement);FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);// 三、生成元信息配置文件String metaOutputPath = sourceRootPath + File.separator + "meta.json";Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();fileInfo.setInputPath(fileInputPath);fileInfo.setOutputPath(fileOutputPath);fileInfo.setType(FileTypeEnum.FILE.getType());fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());// 非首次制作,不需要重复输入已有的元信息,而是在此基础上,可以覆盖或追加元信息配置if (FileUtil.exist(metaOutputPath)) {// 1、构造配置参数Meta newMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);// 文件配置List<Meta.FileConfigDTO.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();fileInfoList.add(fileInfo);// 模型配置List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();modelInfoList.add(modelInfo);// 文件去重newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));// 配置去重newMeta.getModelConfig().setModels(distinctModels(modelInfoList));// 2、输出元信息配置文件FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);} else {// 1、构造配置参数Meta.FileConfigDTO fileConfig = new Meta.FileConfigDTO();meta.setFileConfig(fileConfig);fileConfig.setSourceRootPath(sourceRootPath);fileConfig.setType("dir");List<Meta.FileConfigDTO.FileInfo> fileInfoList = new ArrayList<>();fileInfoList.add(fileInfo);fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();modelInfoList.add(modelInfo);modelConfig.setModels(modelInfoList);meta.setModelConfig(modelConfig);// 2、输出元信息配置文件String metaJson = JSONUtil.toJsonPrettyStr(meta);FileUtil.writeUtf8String(metaJson, metaOutputPath);}return id;
}
测试
编写main方法,指定2套不同的模型参数和替换变量作为测试数据,示例代码如下:
public static void main(String[] args) {String projectPath = System.getProperty("user.dir");String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";// 项目基础信息Meta meta = new Meta();String projectName = "acm-template-generator";String description = "ACM模板生成器";meta.setName(projectName);meta.setDescription(description);// 输入文件路径(相对路径)String fileInputPath = "src/com/liucc/acm/MainTemplate.java";// 模型参数信息(首次)
// Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();
// modelInfo.setFieldName("outputText");
// modelInfo.setDefaultValue("Sum: ");// 模型参数信息(第二次)Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();modelInfo.setFieldName("className");// 替换变量(首次)
// String str = "Sum: ";// 替换变量(第二次)String str = "MainTemplate";Long id = makeTemplate(1914264474699747328L, sourceRootPath, meta, fileInputPath, modelInfo, str);System.out.println("id = " + id);
}
第一执行,id参数给null,表示第一次执行制作工具。效果:成功在.temp目录下生成了目标代码文件
最后控制台会输出本次的工作空间id
第二次执行,将第一次执行中控制台打印的id
参数值传递给makeTemplate
方法。检查效果:模板文件成功挖取了第二个参数,且meta.json文件中多了第二个模型配置参数:
完整代码
package com.liucc.maker.template;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.liucc.maker.meta.Meta;
import com.liucc.maker.meta.enums.FileGenerateTypeEnum;
import com.liucc.maker.meta.enums.FileTypeEnum;import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;/*** 模板生成器,生成动态模板文件和元信息配置文件*/
public class TemplateMaker {/*** 制作模板** @param id id不存在,表示首次制作模板;id存在,更新模板* @return*/private static Long makeTemplate(Long id, String sourceRootPath, Meta meta, String fileInputPath, Meta.ModelConfigDTO.ModelInfo modelInfo, String searchStr) {if (id == null) {id = IdUtil.getSnowflakeNextId();}// 拷贝原始项目到临时目录(工作空间)中String projectPath = System.getProperty("user.dir");String copyDestPath = projectPath + File.separator + ".temp/" + id;// 非首次制作,不需要重复拷贝原始项目文件if (!FileUtil.exist(copyDestPath)) {FileUtil.mkdir(copyDestPath);FileUtil.copy(sourceRootPath, copyDestPath, true);}// 一、输入信息// 2、输入文件信息(相对路径)sourceRootPath = copyDestPath + File.separator + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).toString();// windows系统需要对文件路径进行转移sourceRootPath = sourceRootPath.replace("\\", "/");String fileOutputPath = fileInputPath + ".ftl";// 二、使用字符串替换算法,生成模板文件String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;String originalContent;// 非首次制作,可以在已有模板文件的基础上再次挖坑if (FileUtil.exist(fileOutputAbsolutePath)) {originalContent = FileUtil.readUtf8String(fileOutputAbsolutePath);} else {originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);}String replacement = String.format("${%s}", modelInfo.getFieldName());String newContent = StrUtil.replace(originalContent, searchStr, replacement);FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);// 三、生成元信息配置文件String metaOutputPath = sourceRootPath + File.separator + "meta.json";Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();fileInfo.setInputPath(fileInputPath);fileInfo.setOutputPath(fileOutputPath);fileInfo.setType(FileTypeEnum.FILE.getType());fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());// 非首次制作,不需要重复输入已有的元信息,而是在此基础上,可以覆盖或追加元信息配置if (FileUtil.exist(metaOutputPath)) {// 1、构造配置参数Meta newMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);// 文件配置List<Meta.FileConfigDTO.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();fileInfoList.add(fileInfo);// 模型配置List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();modelInfoList.add(modelInfo);// 文件去重newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));// 配置去重newMeta.getModelConfig().setModels(distinctModels(modelInfoList));// 2、输出元信息配置文件FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);} else {// 1、构造配置参数Meta.FileConfigDTO fileConfig = new Meta.FileConfigDTO();meta.setFileConfig(fileConfig);fileConfig.setSourceRootPath(sourceRootPath);fileConfig.setType("dir");List<Meta.FileConfigDTO.FileInfo> fileInfoList = new ArrayList<>();fileInfoList.add(fileInfo);fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();modelInfoList.add(modelInfo);modelConfig.setModels(modelInfoList);meta.setModelConfig(modelConfig);// 2、输出元信息配置文件String metaJson = JSONUtil.toJsonPrettyStr(meta);FileUtil.writeUtf8String(metaJson, metaOutputPath);}return id;}/*** 文件去重* @param fileInfoList* @return*/private static List<Meta.FileConfigDTO.FileInfo> distinctFiles(List<Meta.FileConfigDTO.FileInfo> fileInfoList){if (CollUtil.isEmpty(fileInfoList)){return fileInfoList;}Collection<Meta.FileConfigDTO.FileInfo> values = fileInfoList.stream().collect(Collectors.toMap(Meta.FileConfigDTO.FileInfo::getInputPath,Function.identity(), (existing, replacement) -> replacement)).values();List<Meta.FileConfigDTO.FileInfo> distinctList = new ArrayList<>(values);return distinctList;}/*** 配置去重* @param modelInfoList* @return*/private static List<Meta.ModelConfigDTO.ModelInfo> distinctModels(List<Meta.ModelConfigDTO.ModelInfo> modelInfoList){if (CollUtil.isEmpty(modelInfoList)){return modelInfoList;}Collection<Meta.ModelConfigDTO.ModelInfo> values = modelInfoList.stream().collect(Collectors.toMap(Meta.ModelConfigDTO.ModelInfo::getFieldName,Function.identity(), (existing, replacement) -> replacement)).values();List<Meta.ModelConfigDTO.ModelInfo> distinctList = new ArrayList<>(values);return distinctList;}public static void main(String[] args) {String projectPath = System.getProperty("user.dir");String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/acm-template";// 项目基础信息Meta meta = new Meta();String projectName = "acm-template-generator";String description = "ACM模板生成器";meta.setName(projectName);meta.setDescription(description);// 输入文件路径(相对路径)String fileInputPath = "src/com/liucc/acm/MainTemplate.java";// 模型参数信息(首次)Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();modelInfo.setFieldName("outputText");modelInfo.setDefaultValue("Sum: ");// 模型参数信息(第二次)
// Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();
// modelInfo.setFieldName("className");// 替换变量(首次)String str = "Sum: ";// 替换变量(第二次)
// String str = "MainTemplate";Long id = makeTemplate(null, sourceRootPath, meta, fileInputPath, modelInfo, str);System.out.println("id = " + id);}
}
四、更多功能实现
1、单次制作多个模板文件
虽然现在制作工具可以通过多次执行来制作多个模板文件,但还是比较麻烦。比如之前的一个需求“批量替换 SpringBoot 项目中的所有包名”,可能存在几十个文件,难道我们要执行几十次制作工具吗?显然不可能,我们期望制作工具支持单次能够制作多个模板文件。
有2种实现方法:
- 支持输入文件目录,制作工具扫描指定目录下的所有文件,然后遍历生成模板文件和配置文件
- 支持用户一次性输入多个文件路径,制作工具同时处理指定文件列表
下面依次进行实现:
支持输入文件目录
实现思路:之前已经单个模板文件生成了么,将制作模板文件方法抽取出来,然后循环遍历指定目录下的所有文件,执行刚抽取的方法不就可以了吗。
修改TemplateMaker 类,将单次制作模板文件方法抽取出来,示例代码如下:
private static Meta.FileConfigDTO.FileInfo makeFileTemplate(String sourceRootPath, File inputFile, Meta.ModelConfigDTO.ModelInfo modelInfo, String searchStr) {String fileInputAbsolutePath = inputFile.getAbsolutePath();// windows系统需要对文件路径进行转移fileInputAbsolutePath = fileInputAbsolutePath.replaceAll("\\\\", "/");String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");String fileOutputPath = fileInputPath + ".ftl";// 二、使用字符串替换算法,生成模板文件String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;String originalContent;// 非首次制作,可以在已有模板文件的基础上再次挖坑if (FileUtil.exist(fileOutputAbsolutePath)) {originalContent = FileUtil.readUtf8String(fileOutputAbsolutePath);} else {originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);}String replacement = String.format("${%s}", modelInfo.getFieldName());String newContent = StrUtil.replace(originalContent, searchStr, replacement);Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();fileInfo.setInputPath(fileInputPath);fileInfo.setType(FileTypeEnum.FILE.getType());// 模板文件内容未发生变化,则生成静态文件if (StrUtil.equals(originalContent, newContent)) {fileInfo.setOutputPath(fileInputPath);fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getType());} else { // 动态文件fileInfo.setOutputPath(fileOutputPath);fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);}return fileInfo;
}
思考:该方法第二个参数inputFile
输入文件为什么改为 File
类型,而不是之前的 String
类型了?
主要有两个原因:
- 考虑到后期扩展,后面文件路径可能是网络资源 url 而不是本地文件系统路径,因此就统一使用 File 进行传参
- 方便调用方的使用。调用方可能是单个文件执行,也可能是遍历目录循环执行方法。这样就可以保持调用方的使用一致性。
调用方只需要判断输入文件是单个文件还是目录,示例代码修改如下:
// 如果输入文件是目录
if (FileUtil.isDirectory(inputFile)) {List<File> fileList = FileUtil.loopFiles(inputFile);for (File file : fileList) {Meta.FileConfigDTO.FileInfo fileInfo = makeFileTemplate(sourceRootPath, file, modelInfo, searchStr);fileInfos.add(fileInfo);}
} else { // 单个文件Meta.FileConfigDTO.FileInfo fileInfo = makeFileTemplate(sourceRootPath, inputFile, modelInfo, searchStr);fileInfos.add(fileInfo);
}
支持输入多个文件
这个需求就更简单了,我们现在已经支持对文件或目录进行多次执行生成了。我们只需要在外层遍历用户输入的参数,判断每一个“file”是文件还是目录,然后调用makeFileTemplate
方法即可!
示例代码如下:
fileInputPaths
参数是用户输入的多个文件路径。注意,Windows 系统对文件路径需要进行转义的问题。
List<Meta.FileConfigDTO.FileInfo> fileInfos = new ArrayList<>();
sourceRootPath = tempFilePath + File.separator + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).toString();
for (String fileInputPath : fileInputPaths) {File inputFile = new File(sourceRootPath + File.separator + fileInputPath);// windows系统需要对文件路径进行转移sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");// 如果输入文件是目录if (FileUtil.isDirectory(inputFile)) {List<File> fileList = FileUtil.loopFiles(inputFile);for (File file : fileList) {Meta.FileConfigDTO.FileInfo fileInfo = makeFileTemplate(sourceRootPath, file, modelInfo, searchStr);fileInfos.add(fileInfo);}} else { // 单个文件Meta.FileConfigDTO.FileInfo fileInfo = makeFileTemplate(sourceRootPath, inputFile, modelInfo, searchStr);fileInfos.add(fileInfo);}
}
测试执行
我们在 main 方法指定多个文件路径,调用测试执行
main 方法代码如下:
public static void main(String[] args) {String projectPath = System.getProperty("user.dir");String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/springboot-init";// 项目基础信息Meta meta = new Meta();String projectName = "acm-template-generator";String description = "ACM模板生成器";meta.setName(projectName);meta.setDescription(description);// 输入文件路径(相对路径)// yuzi-generator-demo-projects/springboot-init/src/main/java/com/liucc/springbootinitString fileInputPath = "src/main/java/com/liucc/springbootinit";// 模型参数信息(首次)
// Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();
// modelInfo.setFieldName("outputText");
// modelInfo.setDefaultValue("Sum: ");// 模型参数信息(第二次)Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();modelInfo.setFieldName("className");// 替换变量(首次)String str = "BaseResponse";// 替换变量(第二次)
// String str = "MainTemplate";String inputFilePath1 = "src/main/java/com/liucc/springbootinit/common";String inputFilePath2 = "src/main/java/com/liucc/springbootinit/config";Long id = makeTemplate(1914326387714437120L, sourceRootPath, meta, Arrays.asList(inputFilePath1, inputFilePath2), modelInfo, str);System.out.println("id = " + id);
}
测试执行,可以看到 common 包下生成了模板文件:
完整代码
package com.liucc.maker.template;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.liucc.maker.meta.Meta;
import com.liucc.maker.meta.enums.FileGenerateTypeEnum;
import com.liucc.maker.meta.enums.FileTypeEnum;import java.io.File;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;/*** 模板生成器,生成动态模板文件和元信息配置文件*/
public class TemplateMaker {/*** 制作模板** @param id id不存在,表示首次制作模板;id存在,更新模板* @return*/private static Long makeTemplate(Long id, String sourceRootPath, Meta meta, List<String> fileInputPaths, Meta.ModelConfigDTO.ModelInfo modelInfo, String searchStr) {if (CollUtil.isEmpty(fileInputPaths)) {System.out.println("输入文件为空fileInputPaths:" + fileInputPaths);return null;}if (id == null) {id = IdUtil.getSnowflakeNextId();}// 拷贝原始项目到临时目录(工作空间)中String projectPath = System.getProperty("user.dir");String tempFilePath = projectPath + File.separator + ".temp" + File.separator + id;// 非首次制作,不需要重复拷贝原始项目文件if (!FileUtil.exist(tempFilePath)) {FileUtil.mkdir(tempFilePath);FileUtil.copy(sourceRootPath, tempFilePath, true);}// 一、输入信息// 2、输入文件信息(相对路径)List<Meta.FileConfigDTO.FileInfo> fileInfos = new ArrayList<>();sourceRootPath = tempFilePath + File.separator + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).toString();for (String fileInputPath : fileInputPaths) {File inputFile = new File(sourceRootPath + File.separator + fileInputPath);// windows系统需要对文件路径进行转移sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");// 如果输入文件是目录if (FileUtil.isDirectory(inputFile)) {List<File> fileList = FileUtil.loopFiles(inputFile);for (File file : fileList) {Meta.FileConfigDTO.FileInfo fileInfo = makeFileTemplate(sourceRootPath, file, modelInfo, searchStr);fileInfos.add(fileInfo);}} else { // 单个文件Meta.FileConfigDTO.FileInfo fileInfo = makeFileTemplate(sourceRootPath, inputFile, modelInfo, searchStr);fileInfos.add(fileInfo);}}// 三、生成元信息配置文件String metaOutputPath = sourceRootPath + File.separator + "meta.json";// 非首次制作,不需要重复输入已有的元信息,而是在此基础上,可以覆盖或追加元信息配置if (FileUtil.exist(metaOutputPath)) {// 1、构造配置参数Meta newMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);// 文件配置List<Meta.FileConfigDTO.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();fileInfoList.addAll(fileInfos);// 模型配置List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();modelInfoList.add(modelInfo);// 文件去重newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));// 配置去重newMeta.getModelConfig().setModels(distinctModels(modelInfoList));// 2、输出元信息配置文件FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);} else {// 1、构造配置参数Meta.FileConfigDTO fileConfig = new Meta.FileConfigDTO();meta.setFileConfig(fileConfig);fileConfig.setSourceRootPath(sourceRootPath);fileConfig.setType("dir");List<Meta.FileConfigDTO.FileInfo> fileInfoList = new ArrayList<>();fileInfoList.addAll(fileInfos);fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();modelInfoList.add(modelInfo);modelConfig.setModels(modelInfoList);meta.setModelConfig(modelConfig);// 2、输出元信息配置文件String metaJson = JSONUtil.toJsonPrettyStr(meta);FileUtil.writeUtf8String(metaJson, metaOutputPath);}return id;}private static Meta.FileConfigDTO.FileInfo makeFileTemplate(String sourceRootPath, File inputFile, Meta.ModelConfigDTO.ModelInfo modelInfo, String searchStr) {String fileInputAbsolutePath = inputFile.getAbsolutePath();// windows系统需要对文件路径进行转移fileInputAbsolutePath = fileInputAbsolutePath.replaceAll("\\\\", "/");String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");String fileOutputPath = fileInputPath + ".ftl";// 二、使用字符串替换算法,生成模板文件String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;String originalContent;// 非首次制作,可以在已有模板文件的基础上再次挖坑if (FileUtil.exist(fileOutputAbsolutePath)) {originalContent = FileUtil.readUtf8String(fileOutputAbsolutePath);} else {originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);}String replacement = String.format("${%s}", modelInfo.getFieldName());String newContent = StrUtil.replace(originalContent, searchStr, replacement);Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();fileInfo.setInputPath(fileInputPath);fileInfo.setType(FileTypeEnum.FILE.getType());// 模板文件内容未发生变化,则生成静态文件if (StrUtil.equals(originalContent, newContent)) {fileInfo.setOutputPath(fileInputPath);fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getType());} else { // 动态文件fileInfo.setOutputPath(fileOutputPath);fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);}return fileInfo;}/*** 文件去重** @param fileInfoList* @return*/private static List<Meta.FileConfigDTO.FileInfo> distinctFiles(List<Meta.FileConfigDTO.FileInfo> fileInfoList) {if (CollUtil.isEmpty(fileInfoList)) {return fileInfoList;}Collection<Meta.FileConfigDTO.FileInfo> values = fileInfoList.stream().collect(Collectors.toMap(Meta.FileConfigDTO.FileInfo::getInputPath,Function.identity(), (existing, replacement) -> replacement)).values();List<Meta.FileConfigDTO.FileInfo> distinctList = new ArrayList<>(values);return distinctList;}/*** 配置去重** @param modelInfoList* @return*/private static List<Meta.ModelConfigDTO.ModelInfo> distinctModels(List<Meta.ModelConfigDTO.ModelInfo> modelInfoList) {if (CollUtil.isEmpty(modelInfoList)) {return modelInfoList;}Collection<Meta.ModelConfigDTO.ModelInfo> values = modelInfoList.stream().collect(Collectors.toMap(Meta.ModelConfigDTO.ModelInfo::getFieldName,Function.identity(), (existing, replacement) -> replacement)).values();List<Meta.ModelConfigDTO.ModelInfo> distinctList = new ArrayList<>(values);return distinctList;}public static void main(String[] args) {String projectPath = System.getProperty("user.dir");String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/springboot-init";// 项目基础信息Meta meta = new Meta();String projectName = "acm-template-generator";String description = "ACM模板生成器";meta.setName(projectName);meta.setDescription(description);// 输入文件路径(相对路径)// yuzi-generator-demo-projects/springboot-init/src/main/java/com/liucc/springbootinitString fileInputPath = "src/main/java/com/liucc/springbootinit";// 模型参数信息(首次)
// Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();
// modelInfo.setFieldName("outputText");
// modelInfo.setDefaultValue("Sum: ");// 模型参数信息(第二次)Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();modelInfo.setFieldName("className");// 替换变量(首次)String str = "BaseResponse";// 替换变量(第二次)
// String str = "MainTemplate";String inputFilePath1 = "src/main/java/com/liucc/springbootinit/common";String inputFilePath2 = "src/main/java/com/liucc/springbootinit/config";Long id = makeTemplate(1914326387714437120L, sourceRootPath, meta, Arrays.asList(inputFilePath1, inputFilePath2), modelInfo, str);System.out.println("id = " + id);}
}
2、文件过滤
之前梳理的 SpringBoot模板生成器有一个需求:
需求:控制是否生成帖子相关功能
实现思路:允许用户通过一个参数来控制帖子相关代码是否生成,帖子相关代码包括有:PostController、PostService、Mapper、Model、Mapper.xml等等。
通用能力:用一个参数同时控制多个文件是否生成,而不仅仅是某段代码是否生成。
为了实现这个需求,我们需要同时对多个包含 Post
的文件进行处理,虽然现在制作工具已经支持批量处理文件生成,但是这些包含 Post 文件分布在不同的目录下,如果这类文件数量特别多,用户需要指定的文件路径数量就越多,依然是一种麻烦。
有对应的解决方案吗?答案是肯定的。
类似于我们在 IDEA 搜索代码文件一样,IDEA 支持按照文件名称、文件内容搜索。同理,我们也可以给制作工具添加类似的文件过滤功能。
文件过滤机制设计
文件过滤有很多不同的过滤配置,常见的是这两类配置:
- 过滤范围:按照文件名称、或文件内容进行过滤:
- 过滤规则:包含(contains)、前缀匹配(startWith)、后缀匹配(endWith)、正则(regex)、相等(equals)
因为制作工具已经支持输入多个文件/目录,所以可以给每一个文件/目录都能指定自己的过滤规则(支持多条)。
参考文件过滤的json 结构如下:
{"files": [{"path": "文件(目录)路径","filters": [{"range": "fileName","rule": "regex","value": ".*lala.*"},{"range": "fileContent","rule": "contains","value": "haha"}]}],
}
上面这个 json 表示:搜索文件名称符合正则,且文件内容里包含haha
的文件。
开发实现
1)定义json 结构对应的实体类
在temple.model包下新建FileFilterConfig
类,定义文件过滤配置相关字段。示例代码如下:
package com.liucc.maker.template.model;import lombok.Builder;
import lombok.Data;/*** 文件过滤配置*/
@Data
@Builder
public class FileFilterConfig {/*** 过滤范围(文件名称、文件内容)*/private String range;/*** 过滤规则(包含、前缀匹配、后缀匹配、正则、相等)*/private String rule;/*** 过滤值*/private String value;}
定义外层 json 结构对应的实体类TemplateMakerFileConfig,示例代码如下:
package com.liucc.maker.template.model;import lombok.Data;import java.util.List;/*** 过滤文件配置*/
@Data
public class TemplateMakerFileConfig {private List<FileInfoConfig> files;@Datapublic static class FileInfoConfig {/*** 文件路径*/private String path;/*** 文件过滤器*/private List<FileFilterConfig> filters;}
}
2)定义文件过滤范围、过滤规则枚举类:
package com.liucc.maker.template.enums;import lombok.Getter;/*** 文件过滤范围枚举*/
public enum FileFilterRangeEnum {FILENAME("文件名称", "filename"),FILE_CONTENT("文件内容", "file_content");private String name;private String type;FileFilterRangeEnum(String name, String type) {this.name = name;this.type = type;}public String getName() {return name;}public String getType() {return type;}/*** 根据type获取枚举* @param type* @return*/public static FileFilterRangeEnum getByType(String type) {for (FileFilterRangeEnum fileFilterRangeEnum : FileFilterRangeEnum.values()) {if (fileFilterRangeEnum.getType().equals(type)) {return fileFilterRangeEnum;}}return null;}
}
package com.liucc.maker.template.enums;/*** 文件过滤规则枚举*/
public enum FileFilterRuleEnum {CONTAINS("文件名称", "contains"),START_WITH("文件名称", "startWith"),END_WITH("文件名称", "endWith"),REGEX("文件名称", "regex"),EQUALS("文件内容", "equals");private String name;private String type;FileFilterRuleEnum(String name, String type) {this.name = name;this.type = type;}public String getName() {return name;}public String getType() {return type;}/*** 根据type获取枚举* @param type* @return*/public static FileFilterRuleEnum getByType(String type) {for (FileFilterRuleEnum fileFilterRangeEnum : FileFilterRuleEnum.values()) {if (fileFilterRangeEnum.getType().equals(type)) {return fileFilterRangeEnum;}}return null;}
}
3)配置类准备完毕,开始编码实现文件过滤功能
在 template
包下新建FileFilter
类,实现单个文件过滤功能。
参数列表接收文件过滤器列表、单个文件对象。循环遍历文件过滤器列表,首先判断当前过滤器的过滤范围(文件名 or 文件内容),然后按照过滤规则进行过滤,最后将结果 result 进行返回。
完整代码如下:
package com.liucc.maker.template;import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import com.liucc.maker.template.enums.FileFilterRangeEnum;
import com.liucc.maker.template.enums.FileFilterRuleEnum;
import com.liucc.maker.template.model.FileFilterConfig;import java.io.File;
import java.util.List;/*** 文件过滤*/
public class FileFilter {/*** 单个文件过滤* @param fileFilterConfigs 文件过滤器* @param file* @return*/public static boolean doSingleFileFilter(List<FileFilterConfig> fileFilterConfigs, File file){// 没有过滤器if (CollUtil.isEmpty(fileFilterConfigs)){return true;}// 文件名称String fileName = file.getName();// 文件内容String fileContent = FileUtil.readUtf8String(file);boolean result = true;for (FileFilterConfig fileFilterConfig : fileFilterConfigs) {String range = fileFilterConfig.getRange();String rule = fileFilterConfig.getRule();String value = fileFilterConfig.getValue();FileFilterRangeEnum filterRangeEnum = FileFilterRangeEnum.getByType(range);FileFilterRuleEnum filterRuleEnum = FileFilterRuleEnum.getByType(rule);// 过滤规则不存在,跳过本次过滤if (filterRuleEnum == null){continue;}String content = fileName;switch (filterRangeEnum){case FILENAME:content = fileName;break;case FILE_CONTENT:content = fileContent;break;default:break;}// 根据规则进行过滤switch (filterRuleEnum){case CONTAINS:result = content.contains(value);break;case START_WITH:result = content.startsWith(value);break;case END_WITH:result = content.endsWith(value);break;case REGEX:result = content.matches(value);break;case EQUALS:result = content.equals(value);break;default:break;}return result;}return true;}
}
然后编写过滤器的主方法doFilter
,方法接收filePath
文件路径参数,支持传入单个文件或目录,能够同时对多个文件进行过滤处理。
实现思路:使用 hutool 工具类的loopFiles
方法递归获取指定 filePath
下的所有文件列表(不包含目录),再调用之前编写的单个文件过滤方法doSingleFileFilter
loopFiles
的参数哪怕传递的是单个文件,也会返回一个文件列表
代码如下:
/*** 文件过滤** @param filePath 可以是单个文件或目录的路径* @param fileFilterConfigs 过滤器配置* @return*/
public static List<File> doFilter(String filePath, List<FileFilterConfig> fileFilterConfigs){List<File> fileList = FileUtil.loopFiles(filePath);return fileList.stream().filter(file -> doSingleFileFilter(fileFilterConfigs, file)).collect(Collectors.toList());
}
文件过滤器有了,接下来修改之前的 TemplateMaker
的makeTemplate
方法,参数 List<String> fileInputPaths
改为从TemplateMakerFileConfig templateMakerConfig
对象中获取。
从templateMakerConfig
获取文件输入路径 fileInputPath
,组合
sourceRootPath
拼接全路径,调用文件过滤器进行过滤,获取过滤后的文件列表,然后循环执行makeFileTemplate
方法制作模板文件。
方法的最终代码如下:
/*** 制作模板* * @param id id不存在,表示首次制作模板;id存在,更新模板* @param sourceRootPath 项目根路径* @param meta 元信息* @param templateMakerConfig 文件过滤配置* @param modelInfo 模型数据* @param searchStr 替换字符串* @return*/
private static Long makeTemplate(Long id, String sourceRootPath, Meta meta, TemplateMakerFileConfig templateMakerConfig, Meta.ModelConfigDTO.ModelInfo modelInfo, String searchStr) {List<TemplateMakerFileConfig.FileInfoConfig> fileInfoConfigList = templateMakerConfig.getFiles();if (CollUtil.isEmpty(fileInfoConfigList)) {System.out.println("输入文件为空fileInputPaths:" + fileInfoConfigList);return null;}if (id == null) {id = IdUtil.getSnowflakeNextId();}// 拷贝原始项目到临时目录(工作空间)中String projectPath = System.getProperty("user.dir");String tempFilePath = projectPath + File.separator + ".temp" + File.separator + id;// 非首次制作,不需要重复拷贝原始项目文件if (!FileUtil.exist(tempFilePath)) {FileUtil.mkdir(tempFilePath);FileUtil.copy(sourceRootPath, tempFilePath, true);}// 一、输入信息// 2、输入文件信息(相对路径)List<Meta.FileConfigDTO.FileInfo> fileInfos = new ArrayList<>();sourceRootPath = tempFilePath + File.separator + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).toString();for (TemplateMakerFileConfig.FileInfoConfig fileInfoConfig : fileInfoConfigList) {String fileInputPath = fileInfoConfig.getPath(); // 相对路径String fileInputAbsolutePath = sourceRootPath + File.separator + fileInputPath;// 文件过滤 获取所有符合条件的文件列表(都是文件,不存在目录)List<File> fileList = FileFilter.doFilter(fileInputAbsolutePath, fileInfoConfig.getFilters());for (File file : fileList) {Meta.FileConfigDTO.FileInfo fileInfo = makeFileTemplate(sourceRootPath, file, modelInfo, searchStr);fileInfos.add(fileInfo);}}// 三、生成元信息配置文件String metaOutputPath = sourceRootPath + File.separator + "meta.json";// 非首次制作,不需要重复输入已有的元信息,而是在此基础上,可以覆盖或追加元信息配置if (FileUtil.exist(metaOutputPath)) {// 1、构造配置参数Meta newMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);// 文件配置List<Meta.FileConfigDTO.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();fileInfoList.addAll(fileInfos);// 模型配置List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();modelInfoList.add(modelInfo);// 文件去重newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));// 配置去重newMeta.getModelConfig().setModels(distinctModels(modelInfoList));// 2、输出元信息配置文件FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);} else {// 1、构造配置参数Meta.FileConfigDTO fileConfig = new Meta.FileConfigDTO();meta.setFileConfig(fileConfig);fileConfig.setSourceRootPath(sourceRootPath);fileConfig.setType("dir");List<Meta.FileConfigDTO.FileInfo> fileInfoList = new ArrayList<>();fileInfoList.addAll(fileInfos);fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();modelInfoList.add(modelInfo);modelConfig.setModels(modelInfoList);meta.setModelConfig(modelConfig);// 2、输出元信息配置文件String metaJson = JSONUtil.toJsonPrettyStr(meta);FileUtil.writeUtf8String(metaJson, metaOutputPath);}return id;
}
在 main 方法中编写测试数据,检查文件过滤器是否生效。
编写两个过滤器:
- 过滤器 1,获取
inputFilePath1
文件路径,过滤范围(文件名),过滤规则(包含),过滤值(Base) - 过滤器 2,获取
inputFilePath2
文件路径,但不添加任何过滤配置
main 方法示例代码如下:
public static void main(String[] args) {String projectPath = System.getProperty("user.dir");String sourceRootPath = new File(projectPath).getParent() + File.separator + "yuzi-generator-demo-projects/springboot-init";// 项目基础信息Meta meta = new Meta();String projectName = "acm-template-generator";String description = "ACM模板生成器";meta.setName(projectName);meta.setDescription(description);// 输入文件路径(相对路径)// yuzi-generator-demo-projects/springboot-init/src/main/java/com/liucc/springbootinitString fileInputPath = "src/main/java/com/liucc/springbootinit";// 模型参数信息(首次)
// Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();
// modelInfo.setFieldName("outputText");
// modelInfo.setDefaultValue("Sum: ");// 模型参数信息(第二次)Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();modelInfo.setFieldName("className");// 替换变量(首次)String str = "BaseResponse";// 替换变量(第二次)
// String str = "MainTemplate";String inputFilePath1 = "src/main/java/com/liucc/springbootinit/common";String inputFilePath2 = "src/main/java/com/liucc/springbootinit/config";// 准备文件过滤配置TemplateMakerFileConfig templateMakerConfig = new TemplateMakerFileConfig();TemplateMakerFileConfig.FileInfoConfig fileInfoConfig1 = new TemplateMakerFileConfig.FileInfoConfig();fileInfoConfig1.setPath(inputFilePath1);FileFilterConfig fileFilter1 = FileFilterConfig.builder().range(FileFilterRangeEnum.FILENAME.getType()).rule(FileFilterRuleEnum.CONTAINS.getType()).value("Base").build();fileInfoConfig1.setFilters(Arrays.asList(fileFilter1));// 不设置过滤器TemplateMakerFileConfig.FileInfoConfig fileInfoConfig2 = new TemplateMakerFileConfig.FileInfoConfig();fileInfoConfig2.setPath(inputFilePath2);List<TemplateMakerFileConfig.FileInfoConfig> fileInfoConfigs = Arrays.asList(fileInfoConfig1, fileInfoConfig2);templateMakerConfig.setFiles(fileInfoConfigs);Long id = makeTemplate(null, sourceRootPath, meta, templateMakerConfig, modelInfo, str);System.out.println("id = " + id);
}
测试执行,common
包下的BaseResponse.java
文件成功制作了对应的模板文件。
3、文件分组
我们现在的代码生成器制作工具已经支持对文件进行分组,通过设置 condition 实现一个模型参数同时控制多个文件或代码的生成。那自然,我们的模板制作工具也要支持快速生成文件组配置的能力。
实现思路
通常,可以将用户批量制作的模板文件当做是同一个分组内,也即TemplateMakerFileConfig
类里的files
字段都隶属于同一个分组。因此我们的实现思路就是:给TemplateMakerFileConfig
再新增一个字段fileGroupConfig
,维护分组本身信息。
开发实现
1)在TemplateMakerFileConfig
类内定义分组实体,示例代码如下:
package com.liucc.maker.template.model;import lombok.Data;import java.util.List;/*** 过滤文件配置*/
@Data
public class TemplateMakerFileConfig {private List<FileInfoConfig> files;private FileGroupConfig fileGroupConfig;/*** 文件分组**/@Datapublic static class FileGroupConfig {private String condition;private String groupKey;private String groupName;}@Datapublic static class FileInfoConfig {/*** 文件路径(相对路径)*/private String path;/*** 文件过滤器*/private List<FileFilterConfig> filters;}
}
2)在模板生成器TemplateMaker
类的makeTemplate
内新增文件组配置。
判断如果当前是否为文件组(groupKey 是否存在),如果存在,将 group 相关配置进行维护即可。示例代码如下:
// 新增文件组配置
TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = templateMakerConfig.getFileGroupConfig();
String condition = fileGroupConfig.getCondition();
String groupKey = fileGroupConfig.getGroupKey();
String groupName = fileGroupConfig.getGroupName();
if (StrUtil.isNotBlank(groupKey)){ // 说明是文件组Meta.FileConfigDTO.FileInfo fileInfoGroup = new Meta.FileConfigDTO.FileInfo();fileInfoGroup.setType(FileTypeEnum.GROUP.getType());fileInfoGroup.setCondition(condition);fileInfoGroup.setGroupKey(groupKey);fileInfoGroup.setGroupName(groupName);fileInfoGroup.setFiles(fileInfos);// 重置 fileInfos 为文件组fileInfos = new ArrayList<>();fileInfos.add(fileInfoGroup);
}
3)在main 方法内测试执行,检查最后生成的 meta.json配置文件中是否有文件组相关配置项。
...
// 测试新增文件组配置
TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = new TemplateMakerFileConfig.FileGroupConfig();
fileGroupConfig.setGroupKey("test");
fileGroupConfig.setGroupName("测试分组");
fileGroupConfig.setCondition("outputText");
templateMakerConfig.setFileGroupConfig(fileGroupConfig);
Long id = makeTemplate(null, sourceRootPath, meta, templateMakerConfig, modelInfo, str);
追加文件分组配置能力
1)明确需求
这个需求是什么意思呢?就是说假如我有多次模板制作,我希望这几次模板制作的所有文件都可以隶属于同一个分组下、也可以不同组。具体的需求如下:
- 同分组、相同文件自动合并
- 不同分组、相同文件进行保留
示例:
第一次模板制作:groupKey=testA,文件有[a, b, c]
第二次模板制作:groupKey=testA,文件有[b, c, d]
第三次模板制作:groupKey=testB,文件有[c, d, e]
那么最后我的 meta.json的文件配置项是:
"fileConfig": {"sourceRootPath": "/Users/liuchuangchuang/code/yuzi-generator/yuzi-generator-marker/.temp/1914638831886233600/springboot-init","type": "dir","files": [{"groupKey": "testA","files": [a, b, c, d]},{"groupKey": "testB","files": [c, d, e]}]},
2)实现流程
我们最终需要对文件分组列表进行去重,具体有以下步骤:
- 将所有的文件列表(fileInfo)分为有分组和无分组的;
- 对于有分组的文件配置,按照 groupKey 进行分组,同分组内的相同文件进行合并,不同分组的相同文件进行保留;
- 创建一个新的文件配置列表(结果列表),将合并后的分组添加到结果列表
- 最后,再将无分组的文件添加到结果列表中
3)编码实现
修改TemplateMaker
的文件去重方法distinctFiles
,按照上述4个流程进行编码实现,修改代码如下:
/*** 文件去重** @param fileInfoList* @return*/
private static List<Meta.FileConfigDTO.FileInfo> distinctFiles(List<Meta.FileConfigDTO.FileInfo> fileInfoList) {if (CollUtil.isEmpty(fileInfoList)) {return fileInfoList;}// 1、将所有的文件列表(fileInfo)分为有分组和无分组的;List<Meta.FileConfigDTO.FileInfo> groupFileList = fileInfoList.stream().filter(file -> StrUtil.isNotBlank(file.getGroupKey())).collect(Collectors.toList());// 2、对于有分组的文件配置,按照 groupKey 进行分组,同分组内的相同文件进行合并,不同分组的相同文件进行保留;// testA -> [file1, file2, file3], testA -> [file2, file3, file4], testB -> [file2, file3, file4]// ==> testA -> [file1, file2, file3, file4], testB -> [file2, file3, file4]Map<String, List<Meta.FileConfigDTO.FileInfo>> groupKeyFileInfoListMap = groupFileList.stream().collect(Collectors.groupingBy(Meta.FileConfigDTO.FileInfo::getGroupKey));// 同组内的文件进行合并Map<String, Meta.FileConfigDTO.FileInfo> groupKeyMergedFileInfoMap = new HashMap<>();for (Map.Entry<String, List<Meta.FileConfigDTO.FileInfo>> entry : groupKeyFileInfoListMap.entrySet()) {List<Meta.FileConfigDTO.FileInfo> tempFileInfoList = entry.getValue();// 合并后的文件列表List<Meta.FileConfigDTO.FileInfo> newFileInfoList = new ArrayList<>(tempFileInfoList.stream().flatMap(fileInfo -> fileInfo.getFiles().stream()).collect(Collectors.toMap(Meta.FileConfigDTO.FileInfo::getInputPath, o->o, (existing, replacement) -> replacement)).values());// 更新 group 组配置,使用最后一个组配置进行覆盖Meta.FileConfigDTO.FileInfo newestFileInfo = CollUtil.getLast(tempFileInfoList);newestFileInfo.setFiles(newFileInfoList);String groupKey = entry.getKey();groupKeyMergedFileInfoMap.put(groupKey, newestFileInfo);}// 3、创建一个新的文件配置列表(结果列表),将合并后的分组添加到结果列表List<Meta.FileConfigDTO.FileInfo> resultList = new ArrayList<>();resultList.addAll(groupKeyMergedFileInfoMap.values());// 4、将无分组的文件配置添加到结果列表List<Meta.FileConfigDTO.FileInfo> noGroupFileList = new ArrayList<>(fileInfoList.stream().filter(file -> StrUtil.isBlank(file.getGroupKey())).collect(Collectors.toMap(Meta.FileConfigDTO.FileInfo::getInputPath,Function.identity(), (existing, replacement) -> replacement)).values());resultList.addAll(noGroupFileList);return resultList;
}
核心代码:
- 利用Java8 StreamAPI的
groupingBy
快速对文件进行分组,简化代码开发 - 使用Java8 StreamAPI的
flatMap
将列表打散平铺,将2个list合并为1个list,最后使用Collectors.toMap
进行数据收集
4)测试执行
-
第一次执行,id给空,文件路径分别是
String inputFilePath1 = "src/main/java/com/liucc/springbootinit/common"; String inputFilePath2 = "src/main/java/com/liucc/springbootinit/controller";// 测试新增文件组配置 TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = new TemplateMakerFileConfig.FileGroupConfig(); fileGroupConfig.setGroupKey("test"); fileGroupConfig.setGroupName("测试分组"); fileGroupConfig.setCondition("outputText"); templateMakerConfig.setFileGroupConfig(fileGroupConfig); Long id = makeTemplate(null, sourceRootPath, meta, templateMakerConfig, modelInfo, str);
执行main方法,controller和common包生成了对应的模板文件,meta.json生成了正确的文件配置信息:
-
第二次执行,id为第一次生成的工作空间id,文件路径改为:
String inputFilePath1 = "src/main/java/com/liucc/springbootinit/common"; String inputFilePath2 = "src/main/java/com/liucc/springbootinit/constant";// 测试新增文件组配置 TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = new TemplateMakerFileConfig.FileGroupConfig(); fileGroupConfig.setGroupKey("test"); fileGroupConfig.setGroupName("测试分组2"); fileGroupConfig.setCondition("outputText"); templateMakerConfig.setFileGroupConfig(fileGroupConfig); Long id = makeTemplate(1914669831439847424L, sourceRootPath, meta, templateMakerConfig, modelInfo, str);
执行main方法,分组配置调整为最新的分组配置信息,且输入文件配置添加进constant相关文件信息。
4、模型分组
和文件分组一样,之前我们的制作工具已经实现了模型分组的能力,那我们的模板制作工具也要能够支持同时指定多个模型参数对模板文件进行“挖坑”。
实现思路:和文件分组的实现思路是一样的。
开发实现
1)定义模型配置类TemplateMakerModelConfig
package com.liucc.maker.template.model;import com.liucc.maker.meta.Meta;
import lombok.Data;import java.util.List;/*** 过滤模型配置*/
@Data
public class TemplateMakerModelConfig {private List<ModelInfoConfig> models;private ModelGroupConfig modelGroupConfig;@Datapublic static class ModelGroupConfig {private String condition;private String groupKey;private String groupName;}@Datapublic static class ModelInfoConfig {private String fieldName;private String type;private String description;private Object defaultValue;private String abbr;// 要替换的文本private String replaceText;}
}
2)修改模型去重方法distinctModels
和文件分组去重一样,模型去重也要支持对分组模型参数进行去重,逻辑和文件去重是完全一样的。
代码如下:
/*** 配置去重** @param modelInfoList* @return*/
private static List<Meta.ModelConfigDTO.ModelInfo> distinctModels(List<Meta.ModelConfigDTO.ModelInfo> modelInfoList) {if (CollUtil.isEmpty(modelInfoList)) {return modelInfoList;}// 1、将所有的模型列表(modelInfo)分为有分组和无分组的;List<Meta.ModelConfigDTO.ModelInfo> groupModelList = modelInfoList.stream().filter(model -> StrUtil.isNotBlank(model.getGroupKey())).collect(Collectors.toList());// 2、对于有分组的模型配置,按照 groupKey 进行分组,同分组内的相同模型进行合并,不同分组的相同模型进行保留;// testA -> [model1, model2, model3], testA -> [model2, model3, model4], testB -> [model2, model3, model4]// ==> testA -> [model1, model2, model3, model4], testB -> [model2, model3, model4]Map<String, List<Meta.ModelConfigDTO.ModelInfo>> groupKeyModelInfoListMap = groupModelList.stream().collect(Collectors.groupingBy(Meta.ModelConfigDTO.ModelInfo::getGroupKey));// 同组内的模型进行合并Map<String, Meta.ModelConfigDTO.ModelInfo> groupKeyMergedModelInfoMap = new HashMap<>();for (Map.Entry<String, List<Meta.ModelConfigDTO.ModelInfo>> entry : groupKeyModelInfoListMap.entrySet()) {List<Meta.ModelConfigDTO.ModelInfo> tempModelInfoList = entry.getValue();// 合并后的模型列表List<Meta.ModelConfigDTO.ModelInfo> newModelInfoList = new ArrayList<>(tempModelInfoList.stream().flatMap(modelInfo -> modelInfo.getModels().stream()).collect(Collectors.toMap(Meta.ModelConfigDTO.ModelInfo::getFieldName, o->o, (existing, replacement) -> replacement)).values());// 更新 group 组配置,使用最后一个组配置进行覆盖Meta.ModelConfigDTO.ModelInfo newestModelInfo = CollUtil.getLast(tempModelInfoList);newestModelInfo.setModels(newModelInfoList);String groupKey = entry.getKey();groupKeyMergedModelInfoMap.put(groupKey, newestModelInfo);}// 3、创建一个新的模型配置列表(结果列表),将合并后的分组添加到结果列表List<Meta.ModelConfigDTO.ModelInfo> resultList = new ArrayList<>();resultList.addAll(groupKeyMergedModelInfoMap.values());// 4、将无分组的模型配置添加到结果列表List<Meta.ModelConfigDTO.ModelInfo> noGroupModelList = new ArrayList<>(modelInfoList.stream().filter(model -> StrUtil.isBlank(model.getGroupKey())).collect(Collectors.toMap(Meta.ModelConfigDTO.ModelInfo::getFieldName,Function.identity(), (existing, replacement) -> replacement)).values());resultList.addAll(noGroupModelList);return resultList;
}
3)修改makeTemplate
方法的参数列表,使用封装好的TemplateMakerModelConfig
代替modelInfo
和searchStr
/*** 制作模板* * @param id id不存在,表示首次制作模板;id存在,更新模板* @param sourceRootPath 项目根路径* @param meta 元信息* @param templateMakerConfig 文件过滤配置* @param templateMakerModelConfig 模型过滤配置* @return*/
private static Long makeTemplate(Long id, String sourceRootPath, Meta meta, TemplateMakerFileConfig templateMakerConfig, TemplateMakerModelConfig templateMakerModelConfig) {}
和文件分组一样,在生成元信息配置之前,我们也要对模型分组进行处理,从模型配置中读出分组信息和模型列表信息,然后转换为可以生成元信息配置的newModelInfoList
,代码如下:
// 处理模型信息
List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();
// - 转换为元信息可接受的ModelInfo对象
List<Meta.ModelConfigDTO.ModelInfo> inputModelInfoList = models.stream().map(modelInfoConfig -> {Meta.ModelConfigDTO.ModelInfo modelInfo = new Meta.ModelConfigDTO.ModelInfo();BeanUtil.copyProperties(modelInfoConfig, modelInfo);return modelInfo;
}).collect(Collectors.toList());
// - 本次新增的模型配置列表
List<Meta.ModelConfigDTO.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.ModelConfigDTO.ModelInfo newModelInfo = new Meta.ModelConfigDTO.ModelInfo();newModelInfo.setGroupKey(groupKey);newModelInfo.setGroupName(groupName);newModelInfo.setCondition(condition);newModelInfo.setModels(inputModelInfoList);newModelInfoList.add(newModelInfo);
}else {// 不是分组,添加所有的模型列表newModelInfoList.addAll(inputModelInfoList);
}
4)修改makeFileTemplate
方法,要能够支持使用多个模型参数(一组参数)对模板文件进行“挖坑”
修改方法的参数列表,如下:
/*** 单次制作模板文件** @param sourceRootPath 输入文件根路径* @param inputFile 输入文件* @param templateMakerModelConfig 支持一组模型参数对单个文件进行挖坑* @return*/
private static Meta.FileConfigDTO.FileInfo makeFileTemplate(String sourceRootPath, File inputFile, TemplateMakerModelConfig templateMakerModelConfig) {}
调整单次制作模板文件的逻辑:能够支持多轮“挖坑”,第一轮挖坑后的内容作为第二轮挖坑的原始内容,直至模型参数全部替换完成。代码如下:
// 二、使用字符串替换算法,生成模板文件
String fileOutputAbsolutePath = sourceRootPath + File.separator + fileOutputPath;
String originalContent;
// 非首次制作,可以在已有模板文件的基础上再次挖坑
if (FileUtil.exist(fileOutputAbsolutePath)) {originalContent = FileUtil.readUtf8String(fileOutputAbsolutePath);
} else {originalContent = FileUtil.readUtf8String(fileInputAbsolutePath);
}String newContent = originalContent;
List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();
TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();
for (TemplateMakerModelConfig.ModelInfoConfig modelInfo : models) {if (modelGroupConfig == null){// 不是分组String replacement = String.format("${%s}", modelInfo.getFieldName());newContent = StrUtil.replace(newContent, modelInfo.getReplaceText(), replacement);}else {// 是分组,挖坑时要注意多一个层级String groupKey = modelGroupConfig.getGroupKey();String replacement = String.format("${%s.%s}", groupKey, modelInfo.getFieldName());newContent = StrUtil.replace(newContent, modelInfo.getReplaceText(), replacement);}
}
5)测试验证,定义一组能够替换 M y S Q L MySQL MySQL 配置的模型组参数,用来替换 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);Long id = makeTemplate(null, sourceRootPath, meta, templateMakerFileConfig, templateMakerModelConfig);
最后
本章节的内容到此就完成了,主要是给制作工具增加模板制作的能力,以后就不需要我们人工地编写模板文件和手动“挖坑”了。给制作工具增加的功能主要有:
- 支持单次制作多个模板文件
- 支持文件过滤(给用户指定的文件范围生成模板文件)
- 支持文件分组(同时批处理的文件属于同一个分组)
- 支持模型分组(同时可以挖多个参数)