08-boot项目生成
- 工具 - Bug 修复
- 工具 - 易用性优化
- 制作 SpringBoot 生成器
- 测试成果
- 扩展思路
1. Bug修复
目前,虽然工具已经完成核心功能的开发,但是并没有经过充分的测试验证
package com.yupi.maker.template.go;
public class Tmp7 {
/**
* 1. 同配置多次生成,变为静态
* 2. 误处理.ftl
* 3. 文件输入和输出路径相反
* 4. 调整meta.json路径
*/
@Test
public void test() {
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);
// 文件过滤
TemplateMakerFileConfig templateMakerFileConfig = new TemplateMakerFileConfig();
TemplateMakerFileConfig.FileInfoConfig fileInfoConfig1 = new TemplateMakerFileConfig.FileInfoConfig();
fileInfoConfig1.setPath(inputFilePath1);
List<FileFilterConfig> fileFilterConfigList = new ArrayList<>();
FileFilterConfig fileFilterConfig = FileFilterConfig.builder()
.range(FileFilterRangeEnum.FILE_NAME.getValue())
.rule(FileFilterRuleEnum.CONTAINS.getValue())
.value("Base")
.build();
fileFilterConfigList.add(fileFilterConfig);
fileInfoConfig1.setFilterConfigList(fileFilterConfigList);
TemplateMakerFileConfig.FileInfoConfig fileInfoConfig2 = new TemplateMakerFileConfig.FileInfoConfig();
fileInfoConfig2.setPath(inputFilePath2);
templateMakerFileConfig.setFiles(Arrays.asList(fileInfoConfig1, fileInfoConfig2));
// 分组配置
TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = new TemplateMakerFileConfig.FileGroupConfig();
fileGroupConfig.setCondition("outputText");
fileGroupConfig.setGroupKey("test");
fileGroupConfig.setGroupName("测试分组");
templateMakerFileConfig.setFileGroupConfig(fileGroupConfig);
long id = makeTemplate(meta, originProjectPath, templateMakerFileConfig, templateMakerModelConfig, 1854123915333320704L);
System.out.println(id);
}
/**
* 制作模板
*/
public static long makeTemplate(Meta newMeta, String originProjectPath,
TemplateMakerFileConfig templateMakerFileConfig,
TemplateMakerModelConfig templateMakerModelConfig,
Long id) {
// 没有 id 则生成
if (id == null) {
id = IdUtil.getSnowflakeNextId();
}
// 复制目录
String projectPath = System.getProperty("user.dir");
String tempDirPath = projectPath + File.separator + ".temp";
String templatePath = tempDirPath + File.separator + id;
// 是否为首次制作模板
// 目录不存在,则是首次制作
if (!FileUtil.exist(templatePath)) {
FileUtil.mkdir(templatePath);
FileUtil.copy(originProjectPath, templatePath, true);
}
// 一、输入信息
// 输入文件信息
String sourceRootPath = templatePath + File.separator + FileUtil.getLastPathEle(Paths.get(originProjectPath)).toString();
// 注意 win 系统需要对路径进行转义
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
List<TemplateMakerFileConfig.FileInfoConfig> fileConfigInfoList = templateMakerFileConfig.getFiles();
// 二、生成文件模板
// 遍历输入文件
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
for (TemplateMakerFileConfig.FileInfoConfig fileInfoConfig : fileConfigInfoList) {
String inputFilePath = fileInfoConfig.getPath();
// 如果填的是相对路径,要改为绝对路径
if (!inputFilePath.startsWith(sourceRootPath)) {
inputFilePath = sourceRootPath + File.separator + inputFilePath;
}
// 获取过滤后的文件列表(不会存在目录)
List<File> fileList = FileFilter.doFilter(inputFilePath, fileInfoConfig.getFilterConfigList());
// 不处理已生成的 FTL 模板文件
fileList = fileList.stream()
.filter(file -> !file.getAbsolutePath().endsWith(".ftl"))
.collect(Collectors.toList());
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 = templatePath + File.separator + "meta.json";
// 如果已有 meta 文件,说明不是第一次制作,则在 meta 基础上进行修改
if (FileUtil.exist(metaOutputPath)) {
Meta oldMeta = JSONUtil.toBean(FileUtil.readUtf8String(metaOutputPath), Meta.class);
BeanUtil.copyProperties(newMeta, oldMeta, CopyOptions.create().ignoreNullValue());
newMeta = oldMeta;
// 1. 追加配置参数
List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
fileInfoList.addAll(newFileInfoList);
List<Meta.ModelConfig.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();
modelInfoList.addAll(newModelInfoList);
// 配置去重
newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));
newMeta.getModelConfig().setModels(distinctModels(modelInfoList));
} else {
// 1. 构造配置参数
Meta.FileConfig fileConfig = new Meta.FileConfig();
newMeta.setFileConfig(fileConfig);
fileConfig.setSourceRootPath(sourceRootPath);
List<Meta.FileConfig.FileInfo> fileInfoList = new ArrayList<>();
fileConfig.setFiles(fileInfoList);
fileInfoList.addAll(newFileInfoList);
Meta.ModelConfig modelConfig = new Meta.ModelConfig();
newMeta.setModelConfig(modelConfig);
List<Meta.ModelConfig.ModelInfo> modelInfoList = new ArrayList<>();
modelConfig.setModels(modelInfoList);
modelInfoList.addAll(newModelInfoList);
}
// 2. 输出元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);
return id;
}
/**
* 制作文件模板
*/
private static Meta.FileConfig.FileInfo makeFileTemplate(
TemplateMakerModelConfig templateMakerModelConfig,
String sourceRootPath, File inputFile) {
// 要挖坑的文件绝对路径(用于制作模板)
// 注意 win 系统需要对路径进行转义
String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";
// 文件输入输出相对路径(用于生成配置)
String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
String fileOutputPath = fileInputPath + ".ftl";
// 使用字符串替换,生成模板文件
String fileContent;
// 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
boolean hasTemplateFile = FileUtil.exist(fileOutputAbsolutePath);
if (hasTemplateFile) {
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(fileOutputPath);
fileInfo.setOutputPath(fileInputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
// 是否更改了文件内容
boolean contentEquals = newFileContent.equals(fileContent);
// 之前不存在模板文件,并且没有更改文件内容,则为静态生成
if (!hasTemplateFile) {
if (contentEquals) {
// 输入路径没有 FTL 后缀
fileInfo.setInputPath(fileInputPath);
fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
} else {
// 没有模板文件,需要挖坑,生成模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
}
} else if (!contentEquals) {
// 有模板文件,且增加了新坑,生成模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
}
return fileInfo;
}
/**
* 模型分组去重
*/
private static List<Meta.ModelConfig.ModelInfo> distinctModels(List<Meta.ModelConfig.ModelInfo> modelInfoList) {
// 策略:同分组内模型 merge,不同分组保留
// 1. 有分组的,以组为单位划分
Map<String, List<Meta.ModelConfig.ModelInfo>> groupKeyModelInfoListMap = modelInfoList
.stream()
.filter(modelInfo -> StrUtil.isNotBlank(modelInfo.getGroupKey()))
.collect(
Collectors.groupingBy(Meta.ModelConfig.ModelInfo::getGroupKey)
);
// 2. 同组内的模型配置合并
// 保存每个组对应的合并后的对象 map
Map<String, Meta.ModelConfig.ModelInfo> groupKeyMergedModelInfoMap = new HashMap<>();
for (Map.Entry<String, List<Meta.ModelConfig.ModelInfo>> entry : groupKeyModelInfoListMap.entrySet()) {
List<Meta.ModelConfig.ModelInfo> tempModelInfoList = entry.getValue();
List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>(tempModelInfoList.stream()
.flatMap(modelInfo -> modelInfo.getModels().stream())
.collect(
Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)
).values());
// 使用新的 group 配置
Meta.ModelConfig.ModelInfo newModelInfo = CollUtil.getLast(tempModelInfoList);
newModelInfo.setModels(newModelInfoList);
String groupKey = entry.getKey();
groupKeyMergedModelInfoMap.put(groupKey, newModelInfo);
}
// 3. 将模型分组添加到结果列表
List<Meta.ModelConfig.ModelInfo> resultList = new ArrayList<>(groupKeyMergedModelInfoMap.values());
// 4. 将未分组的模型添加到结果列表
List<Meta.ModelConfig.ModelInfo> noGroupModelInfoList = modelInfoList.stream()
.filter(modelInfo -> StrUtil.isBlank(modelInfo.getGroupKey()))
.collect(Collectors.toList());
resultList.addAll(new ArrayList<>(noGroupModelInfoList.stream()
.collect(
Collectors.toMap(Meta.ModelConfig.ModelInfo::getFieldName, o -> o, (e, r) -> r)
).values()));
return resultList;
}
/**
* 文件分组去重
*/
private static List<Meta.FileConfig.FileInfo> distinctFiles(List<Meta.FileConfig.FileInfo> fileInfoList) {
// 策略:同分组内文件 merge,不同分组保留
// 1. 有分组的,以组为单位划分
Map<String, List<Meta.FileConfig.FileInfo>> groupKeyFileInfoListMap = fileInfoList
.stream()
.filter(fileInfo -> StrUtil.isNotBlank(fileInfo.getGroupKey()))
.collect(
Collectors.groupingBy(Meta.FileConfig.FileInfo::getGroupKey)
);
// 2. 同组内的文件配置合并
// 保存每个组对应的合并后的对象 map
Map<String, Meta.FileConfig.FileInfo> groupKeyMergedFileInfoMap = new HashMap<>();
for (Map.Entry<String, List<Meta.FileConfig.FileInfo>> entry : groupKeyFileInfoListMap.entrySet()) {
List<Meta.FileConfig.FileInfo> tempFileInfoList = entry.getValue();
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(tempFileInfoList.stream()
.flatMap(fileInfo -> fileInfo.getFiles().stream())
.collect(
Collectors.toMap(Meta.FileConfig.FileInfo::getOutputPath, 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::getOutputPath, o -> o, (e, r) -> r)
).values()));
return resultList;
}
}
1. 同配置多次执行变为静态
1. Bug介绍
- 第一次生成的
meta.json
文件
{
"inputPath": "src/main/resources/application.yml",
"outputPath": "src/main/resources/application.yml.ftl",
"type": "file",
"generateType": "dynamic"
}
- 第二次生成时,不修改任何的输入配置,直接再次执行工具
generateType
被修改为static
类型,显然这是有问题的,因为application.yml
已经被制作为了模板
{
"inputPath": "src/main/resources/application.yml",
"outputPath": "src/main/resources/application.yml",
"type": "file",
"generateType": "static"
}
2. 解决方案
该 Bug 的解决方案很简单:如果后续制作时发现已有模板文件,则该文件不会被设置为静态生成
makeFileTemplate()
方法中,抽象出hasTemplateFile
布尔变量,用于判断是否已有模板文件- 调整设置文件信息对象的逻辑,默认设置生成类型为动态
- 如果之前不存在模板文件,并且经过字符串替换后没有更改文件内容(只有这一种情况),才为静态生成
/**
* 制作文件模板
*/
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;
// 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
boolean hasTemplateFile = FileUtil.exist(fileOutputAbsolutePath);
if (hasTemplateFile) {
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(fileOutputPath);
fileInfo.setOutputPath(fileInputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
// 是否更改了文件内容
boolean contentEquals = newFileContent.equals(fileContent);
// 之前不存在模板文件,并且没有更改文件内容,则为静态生成
if (!hasTemplateFile) {
if (contentEquals) {
// 输入路径没有 FTL 后缀
fileInfo.setInputPath(fileInputPath);
fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
} else {
// 没有模板文件,需要挖坑,生成模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
}
} else if (!contentEquals) {
// 有模板文件,且增加了新坑,生成模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
}
return fileInfo;
}
3. 测试验证
在 TemplateMaker
类名上按 Alt + Enter
,选中 Create Test
,即可快速创建单元测试
选择正确的单元测试库,并勾选需要测试的方法
连续执行两次方法,发现文件的生成类型符合预期
2. 错误处理.ftl
1. Bug介绍
多次制作时,指定了相同的目录,会基于 FTL 文件再次制作模板,导致生成错误配置。BaseResponse.java.ftl
被进行了处理
{
"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/common/BaseResponse.java.ftl",
"outputPath": "src/main/java/com/yupi/springbootinit/common/BaseResponse.java.ftl",
"type": "file",
"generateType": "static"
}
2. 解决方案
修改 makeTemplate()
,在文件过滤后补充移除 FTL 模板文件
// 获取过滤后的文件列表(不会存在目录)
List<File> fileList = FileFilter.doFilter(inputFilePath, fileInfoConfig.getFilterConfigList());
// 不处理已生成的 FTL 模板文件
fileList = fileList.stream()
.filter(file -> !file.getAbsolutePath().endsWith(".ftl"))
.collect(Collectors.toList());
3. 文件输入输出路径相反
1. Bug介绍
- 在制作模板时,根据原始文件得到 FTL 模板文件
- 但在生成器的元信息中,其实是根据 FTL 模板文件来生成目标文件
{
"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"
}
2. 解决方案
- 在封装
fileInfo
对象时,对输入输出路径的值进行替换。保证输入路径是 FTL 模板文件、输出路径是预期生成的文件 - 注意,如果是静态生成,也要保证输入路径等于输出路径
// 文件配置信息
Meta.FileConfig.FileInfo fileInfo = new Meta.FileConfig.FileInfo();
// 注意文件输入路径要和输出路径反转
fileInfo.setInputPath(fileOutputPath);
fileInfo.setOutputPath(fileInputPath);
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
// 是否更改了文件内容
boolean contentEquals = newFileContent.equals(fileContent);
// 之前不存在模板文件,并且没有更改文件内容,则为静态生成
if (!hasTemplateFile) {
if (contentEquals) {
// 输入路径没有 FTL 后缀
fileInfo.setInputPath(fileInputPath);
fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
} else {
// 没有模板文件,且增加了新坑,生成模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
}
}
return fileInfo;
- 注意,文件去重方法
distinctFiles
也要同步修改,改为根据outputPath
属性去重
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>(tempFileInfoList.stream()
.flatMap(fileInfo -> fileInfo.getFiles().stream())
.collect(
Collectors.toMap(Meta.FileConfig.FileInfo::getOutputPath, o -> o, (e, r) -> r)
).values());
4. 调整meta.json路径
1. Bug介绍
meta.json
会生成在工作空间内、项目的根目录下,如果输入文件路径是项目的根目录,那么 meta.json
也会被当成项目文件被处理
2. 解决方案
修改 meta.json
的生成路径,调整为工作空间根目录下,和项目目录平级
// 三、生成配置文件
String metaOutputPath = templatePath + File.separator + "meta.json";
2. 参数封装 - 易用性优化
把工具需要的所有参数统一封装为一个对象,通过传递一个 JSON 配置文件(或后续的 HTTP Post 请求)来快速填充参数
- 在
template.model
包下新建TemplateMakerConfig
package com.yupi.maker.template.model;
import com.yupi.maker.meta.Meta;
import lombok.Data;
/**
* 模板制作配置
*/
@Data
public class TemplateMakerConfig {
private Long id;
private Meta meta = new Meta();
private String originProjectPath;
TemplateMakerFileConfig fileConfig = new TemplateMakerFileConfig();
TemplateMakerModelConfig modelConfig = new TemplateMakerModelConfig();
}
TemplateMaker
工具类中新增接受该封装类的重载方法
package com.yupi.maker.template;
public class TemplateMaker {
/**
* 制作模板
*/
public static long makeTemplate(TemplateMakerConfig templateMakerConfig) {
Meta meta = templateMakerConfig.getMeta();
String originProjectPath = templateMakerConfig.getOriginProjectPath();
TemplateMakerFileConfig templateMakerFileConfig = templateMakerConfig.getFileConfig();
TemplateMakerModelConfig templateMakerModelConfig = templateMakerConfig.getModelConfig();
TemplateMakerOutputConfig templateMakerOutputConfig = templateMakerConfig.getOutputConfig();
Long id = templateMakerConfig.getId();
return makeTemplate(meta, originProjectPath, templateMakerFileConfig, templateMakerModelConfig, templateMakerOutputConfig, id);
}
}
resources
资源目录下新建临时templateMaker.json
,用于给封装对象设置参数
{
"meta": {
"name": "acm-template-pro-generator",
"description": "ACM 示例模板生成器"
},
"originProjectPath": "../../../yuzi-generator-demo-projects/springboot-init",
"fileConfig": {
"files": [
{
"path": "src/main/java/com/yupi/springbootinit/common"
}
]
},
"modelConfig": {
"models": [
{
"fieldName": "className",
"type": "String",
"defaultValue": true,
"replaceText": "BaseResponse"
}
]
}
}
- 编写单元测试,读取 JSON 并转换为配置对象
/**
* 使用 JSON 制作模板
*/
@Test
public void testMakeTemplateWithJSON() {
String configStr = ResourceUtil.readUtf8Str("templateMaker.json");
TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);
long id = TemplateMaker.makeTemplate(templateMakerConfig);
System.out.println(id);
}
- 测试执行,生成了符合预期的代码
3. SpringBoot生成器
制作思路:
- 通过一步一步编写工具所需的配置文件,自动生成模板和元信息文件,依次完成动态生成需求
- 然后再通过工具的生成能力,得到可执行的生成器
1. 项目基本信息
1. 编写配置
{
"id": 1,
"meta": {
"name": "springboot-init-generator",
"description": "Spring Boot 模板项目生成器"
},
"originProjectPath": "../../../yuzi-generator-demo-projects/springboot-init"
}
/**
* 制作 SpringBoot 模板
*/
@Test
public void test() {
String rootPath = "examples/springboot-init/";
String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker.json");
TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);
long id = TemplateMaker.makeTemplate(templateMakerConfig);
System.out.println("id = " + id);
}
运行,结果报错啦!
2. 增加非空校验
1. makeFileTemplates()
抽象出 makeFileTemplates()
(制作文件模板的逻辑),补充文件非空校验
private static List<Meta.FileConfig.FileInfo> makeFileTemplates(TemplateMakerFileConfig templateMakerFileConfig
,TemplateMakerModelConfig templateMakerModelConfig
,String sourceRootPath) {
// 遍历输入文件
List<Meta.FileConfig.FileInfo> newFileInfoList = new ArrayList<>();
// 非空校验
if (templateMakerFileConfig == null) {
return newFileInfoList;
}
List<TemplateMakerFileConfig.FileInfoConfig> fileConfigInfoList = templateMakerFileConfig.getFiles();
// 非空校验
if (CollUtil.isEmpty(fileConfigInfoList)) {
return newFileInfoList;
}
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());
// 不处理已生成的 FTL 模板文件
fileList = fileList.stream()
.filter(file -> !file.getAbsolutePath().endsWith(".ftl"))
.collect(Collectors.toList());
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);
}
return newFileInfoList;
}
2. getModelInfoList()
抽象出 getModelInfoList()
(模型配置列表的逻辑),补充模型非空校验
private static List<Meta.ModelConfig.ModelInfo> getModelInfoList(TemplateMakerModelConfig templateMakerModelConfig) {
// - 本次新增的模型配置列表
List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>();
if (templateMakerModelConfig == null) {
return newModelInfoList;
}
List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();
if (CollUtil.isEmpty(models)) {
return newModelInfoList;
}
// - 转换为配置接受的 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());
// - 如果是模型组
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);
}
return newModelInfoList;
}
再次制作模板,成功生成包含项目基本信息的元信息配置文件
2. 需求 - 替换包名
允许用户传入 basePackage
模型参数,对 SpringBoot 中所有出现包名的地方进行替换(eg:@MapperScan
注解里也有包名)
1. 持久化项目路径
- 先完善一下配置追加能力。如果非首次制作,其实配置文件中肯定已经存在了
originProjectPath
参数,那么后续制作时,不需要再在配置文件中指定该参数 - 工具只有获取
sourceRootPath
时用到了originProjectPath
,修改该变量的获取方式,自动读取工作空间下的第一个目录(项目根目录)即可- 注意:获取第一个目录时,需要设置层级为 1,且必须读取目录而不是文件。否则可能会因为
.DS_Store
等系统临时生成的文件干扰结果
- 注意:获取第一个目录时,需要设置层级为 1,且必须读取目录而不是文件。否则可能会因为
/**
* 制作模板
*/
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();
String sourceRootPath = FileUtil.loopFiles(new File(templatePath), 1, null)
.stream()
.filter(File::isDirectory)
.findFirst()
.orElseThrow(RuntimeException::new)
.getAbsolutePath();
// 注意 win 系统需要对路径进行转义
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
// 二、生成文件模板
List<Meta.FileConfig.FileInfo> newFileInfoList = makeFileTemplates(templateMakerFileConfig,
templateMakerModelConfig, sourceRootPath);
// 处理模型信息
List<Meta.ModelConfig.ModelInfo> newModelInfoList = getModelInfoList(templateMakerModelConfig);
// 三、生成配置文件
String metaOutputPath = templatePath + 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;
}
2. 测试执行
全局替换 springboot-init
所有文件的 com.yupi
字符串为 basePackage
模型参数
{
"id": 1,
"fileConfig": {
"files": [
{
"path": ""
}
]
},
"modelConfig": {
"models": [
{
"fieldName": "basePackage",
"type": "String",
"description": "基础包名",
"defaultValue": "com.yupi",
"replaceText": "com.yupi"
}
]
}
}
/**
* 制作 SpringBoot 模板
*/
@Test
public void test() {
String rootPath = "examples/springboot-init/";
String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker1.json");
TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);
long id = makeTemplate(templateMakerConfig);
System.out.println("id = " + id);
}
执行成功,生成的模板和元信息配置文件符合预期
3. 需求 - 控制生成帖子
允许用户传入 needPost
模型参数,控制帖子功能相关的文件是否生成(PostController
、PostService
、PostMapper
、PostMapper.xml
、Post.java
等)
1. 测试执行
帖子相关文件分散在不同的目录下,使用文件过滤器,只保留文件名包含 Post
。并设置为同一个文件组,needPost
控制是否生成
{
"id": 1,
"fileConfig": {
"fileGroupConfig": {
"groupKey": "post",
"groupName": "帖子文件组",
"condition": "needPost"
},
"files": [
{
"path": "src/main",
"filterConfigList": [
{
"range": "fileName",
"rule": "contains",
"value": "Post"
}
]
}
]
},
"modelConfig": {
"models": [
{
"fieldName": "needPost",
"type": "boolean",
"description": "是否开启帖子功能",
"defaultValue": true
}
]
}
}
查看生成的元信息配置,发现文件分组正确生成
问题:前面制作时,已经生成过帖子相关的模板文件,现在分组内新增了相同文件,导致同一个文件在组内外多次重复出现
2. 自定义去重
- 在
template.model
包下新建TemplateMakerOutputConfig
输出配置类,并定义一个控制分组去重的属性
package com.yupi.maker.template.model;
@Data
public class TemplateMakerOutputConfig {
// 从未分组文件中移除组内的同名文件
private boolean removeGroupFilesFromRoot = true;
}
TemplateMakerConfig
中补充输出配置属性
@Data
public class TemplateMakerConfig {
private Long id;
private Meta meta = new Meta();
private String originProjectPath;
TemplateMakerFileConfig fileConfig = new TemplateMakerFileConfig();
TemplateMakerModelConfig modelConfig = new TemplateMakerModelConfig();
TemplateMakerOutputConfig outputConfig = new TemplateMakerOutputConfig();
}
- 编写分组去重的实现代码
- 由于去重逻辑比较复杂,而且算是一个额外的能力,所以建议单独编写一个工具类实现
/**
* 模板制作工具类
*/
public class TemplateMakerUtils {
/**
* 从未分组文件中移除组内的同名文件
*/
public static List<Meta.FileConfig.FileInfo> removeGroupFilesFromRoot(List<Meta.FileConfig.FileInfo> fileInfoList) {
// 1. 先获取到所有分组
List<Meta.FileConfig.FileInfo> groupFileInfoList = fileInfoList.stream()
.filter(fileInfo -> StrUtil.isNotBlank(fileInfo.getGroupKey()))
.collect(Collectors.toList());
// 2. 获取所有分组内的文件列表
List<Meta.FileConfig.FileInfo> groupInnerFileInfoList = groupFileInfoList.stream()
.flatMap(fileInfo -> fileInfo.getFiles().stream())
.collect(Collectors.toList());
// 3. 获取所有分组内文件输入路径集合
Set<String> fileInputPathSet = groupInnerFileInfoList.stream()
.map(Meta.FileConfig.FileInfo::getInputPath)
.collect(Collectors.toSet());
// 4. 移除所有输入路径在 set 中的外层文件
return fileInfoList.stream()
.filter(fileInfo -> !fileInputPathSet.contains(fileInfo.getInputPath()))
.collect(Collectors.toList());
}
}
- 增加输出配置参数,并根据配置执行文件分组合并
/**
* 制作模板
*/
public static long makeTemplate(Meta newMeta, String originProjectPath,
TemplateMakerFileConfig templateMakerFileConfig,
TemplateMakerModelConfig templateMakerModelConfig,
TemplateMakerOutputConfig templateMakerOutputConfig,
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();
String sourceRootPath = FileUtil.loopFiles(new File(templatePath), 1, null)
.stream()
.filter(File::isDirectory)
.findFirst()
.orElseThrow(RuntimeException::new)
.getAbsolutePath();
// 注意 win 系统需要对路径进行转义
sourceRootPath = sourceRootPath.replaceAll("\\\\", "/");
// 二、生成文件模板
List<Meta.FileConfig.FileInfo> newFileInfoList = makeFileTemplates(templateMakerFileConfig,
templateMakerModelConfig, sourceRootPath);
// 处理模型信息
List<Meta.ModelConfig.ModelInfo> newModelInfoList = getModelInfoList(templateMakerModelConfig);
// 三、生成配置文件
String metaOutputPath = templatePath + 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. 额外的输出配置
if (templateMakerOutputConfig != null) {
// 文件外层和分组去重
if (templateMakerOutputConfig.isRemoveGroupFilesFromRoot()) {
List<Meta.FileConfig.FileInfo> fileInfoList = newMeta.getFileConfig().getFiles();
newMeta.getFileConfig().setFiles(TemplateMakerUtils.removeGroupFilesFromRoot(fileInfoList));
}
}
// 2. 输出元信息文件
FileUtil.writeUtf8String(JSONUtil.toJsonPrettyStr(newMeta), metaOutputPath);
return id;
}
再次执行测试,分组内的文件不会在外层文件中重复出现了,符合预期
4. 需求 - 控制跨域
允许用户传入 needCors
模型参数,控制跨域相关的文件 CorsConfig.java
是否生成
1. 编写配置
设置文件路径 CorsConfig.java
、新增模型参数 needCors
,注意还要给文件配置指定一个生成条件
{
"id": 1,
"fileConfig": {
"files": [
{
"path": "src/main/java/com/yupi/springbootinit/config/CorsConfig.java",
"condition": "needCors"
}
]
},
"modelConfig": {
"models": [
{
"fieldName": "needCors",
"type": "boolean",
"description": "是否开启跨域功能",
"defaultValue": true
}
]
}
}
2. 支持给单文件指定条件
- 文件配置类
TemplateMakerFileConfig$FileInfoConfig
,补充condition
条件参数
@Data
public class TemplateMakerFileConfig {
private List<FileInfoConfig> files;
private FileGroupConfig fileGroupConfig;
@NoArgsConstructor
@Data
public static class FileInfoConfig {
private String path;
private String condition;
private List<FileFilterConfig> filterConfigList;
}
@Data
public static class FileGroupConfig {
private String condition;
private String groupKey;
private String groupName;
}
}
makeFileTemplate()
新增fileConfig
对象的传递,支持从配置中取出condition
并填充fileInfo
对象
/**
* 制作文件模板
*/
private static Meta.FileConfig.FileInfo makeFileTemplate(
TemplateMakerModelConfig templateMakerModelConfig, String sourceRootPath, File inputFile,
TemplateMakerFileConfig.FileInfoConfig fileInfoConfig) {
// 要挖坑的文件绝对路径(用于制作模板)
// 注意 win 系统需要对路径进行转义
String fileInputAbsolutePath = inputFile.getAbsolutePath().replaceAll("\\\\", "/");
String fileOutputAbsolutePath = fileInputAbsolutePath + ".ftl";
// 文件输入输出相对路径(用于生成配置)
String fileInputPath = fileInputAbsolutePath.replace(sourceRootPath + "/", "");
String fileOutputPath = fileInputPath + ".ftl";
// 使用字符串替换,生成模板文件
String fileContent;
// 如果已有模板文件,说明不是第一次制作,则在模板基础上再次挖坑
boolean hasTemplateFile = FileUtil.exist(fileOutputAbsolutePath);
if (hasTemplateFile) {
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(fileOutputPath);
fileInfo.setOutputPath(fileInputPath);
fileInfo.setCondition(fileInfoConfig.getCondition());
fileInfo.setType(FileTypeEnum.FILE.getValue());
fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getValue());
// 是否更改了文件内容
boolean contentEquals = newFileContent.equals(fileContent);
// 之前不存在模板文件,并且没有更改文件内容,则为静态生成
if (!hasTemplateFile) {
if (contentEquals) {
// 输入路径没有 FTL 后缀
fileInfo.setInputPath(fileInputPath);
fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getValue());
} else {
// 没有模板文件,需要挖坑,生成模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
}
} else if (!contentEquals) {
// 有模板文件,且增加了新坑,生成模板文件
FileUtil.writeUtf8String(newFileContent, fileOutputAbsolutePath);
}
return fileInfo;
}
3. 测试执行
执行成功,查看生成的元信息配置,发现跨域文件和模型配置正确生成
5. 需求 - 自定义Knife4jConfig
- 先让用户输入
needDocs
参数,决定是否开启接口文档配置 - 如果开启,再让用户输入一组参数,能够修改 Knife4jConfig 文件中的配置
实现思路:修改 Knife4jConfig 文件中的配置。eg:接口文档的标题、描述、版本号等
1. 完善ModelGroup配置
实现这个需求,给模型分组配置增加一些字段。eg:分组的类型、描述
- 修改
TemplateMakerModelConfig$ModelGroupConfig
@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;
private String type;
private String description;
}
}
- 获取模型信息列表时,需要将配置中指定的分组信息传递给
groupModelInfo
对象- 修改
getModelInfoList()
,通过BeanUtil.copyProperties()
拷贝对象的属性值
- 修改
private static List<Meta.ModelConfig.ModelInfo> getModelInfoList(TemplateMakerModelConfig templateMakerModelConfig) {
// - 本次新增的模型配置列表
List<Meta.ModelConfig.ModelInfo> newModelInfoList = new ArrayList<>();
if (templateMakerModelConfig == null) {
return newModelInfoList;
}
List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();
if (CollUtil.isEmpty(models)) {
return newModelInfoList;
}
// - 转换为配置接受的 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());
// - 如果是模型组
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();
BeanUtil.copyProperties(modelGroupConfig, groupModelInfo);
// groupModelInfo.setGroupKey(groupKey);
// groupModelInfo.setGroupName(groupName);
// groupModelInfo.setCondition(condition);
// 模型全放到一个分组内
groupModelInfo.setModels(inputModelInfoList);
newModelInfoList.add(groupModelInfo);
} else {
// 不分组,添加所有的模型信息到列表
newModelInfoList.addAll(inputModelInfoList);
}
return newModelInfoList;
}
2. 编写配置
分 2 步去制作模板,不要让每次制作的模型参数混在同一组,更清晰一些
- 先控制接口文档文件是否生成
- 再指定修改接口文档内容的参数
- 先控制文件是否生成
{
"id": 1,
"fileConfig": {
"files": [
{
"path": "src/main/java/com/yupi/springbootinit/config/Knife4jConfig.java",
"condition": "needDocs"
}
]
},
"modelConfig": {
"models": [
{
"fieldName": "needDocs",
"type": "boolean",
"description": "是否开启接口文档功能",
"defaultValue": true
}
]
}
}
- 再定义一组配置,控制接口文档文件的内容
{
"id": 1,
"fileConfig": {
"files": [
{
"path": "src/main/java/com/yupi/springbootinit/config/Knife4jConfig.java",
"condition": "needDocs"
}
]
},
"modelConfig": {
"modelGroupConfig": {
"groupKey": "docsConfig",
"groupName": "接口文档配置",
"type": "DocsConfig",
"description": "用于生成接口文档配置",
"condition": "needDocs"
},
"models": [
{
"fieldName": "title",
"type": "String",
"description": "接口文档标题",
"defaultValue": "接口文档",
"replaceText": "接口文档"
},
{
"fieldName": "description",
"type": "String",
"description": "接口文档描述",
"defaultValue": "springboot-init",
"replaceText": "springboot-init"
},
{
"fieldName": "version",
"type": "String",
"description": "接口文档版本",
"defaultValue": "1.0",
"replaceText": "1.0"
}
]
}
}
3. 测试执行
执行成功,查看生成的元信息配置,发现模型分组正确生成
6. 需求 - 自定义MySQL
允许用户传入一组 MySQL 数据库模型参数,修改 application.yml
配置文件中 MySQL 的 url
、username
、password
的值
1. 编写配置
{
"id": 1,
"fileConfig": {
"files": [
{
"path": "src/main/resources/application.yml"
}
]
},
"modelConfig": {
"modelGroupConfig": {
"groupKey": "mysqlConfig",
"groupName": "MySQL数据库配置",
"type": "MysqlConfig",
"description": "用于生成MySQL数据库配置"
},
"models": [
{
"fieldName": "url",
"type": "String",
"description": "地址",
"defaultValue": "jdbc:mysql://localhost:3306/my_db",
"replaceText": "jdbc:mysql://localhost:3306/my_db"
},
{
"fieldName": "username",
"type": "String",
"description": "用户名",
"defaultValue": "root",
"replaceText": "root"
},
{
"fieldName": "password",
"type": "String",
"description": "密码",
"defaultValue": "123456",
"replaceText": "123456"
}
]
}
}
2. 测试执行
执行成功,模型分组正确生成
7. 需求 - 控制Redis
传入 needRedis
模型参数,控制是否开启和 Redis 相关代码。控制修改 application.yml
、pom.xml
、MainApplication.java
中的部分代码
- 这个需求比较定制化,因为每个文件和 Redis 有关的代码都不一样,所以人工修改模板文件并“挖坑”
1. ftl修改
<#if needRedis>
# Redis 配置
redis:
database: 1
host: localhost
port: 6379
timeout: 5000
password: 123456
</#if>
@SpringBootApplication<#if !needRedis>(exclude = {RedisAutoConfiguration.class})</#if>
<#if needRedis>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
</#if>
2. 编写配置
依然使用工具来生成元信息配置
{
"id": 1,
"fileConfig": {
"files": [
{
"path": "src/main/resources/application.yml"
},
{
"path": "src/main/java/com/yupi/springbootinit/MainApplication.java"
},
{
"path": "pom.xml"
}
]
},
"modelConfig": {
"models": [
{
"fieldName": "needRedis",
"type": "boolean",
"description": "是否开启Redis功能",
"defaultValue": true
}
]
}
}
3. 测试执行
执行成功,控制 Redis 的模型参数正确生成
8. 需求 - 控制ES
传入 needEs
模型参数,控制是否开启 Elasticsearch 相关的代码。需要修改和 Elasticsearch 相关的代码
PostController
、PostService
、PostServiceImpl
、application.yml
文件的部分代码needEs
控制PostEsDTO
文件是否生成
1. ftl修改
<#if needEs>
/**
* 分页搜索(从 ES 查询)
*
* @param postQueryRequest
* @return
*/
@PostMapping("/search/page")
public BaseResponse<Page<Post>> searchPostByPage(@RequestBody PostQueryRequest postQueryRequest) {
long size = postQueryRequest.getPageSize();
// 限制爬虫
ThrowUtils.throwIf(size > 20, ErrorCode.PARAMS_ERROR);
Page<Post> postPage = postService.searchFromEs(postQueryRequest);
return ResultUtils.success(postPage);
}
</#if>
<#if needEs>
/**
* 从 ES 查询
*
* @param postQueryRequest
* @return
*/
Page<Post> searchFromEs(PostQueryRequest postQueryRequest);
</#if>
<#if needEs>
@Override
public Page<Post> searchFromEs(PostQueryRequest postQueryRequest) {
Long id = postQueryRequest.getId();
Long notId = postQueryRequest.getNotId();
String searchText = postQueryRequest.getSearchText();
String title = postQueryRequest.getTitle();
String content = postQueryRequest.getContent();
List<String> tagList = postQueryRequest.getTags();
List<String> orTagList = postQueryRequest.getOrTags();
Long userId = postQueryRequest.getUserId();
// es 起始页为 0
long current = postQueryRequest.getCurrent() - 1;
long pageSize = postQueryRequest.getPageSize();
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
// 过滤
boolQueryBuilder.filter(QueryBuilders.termQuery("isDelete", 0));
if (id != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("id", id));
}
if (notId != null) {
boolQueryBuilder.mustNot(QueryBuilders.termQuery("id", notId));
}
if (userId != null) {
boolQueryBuilder.filter(QueryBuilders.termQuery("userId", userId));
}
// 必须包含所有标签
if (CollectionUtil.isNotEmpty(tagList)) {
for (String tag : tagList) {
boolQueryBuilder.filter(QueryBuilders.termQuery("tags", tag));
}
}
// 包含任何一个标签即可
if (CollectionUtil.isNotEmpty(orTagList)) {
BoolQueryBuilder orTagBoolQueryBuilder = QueryBuilders.boolQuery();
for (String tag : orTagList) {
orTagBoolQueryBuilder.should(QueryBuilders.termQuery("tags", tag));
}
orTagBoolQueryBuilder.minimumShouldMatch(1);
boolQueryBuilder.filter(orTagBoolQueryBuilder);
}
// 按关键词检索
if (StringUtils.isNotBlank(searchText)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("description", searchText));
boolQueryBuilder.should(QueryBuilders.matchQuery("content", searchText));
boolQueryBuilder.minimumShouldMatch(1);
}
// 按标题检索
if (StringUtils.isNotBlank(title)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("title", title));
boolQueryBuilder.minimumShouldMatch(1);
}
// 按内容检索
if (StringUtils.isNotBlank(content)) {
boolQueryBuilder.should(QueryBuilders.matchQuery("content", content));
boolQueryBuilder.minimumShouldMatch(1);
}
// 分页
PageRequest pageRequest = PageRequest.of((int) current, (int) pageSize);
// 构造查询
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder().withQuery(boolQueryBuilder)
.withPageable(pageRequest).build();
SearchHits<PostEsDTO> searchHits = elasticsearchRestTemplate.search(searchQuery, PostEsDTO.class);
Page<Post> page = new Page<>();
page.setTotal(searchHits.getTotalHits());
List<Post> resourceList = new ArrayList<>();
// 查出结果后,从 db 获取最新动态数据(比如点赞数)
if (searchHits.hasSearchHits()) {
List<SearchHit<PostEsDTO>> searchHitList = searchHits.getSearchHits();
List<Long> postIdList = searchHitList.stream().map(searchHit -> searchHit.getContent().getId())
.collect(Collectors.toList());
List<Post> postList = baseMapper.selectBatchIds(postIdList);
if (postList != null) {
Map<Long, List<Post>> idPostMap = postList.stream().collect(Collectors.groupingBy(Post::getId));
postIdList.forEach(postId -> {
if (idPostMap.containsKey(postId)) {
resourceList.add(idPostMap.get(postId).get(0));
} else {
// 从 es 清空 db 已物理删除的数据
String delete = elasticsearchRestTemplate.delete(String.valueOf(postId), PostEsDTO.class);
log.info("delete post {}", delete);
}
});
}
}
page.setRecords(resourceList);
return page;
}
</#if>
<#if needEs>
# Elasticsearch 配置
elasticsearch:
uris: http://localhost:9200
username: root
password: 123456
</#if>
2. 编写配置
{
"id": 1,
"fileConfig": {
"files": [
{
"path": "src/main/java/com/yupi/springbootinit/model/dto/post/PostEsDTO.java",
"condition": "needPost && needEs"
}
]
},
"modelConfig": {
"models": [
{
"fieldName": "needEs",
"type": "boolean",
"description": "是否开启ES功能",
"defaultValue": true
}
]
}
}
3. 测试执行
控制 Elasticsearch 的模型参数正确生成
但是,PostEsDTO
文件配置却没有 condition
条件
- 因为
PostEsDTO
文件已经属于 Post 组,会被组内已有的配置覆盖 - 手动调整下
PostEsDTO
的生成策略,将它移动到组外,并指定 condition 条件,同时开启 Post 和 Es 时才生成
{
"inputPath": "src/main/java/com/yupi/springbootinit/model/dto/post/PostEsDTO.java.ftl",
"outputPath": "src/main/java/com/yupi/springbootinit/model/dto/post/PostEsDTO.java",
"type": "file",
"generateType": "dynamic",
"condition": "needPost && needEs"
}
4. 测试成果
至此,所有的需求都已经实现,接下来到了激动人心的时刻,终于可以验证成果了!!!
1. 制作生成器
- 首先将已生成的
meta.json
文件,复制为yuzi-generator-maker/resource/springboot-init-meta.json
- 然后修改
MetaManager
类,加载该文件
public class MetaManager {
private static volatile Meta meta;
private MetaManager() {
// 私有构造函数,防止外部实例化
}
public static Meta getMetaObject() {
if (meta == null) {
synchronized (MetaManager.class) {
if (meta == null) {
meta = initMeta();
}
}
}
return meta;
}
private static Meta initMeta() {
// String metaJson = ResourceUtil.readUtf8Str("meta.json");
String metaJson = ResourceUtil.readUtf8Str("springboot-init-meta.json");
Meta newMeta = JSONUtil.toBean(metaJson, Meta.class);
// 校验和处理默认值
MetaValidator.doValidAndFill(newMeta);
return newMeta;
}
}
- 最后执行工具
main()
,制作生成器- 结果,
CommandLine
对象冲突啦! - 修改
GenerateCommand.java.ftl
的命令调用生成代码,根据模型的groupKey
生成命令行对象名称
- 结果,
<#-- 生成命令调用 -->
<#macro generateCommand indent modelInfo>
${indent}System.out.println("输入${modelInfo.groupName}配置:");
${indent}CommandLine ${modelInfo.groupKey}CommandLine = new CommandLine(${modelInfo.type}Command.class);
${indent}${modelInfo.groupKey}CommandLine.execute(${modelInfo.allArgsStr});
</#macro>
- 再次执行
main()
- 配置文件没有手动指定
inputRootPath
,在MetaValidator
校验器中自动生成了,而生成的逻辑有一些错误 - 需要修改默认生成的路径,确保使用
/
符号分隔目录
- 配置文件没有手动指定
public static void validAndFillFileConfig(Meta meta) {
// fileConfig 默认值
Meta.FileConfig fileConfig = meta.getFileConfig();
if (fileConfig == null) {
return;
}
// sourceRootPath:必填
String sourceRootPath = fileConfig.getSourceRootPath();
if (StrUtil.isBlank(sourceRootPath)) {
throw new MetaException("未填写 sourceRootPath");
}
// inputRootPath:.source + sourceRootPath 的最后一个层级路径
String inputRootPath = fileConfig.getInputRootPath();
String defaultInputRootPath = ".source/" + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).getFileName().toString();
if (StrUtil.isEmpty(inputRootPath)) {
fileConfig.setInputRootPath(defaultInputRootPath);
}
- 打包成功!得到了一个 Spring Boot 生成器
2. 测试使用
- 查看生成命令的帮助手册
- 查看模型参数
- 查看模型列表文件
- 生成文件
./generator generate --needPost=false
- 查看生成的代码,没有产生 Post 相关文件,符合预期
# `needRedis=false`,`needPost` 为默认值 true
./generator generate --needRedis=false
- 结果生成报错,
PostMapper.xml
文件错误- 原因:MyBatis 动态参数语法字符串和 FreeMarker 模板的参数替换语法冲突
- 使用
<#noparse>
语法可以设置某些字符串不被 FreeMarker 解析,修改
<select id="listPostWithDelete" resultType="${basePackage}.springbootinit.model.entity.Post">
select *
from post
where updateTime >= <#noparse>#{minUpdateTime}</#noparse>
</select>
- 再次运行命令,这次可以成功生成。更多命令测试 💯
# 不会生成 Elasticsearch 代码
./generator generate --needEs=false
# 控制文档是否生成
./generator generate --needDocs=false