第八章 模板项目生成
第八章 模板项目生成
创建时间: 2025年4月23日 17:06
状态: Done
前言
本笔记主要用途是学习up 主程序员鱼皮的项目:代码生成器时的一些学习心得。
代码地址:https://github.com/Liucc-123/yuzi-generator.git
项目教程:https://www.codefather.cn/course/1790980795074654209
上一章节内容:https://editor.csdn.net/md/?articleId=147758678
本节重点
本章节仍属于项目第二阶段 — 开发代码生成器制作工具。
本章节主要是完善制作工具,属于成果检验阶段。
本节重要内容:
- 模板制作工具 - Bug 修复
- 模板制作工具 - 易用性优化
- 制作 Spring Boot 项目模板生成器
- 测试成果
- 扩展思路
一、Bug修复
1、相同配置下,制作工具多次执行,配置信息generateType
会被强制变为static
类型。
问题:执行TemplateMaker的main方法,第一次执行时,generateType
是dynamic
类型,再执行一次会变为static
类型
分析:这是因为之前的逻辑:仅仅是判断原始内容和替换后的内容是否一致来判断赋值dynamic
还是static
。因为第一次制作ftl文件已经生成,第二次执行参数是完全一样的,因此原始内容和新内容一样,程序就认为是static
类型。
解决:优化逻辑:内容发生变化 or ftl文件已经存在,就认为是dynamic
类型,否则是static
类型。
修改的代码如下:
/*** 单次制作模板文件** @param sourceRootPath 输入文件根路径* @param inputFile 输入文件* @param templateMakerModelConfig 支持一组模型参数对单个文件进行挖坑* @return*/
private static Meta.FileConfigDTO.FileInfo makeFileTemplate(String sourceRootPath, File inputFile, TemplateMakerModelConfig templateMakerModelConfig) {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;// 非首次制作,可以在已有模板文件的基础上再次挖坑boolean hasTemplateFile = FileUtil.exist(fileOutputAbsolutePath);if (hasTemplateFile) {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);}}Meta.FileConfigDTO.FileInfo fileInfo = new Meta.FileConfigDTO.FileInfo();fileInfo.setInputPath(fileInputPath);fileInfo.setType(FileTypeEnum.FILE.getType());// 模板文件内容未发生变化,则生成静态文件// 模板内容发生变化 || 存在FTL文件,则生成动态文件boolean isChanged = !StrUtil.equals(originalContent, newContent);if(isChanged || hasTemplateFile){fileInfo.setOutputPath(fileOutputPath);fileInfo.setGenerateType(FileGenerateTypeEnum.DYNAMIC.getType());FileUtil.writeUtf8String(newContent, fileOutputAbsolutePath);}else {fileInfo.setOutputPath(fileInputPath);fileInfo.setGenerateType(FileGenerateTypeEnum.STATIC.getType());}return fileInfo;
}
测试执行main方法,可以看到无论执行多少次,files的generateType
已经变为dynamic
。
二、参数封装-易用性优化
目前制作工具的参数过于复杂,从单元测试类中可以看出,要想使用调用制作工具生成代码,需要提供的参数额外的多:
优化思路
我们对其进行优化:
1、将所有的参数封装到一个对象中
2、这个对象需要的参数,我们通过一个配置文件进行传递
编码实现
1)在template.model
包下新建TemplateMakerConfig
配置类,封装制作工具所需要的参数,示例代码如下:
package com.liucc.maker.template.model;import com.liucc.maker.meta.Meta;
import lombok.Data;/*** 模板制作配置*/
@Data
public class TemplateMakerConfig {private Long id;private String sourceRootPath;private Meta meta = new Meta();private TemplateMakerFileConfig fileConfig = new TemplateMakerFileConfig();private TemplateMakerModelConfig modelConfig = new TemplateMakerModelConfig();
}
2)调整制作工具方法makeTemplate
技巧:我们不要直接修改makeTemplate方法,而是通过重载方法,重新定义一个方法,参数使用TemplateMakerConfig
接收参数。在这个方法里最终调用之前的制作工具方法makeTemplate
/*** 模板制作** @param templateMakerConfig 模板制作配置 对象* @return*/
public static Long makeTemplate(TemplateMakerConfig templateMakerConfig){Long id = templateMakerConfig.getId();String sourceRootPath = templateMakerConfig.getSourceRootPath();Meta meta = templateMakerConfig.getMeta();TemplateMakerFileConfig fileConfig = templateMakerConfig.getFileConfig();TemplateMakerModelConfig modelConfig = templateMakerConfig.getModelConfig();return makeTemplate(id, sourceRootPath, meta, fileConfig, modelConfig);
}
3)在resources
目录下编写templateMaker.json
配置文件,主要映射制作工具方法所需要的参数:
{"meta": {"name": "acm-template-pro-generator","description": "ACM 示例模板生成器"},"sourceRootPath": "../../../yuzi-generator-demo-projects/springboot-init","fileConfig": {"files": [{"path": "src/main/java/com/liucc/springbootinit/common"}]},"modelConfig": {"models": [{"fieldName": "className","type": "String","defaultValue": true,"replaceText": "BaseResponse"}]}
}
这里解释一下这个相对路径问题,为什么是向上找三级?
最终用到sourceRootPath参数的是下图中的hutool工具类使用到的,点击查看这个copy方法的源码,可以发现,相对路径自动是从ClassPath开始寻找的,当前位置也就是在classes目录下,那么上一级目录就是target,上上一级目录就是yuzi-generator-marker,上上上一级目录就是yuzi-generator目录了。
三、制作SpringBoot项目模板生成器
下面将实现本阶段的最终目标 — 制作SpringBoot项目模板生成器。
实现思路:通过一步一步编写模板制作工具所需要的配置文件(templateMaker.json),自动生成模板文件(FTL文件)和元信息文件(meta.json);然后再通过制作工具的生成能力,得到可执行的代码生成器项目。
1、需求:项目基础信息
编写配置
首先编写制作工具运行所需要的参数配置文件,在resources
目录下新建examples/springboot-init/templateMaker.json
配置文件。首先编写项目生成器的基础信息,配置如下:
{"id": 1,"meta": {"name": "springboot-init-generator","description": "springboot-init 模板生成器"},"sourceRootPath": "../../../yuzi-generator-demo-projects/springboot-init"
}
测试执行
编写单元测试方法,代码如下:
@Test
public void testMakeSpringBootProjectTemplate(){// 读取资源文件 templateMaker.jsonString configStr = ResourceUtil.readUtf8Str("examples/springboot-init/templateMaker.json");TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);Long id = TemplateMaker.makeTemplate(templateMakerConfig);System.out.println("id = " + id);
}
运行,结果报错了:
需要给makeTemplate方法添加部分参数校验逻辑
但是因为之前的makeTemplate方法主逻辑实在是太长了,如果用if快进行包裹,那么代码看起来实在是太不优雅了,容易挨骂。因此首先需要做的是对makeTemplate方法做进一步方法抽取,使得makeTemplate方法看起来简洁一些。抽取过程如下:
1)处理输入文件单独封装为一个方法,通过IDEA的快捷键ctrl+alt+m
快速提取方法。
在makeTemplateFiles
方法内部,对templateMakerConfig.getFiles()
进行非空校验,代码如下:
private static List<Meta.FileConfigDTO.FileInfo> makeTemplateFiles(String sourceRootPath, TemplateMakerFileConfig templateMakerConfig,TemplateMakerModelConfig templateMakerModelConfig) {// 新增文件信息列表List<Meta.FileConfigDTO.FileInfo> newFileInfoList = new ArrayList<>();List<TemplateMakerFileConfig.FileInfoConfig> fileInfoConfigList = templateMakerConfig.getFiles();if (CollUtil.isEmpty(fileInfoConfigList)){System.out.println("文件配置列表为空 fileInfoConfigList:" + fileInfoConfigList);return newFileInfoList;}for (TemplateMakerFileConfig.FileInfoConfig fileInfoConfig : fileInfoConfigList) {String fileInputPath = fileInfoConfig.getPath(); // 相对路径String fileInputAbsolutePath = sourceRootPath + "/" + fileInputPath;// 文件过滤 获取所有符合条件的文件列表(都是文件,不存在目录)List<File> fileList = FileFilter.doFilter(fileInputAbsolutePath, fileInfoConfig.getFilters());// 过滤掉ftl后缀结尾的文件,避免在生成meta元信息文件中,出现将ftl文件作为输入路径的配置项fileList = fileList.stream().filter(file -> !file.getName().endsWith(".ftl")).collect(Collectors.toList());for (File file : fileList) {Meta.FileConfigDTO.FileInfo fileInfo = makeFileTemplate(sourceRootPath, file, templateMakerModelConfig);newFileInfoList.add(fileInfo);}}// 新增文件组配置TemplateMakerFileConfig.FileGroupConfig fileGroupConfig = templateMakerConfig.getFileGroupConfig();if (BeanUtil.isNotEmpty(fileGroupConfig) && StrUtil.isNotBlank(fileGroupConfig.getGroupKey())){ // 说明是文件组String condition = fileGroupConfig.getCondition();String groupKey = fileGroupConfig.getGroupKey();String groupName = fileGroupConfig.getGroupName();Meta.FileConfigDTO.FileInfo fileInfoGroup = new Meta.FileConfigDTO.FileInfo();fileInfoGroup.setType(FileTypeEnum.GROUP.getType());fileInfoGroup.setCondition(condition);fileInfoGroup.setGroupKey(groupKey);fileInfoGroup.setGroupName(groupName);fileInfoGroup.setFiles(newFileInfoList);// 重置 fileInfos 为文件组newFileInfoList = new ArrayList<>();newFileInfoList.add(fileInfoGroup);}return newFileInfoList;
}
2)处理处理模型信息单独封装为一个方法,通过IDEA的快捷键ctrl+alt+m
快速提取方法。
在getModelInfos
方法内部,对templateMakerModelConfig.getModels()
进行非空校验,代码如下:
private static List<Meta.ModelConfigDTO.ModelInfo> getModelInfos(TemplateMakerModelConfig templateMakerModelConfig) {// - 本次新增的模型配置列表List<Meta.ModelConfigDTO.ModelInfo> newModelInfoList = new ArrayList<>();List<TemplateMakerModelConfig.ModelInfoConfig> models = templateMakerModelConfig.getModels();if (CollUtil.isEmpty(models)){System.out.println("未填写模型配置");return newModelInfoList;}// - 转换为元信息可接受的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());// - 针对模型分组进行处理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);}return newModelInfoList;
}
再次测试执行,控制台没有空指针异常啦,可以正常生成meta.json:
2、需求:替换生成的代码包名
明确需求
允许用户传入basePackage
模型参数,对springboot项目中所有出现包名的地方进行替换。如果是人工做全局包名替换,不仅制作成本高(要一个一个编写动态模板文件)、还容易出现遗漏(比如 @MapperScan 注解里也有包名)。
所以我们可以让制作工具自动“挖坑”并生成模板文件。
持久化项目路径
用户第一次制作项目时,传入的sourceRootPath
项目已经持久化到工作空间.temp目录下了,因此我们没必要使得用户每次制作时都必须传入sourceRootPath
,我们程序完全可以获取得到。
优化:判断如果是非首次制作,sourceRootPath
取tempFilePath
下第一个目录,也就是springboot-init
项目。
修改makeTemplate方法,完善sourceRootPath 的取值逻辑:
// 非首次制作,不需要重复拷贝原始项目文件
if (!FileUtil.exist(tempFilePath)) { // 首次制作FileUtil.mkdir(tempFilePath);FileUtil.copy(sourceRootPath, tempFilePath, true);sourceRootPath = tempFilePath + File.separator + FileUtil.getLastPathEle(Paths.get(sourceRootPath)).toString();
} else { // 非首次制作// 说明工作空间项目文件已经存在,程序可以直接读取到,不需要用户重复在配置文件中指定 sourceRootPath 了。sourceRootPath = FileUtil.loopFiles(new File(tempFilePath), 1, null).stream().filter(File::isDirectory).findFirst().orElseThrow(RuntimeException::new).getAbsolutePath();}
注意:使用hutool工具类的loopFiles方法,递归深度为1
、且必须读取目录
,避免会读取到.DS_Store
等系统临时生成的文件。
编写配置
在resources目录下编写examples/springboot-init/templateMaker1.json配置文件,代码如下:
{"id": 1,"fileConfig": {"files": [{"path": ""}]},"modelConfig": {"models": [{"fieldName": "basePackage","description": "基础包名","type": "String","defaultValue": "com.liucc","replaceText": "com.yupi"}]}
}
因为是对整个项目的包名进行替换,所以files.path取值空字符串就可以了,表示根目录
测试执行
编写测试代码,如下:
@Test
public void testMakeSpringBootProjectTemplate(){// 读取资源文件 templateMaker.jsonString rootPath = "examples/springboot-init/";String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker.json");TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);Long id = TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker1.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);System.out.println("id = " + id);
}
执行成功,生成的模板文件和配置文件符合预期:
3、需求:控制是否生成帖子相关功能
明确需求
通过一个模型参数控制一组相关文件是否生成,比如用户传递模型参数needPost,值为true,那么生成器就要生成所有帖子相关的代码文件(controller、service、mapper等),而且这些文件是分布在不同的目录下的。
编写配置
编写templateMaker2.json
配置文件
文件组过滤配置:所有文件名
包含Post
的文件
模型控制参数:needPost
{"id": 1,"fileConfig": {"files": [{"path": "src/main","filters": [{"range": "filename","rule": "contains","value": "Post"}]}],"fileGroupConfig": {"condition": "needPost","groupKey": "post","groupName": "帖子文件组"}},"modelConfig": {"models": [{"fieldName": "needPost","description": "是否开启帖子功能","type": "boolean","defaultValue": true}]}
}
测试执行
编写测试代码
@Test
public void testMakeSpringBootProjectTemplate(){// 读取资源文件 templateMaker.jsonString rootPath = "examples/springboot-init/";String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker.json");TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);Long id = TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker1.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker2.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);System.out.println("id = " + id);
}
测试执行,元信息配置文件正确输出文件组相关配置信息:
但是存在一个问题:最终生成的配置文件会出现重复内容。
这是因为在第一次制作时,会因为替换包名,给PostThumbService.java生成一次动态模板,第二次是文件组选择开启给文件名携带Post
的文件生成动态模板文件,因此导致组外和组内各出现了一次配置项。
自定义去重
一种简单的方式就是直接去除组外的配置,保留组内的配置。可如果用户就是想同时保留组内和组外的配置呢?
因此我们应该将这个冲突合并策略作为一个扩展项(输出配置),交由用户进行决定。默认值选择保留组内配置。
1)定义模板生成器输出配置
在maker.template.model包下新建TemplateMakerOutputConfig
类,代码如下:
package com.liucc.maker.template.model;import lombok.Data;/*** 模板生成器 输出配置*/
@Data
public class TemplateMakerOutputConfig {/*** 移除组外重复文件配置*/private boolean removeMakerOutputFromRoot = true;
}
2)在TemplateMakerConfig类下,增加输出配置属性
package com.liucc.maker.template.model;import com.liucc.maker.meta.Meta;
import lombok.Data;/*** 模板制作配置*/
@Data
public class TemplateMakerConfig {private Long id;private String sourceRootPath;private Meta meta = new Meta();private TemplateMakerFileConfig fileConfig = new TemplateMakerFileConfig();private TemplateMakerModelConfig modelConfig = new TemplateMakerModelConfig();private TemplateMakerOutputConfig outputConfig = new TemplateMakerOutputConfig();
}
3)单独定义输出配置 去重工具类
因为针对组内组外文件名冲突解决是一个单独的逻辑,避免和原有代码制作逻辑混合,我们新定义一个工具类,将输出配置放到这里,然后在制作工具方法中引用这个工具类。
package com.liucc.maker.template;import cn.hutool.core.util.StrUtil;
import com.liucc.maker.meta.Meta;import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;/*** 模板制作工具类*/
public class TemplateMakerUtils {/*** 文件冲突处理,保留组内同名文件** @param fileInfoList* @return*/public static List<Meta.FileConfigDTO.FileInfo> removeNotGroupFileFromRoot(List<Meta.FileConfigDTO.FileInfo> fileInfoList){// 1、获取所有分组文件List<Meta.FileConfigDTO.FileInfo> groupFileList = fileInfoList.stream().filter(file -> StrUtil.isNotBlank(file.getGroupKey())).collect(Collectors.toList());// 2、将所有分组文件打散到一个列表中List<Meta.FileConfigDTO.FileInfo> groupFileFlattenList = groupFileList.stream().flatMap(file -> file.getFiles().stream()).collect(Collectors.toList());// 3、获取所有分组文件的inputPath 集合Set<String> groupInputPathSet = groupFileFlattenList.stream().map(file -> file.getInputPath()).collect(Collectors.toSet());// 4、获取所有未分组文件,过滤掉 inputPath 包含在 分组文件inputPath集合 的文件列表return fileInfoList.stream().filter(fileInfo -> !groupInputPathSet.contains(fileInfo.getInputPath())).collect(Collectors.toList());}
}
4)调整制作工具方法makeTemplate
在最终生成元信息配置文件之前,对文件列表进行处理:
private static void generateMetaConfigFile(String sourceRootPath, Meta meta, String tempFilePath, List<Meta.FileConfigDTO.FileInfo> newFileInfoList,List<Meta.ModelConfigDTO.ModelInfo> newModelInfoList, TemplateMakerOutputConfig templateMakerOutputConfig) {String metaOutputPath = tempFilePath + 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(newFileInfoList);// 模型配置List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = newMeta.getModelConfig().getModels();modelInfoList.addAll(newModelInfoList);// 文件去重newMeta.getFileConfig().setFiles(distinctFiles(fileInfoList));// 配置去重newMeta.getModelConfig().setModels(distinctModels(modelInfoList));// 处理冲突文件if (templateMakerOutputConfig != null && templateMakerOutputConfig.isRemoveMakerOutputFromRoot()) {newMeta.getFileConfig().setFiles(TemplateMakerUtils.removeNotGroupFileFromRoot(newFileInfoList));}// 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(newFileInfoList);fileConfig.setFiles(fileInfoList);Meta.ModelConfigDTO modelConfig = new Meta.ModelConfigDTO();List<Meta.ModelConfigDTO.ModelInfo> modelInfoList = new ArrayList<>();modelInfoList.addAll(newModelInfoList);modelConfig.setModels(modelInfoList);meta.setModelConfig(modelConfig);// 2、输出元信息配置文件String metaJson = JSONUtil.toJsonPrettyStr(meta);FileUtil.writeUtf8String(metaJson, metaOutputPath);}
}
5)执行单元测试
最后生成的元信息文件,配置项没有重复生成。
4、需求:控制是否需要跨域
明确需求
用一个模型参数needCors
控制CorsConfig文件是否生成。
编写配置
在resources目录下编写examples/springboot-init/templateMaker3.json
配置文件,新增模型参数needCors
,控制输入文件src/main/java/com/liucc/springbootinit/config/CorsConfig.java
是否生成。
{"id": 1,"fileConfig": {"files": [{"path": "src/main/java/com/liucc/springbootinit/config/CorsConfig.java","condition": "needCors"}]},"modelConfig": {"models": [{"filedName": "needCors","description": "是否开启跨域","type": "boolean","defaultValue": true}]}
}
我们之前只是给文件组添加控制生成条件参数condition,现在需要让单个文件也具备condition。
修改TemplateMakerFileConfig.java
文件,给FileInfoConfig
增加字段condition
@Data
public static class FileInfoConfig {/*** 文件路径(相对路径)*/private String path;/*** 文件生成条件*/private String condition;/*** 文件过滤器*/private List<FileFilterConfig> filters;
}
修改制作工具单个生成文件方法makeFileTemplate
,其参数列表如下:
/*** 单次制作模板文件** @param sourceRootPath 输入文件根路径* @param inputFile 输入文件* @param templateMakerModelConfig 支持一组模型参数对单个文件进行挖坑* @param fileInfoConfig 文件配置信息* @return*/
private static Meta.FileConfigDTO.FileInfo makeFileTemplate(String sourceRootPath, File inputFile, TemplateMakerModelConfig templateMakerModelConfig,TemplateMakerFileConfig.FileInfoConfig fileInfoConfig) {}
将condition字段的值赋值到fileInfo里去:
测试执行
单元测试方法
@Test
public void testMakeSpringBootProjectTemplate(){// 读取资源文件 templateMaker.jsonString rootPath = "examples/springboot-init/";String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker.json");TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);Long id = TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker1.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker2.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker3.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);System.out.println("id = " + id);
}
执行单元测试,是否跨域相关文件和模型配置正确生成:
5、需求:自定义 Knife4jConfig 接口文档配置
明确需求
通过一个模型参数控制Knife4jConfig.java文件是否生成;如果开启,再让用户输入一组参数,能够修改Knife4jConfig 文件中的配置(接口文档的标题、描述、版本号等)。
编写配置
在resources
目录下编写templateMaker4.json
配置文件,用一个参数needDocs
控制Knife4jConfig.java
文件是否生成。
{"id": 1,"fileConfig": {"files": [{"path": "src/main/java/com/liucc/springbootinit/config/Knife4jConfig.java","condition": "needDocs"}]},"modelConfig": {"models": [{"fieldName": "needDocs","type": "boolean","description": "是否开启接口文档功能","defaultValue": true}]}
}
在resources
目录下编写templateMaker5.json
配置文件,用一组参数替换Knife4jConfig.java
文件中的配置。
{"id": 1,"fileConfig": {"files": [{"path": "src/main/java/com/liucc/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"}]}
}
编写代码
1)完善TemplateMakerModelConfig对象,给分组对象ModelGroupConfig
添加类型
和模型
字段:
@Data
public static class ModelGroupConfig {private String condition;private String groupKey;private String groupName;private String type;private String description;
}
2)修改制作工具获取模型配置列表方法getModelInfos
,将模型分组信息设置到ModelInfo
:
// - 针对模型分组进行处理
TemplateMakerModelConfig.ModelGroupConfig modelGroupConfig = templateMakerModelConfig.getModelGroupConfig();
if (modelGroupConfig != null) {// 是分组,将所有的模型列表添加到分组里Meta.ModelConfigDTO.ModelInfo newModelInfo = new Meta.ModelConfigDTO.ModelInfo();BeanUtil.copyProperties(modelGroupConfig, newModelInfo);newModelInfo.setModels(inputModelInfoList);newModelInfoList.add(newModelInfo);
} else {// 不是分组,添加所有的模型列表newModelInfoList.addAll(inputModelInfoList);
}
测试执行
编写单元测试方法:
@Test
public void testMakeSpringBootProjectTemplate(){// 读取资源文件 templateMaker.jsonString rootPath = "examples/springboot-init/";String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker.json");TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);Long id = TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker1.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker2.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker3.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker4.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker5.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);System.out.println("id = " + id);
}
测试执行,控制knif4jConfig.java相关文件和模型配置正确输出:
6、需求:自定义 MySQL 配置信息
明确需求
允许用户传入一组配置,替换application.yml文件中mysql相关的配置项。
编写配置
在resources/examples/springboot-init
目录下编写templateMaker6.json
配置文件:
修改文件是src/main/resources/application.yml;
定义模型组mysqlConfig
,修改application.yml中MySQL的配置项。
{"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"}]}
}
测试执行
编写单元测试方法
@Test
public void testMakeSpringBootProjectTemplate(){// 读取资源文件 templateMaker.jsonString rootPath = "examples/springboot-init/";String configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker.json");TemplateMakerConfig templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);Long id = TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker1.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker2.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker3.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker4.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker5.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker6.json");templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);TemplateMaker.makeTemplate(templateMakerConfig);System.out.println("id = " + id);
}
测试执行,mysql模型组配置正确生成:
7、需求:控制是否开启 Redis
明确需求
允许用户传入 needRedis
模型参数,控制是否开启和 Redis 相关的代码。需要修改 application.yml、pom.xml、MainApplication.java 等多个用到 Redis 的文件的部分代码。
实现
因为这个需求比较定制化,每个和redis相关的代码文件都不一样,制作工具很难做到统一处理,因此需要我们手动人工去修改动态模板文件,并进行“挖坑”。这样做相对简单。一定要用程序实现的话,实现成本过高,得不偿失。
依次在模板文件中进行修改和“挖坑”:
1)application.yml.ftl 文件:
<#if needRedis># Redis 配置redis:database: 1host: localhostport: 6379timeout: 5000password: 123456
</#if>
2)MainApplication.java.ftl 文件:
@SpringBootApplication<#if !needRedis>(exclude = {RedisAutoConfiguration.class})</#if>
3)pom.xml.ftl 文件:
<#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>
编写配置
编写好模板后,我们依然可以使用模板制作工具来生成元信息配置。
在 resources/examples/springboot-init
目录下新建 templateMaker7.json
模板配置文件,代码如下:
{"id": 1,"fileConfig": {"files": [{"path": "src/main/resources/application.yml"},{"path": "src/main/java/com/liucc/springbootinit/MainApplication.java"},{"path": "pom.xml"}]},"modelConfig": {"models": [{"fieldName": "needRedis","type": "boolean","description": "是否开启Redis功能","defaultValue": true}]}
}
测试执行
和之前的测试方法一样,在单元测试方法内补充读取新配置并制作模板的代码,如下:
configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker7.json");
templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);
TemplateMaker.makeTemplate(templateMakerConfig);
执行成功,查看生成的元信息配置文件,发现控制redis的模型参数正确生成:
8、需求:控制是否开启ElasticSearch
明确需求
允许用户传入needEs
模型参数,控制是否开启和ElasticSearch相关的代码。需要修改和 Elasticsearch 相关的代码,比如 PostController、PostService、PostServiceImpl、application.yml 等多个文件的部分代码。还要用 needEs
模型参数控制 PostEsDTO 整个文件是否生成。
实现
这个需求比控制 Redis 是否生成更复杂,也需要自己修改模板文件。
让我们依次在工作空间中找到以下模板文件并修改:uEOJiKndpA125Cg3RY5qLKd978bEan7Dh5lQLM1XL4M=
1)PostController.java.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>
2)PostServiceImpl.java.ftl 文件:
<#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 起始页为 0long 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>
3)PostService.java.ftl 文件:
<#if needEs>
/*** 从 ES 查询** @param postQueryRequest* @return*/
Page<Post> searchFromEs(PostQueryRequest postQueryRequest);
</#if>
4)application.yml.ftl 文件:
<#if needEs># Elasticsearch 配置elasticsearch:uris: http://localhost:9200username: rootpassword: 123456
</#if>
编写配置
在 resources/examples/springboot-init
目录下新建 templateMaker8.json
模板配置文件,代码如下:
{"id": 1,"fileConfig": {"files": [{"path": "src/main/java/com/liucc/springbootinit/model/dto/post/PostEsDTO.java","condition": "needPost && needEs"}]},"modelConfig": {"models": [{"fieldName": "needEs","type": "boolean","description": "是否开启ES功能","defaultValue": true}]}
}
测试执行
在单元测试方法内补充读取新配置并制作模板的代码,如下:
configStr = ResourceUtil.readUtf8Str(rootPath + "templateMaker8.json");
templateMakerConfig = JSONUtil.toBean(configStr, TemplateMakerConfig.class);
TemplateMaker.makeTemplate(templateMakerConfig);
执行成功,查看生成的元信息配置,发现控制 Elasticsearch 的模型参数正确生成:
但是,PostEsDTO
文件配置却没有 condition
条件:
这是因为PostEsDTO
已经属于post
文件组,根据输出配置
策略,会保留组内文件,舍弃组外配置。
所以我们手动人工调整一下,将其移出到组外,添加相关配置项:
8、测试最终成果
1)复制制作工具生成的meta.json到yuzi-generator-marker
项目resources
目录下,并替换名称为springboot-init-meta.json
2)修改MetaManager
类中读取元信息配置文件的路径
public static Meta initMeta(){String metaJson = ResourceUtil.readUtf8Str("springboot-init-meta.json");Meta newMeta = JSONUtil.toBean(metaJson, Meta.class);// 校验和处理默认值MetaValidator.doValidAndFill(newMeta);return newMeta;}
3)调用yuzi-generator-marker
的main方法,准备生成springboot代码生成器
成功生成
3)从springboot-init-generator
目录进入终端,使用./generator--help
命令查看帮助手册
4)测试使用config命令
5)测试使用list命令
可以看到代码生成器所用到的模板文件是非常多的,所以如果让我们人工作制作这么多的模板,人还不傻了。因此制作代码生成器的制作工具也是很有必要的,能够大大提高我们制作代码生成器的效率。
6)测试使用generate命令
-
先查看帮助手册
-
全局替换基础包名
./generator generate --basePackage=com.ikun
-
控制是否生成帖子相关文件(生成)
./generator generate --basePackage=com.ikun --needPost=true
结果遇到以下错误:
通过排查发现,是FreeMarker和mybatis的语法冲突,freemarker将
minUpdateTime
识别为自己的变量,结果读取不到这个变量的值,因此就产生报错。解决:使用freemarker的标签,告诉freemarker不要去解析这个变量。
<select id="listPostWithDelete" resultType="${basePackage}.springbootinit.model.entity.Post">select *from postwhere updateTime >= <#noparse>#{minUpdateTime}</#noparse> </select>
可以发现,帖子相关文件全部自动生成
-
控制是否生成帖子相关文件(不生成)
./generator generate --basePackage=com.ikun --needPost=false
最后帖子功能相关文件均没有生成。
-
其他功能这里就不重复测试了, 大同小异
至此,我们的SpringBoot项目代码生成器制作工具就完美成功啦!
最后
本章节修复了模板制作工具的一些bug,并一步步完成了制作SpringBoot项目代码生成器的全部需求,最终通过制作工具+微量人工手动调整的方式完成了复杂的项目生成器。
本章节涉及很多小技巧:lambda API + Stream API的使用,简化对集合的操作,变量的复用,方法的抽象以减少圈复杂度。
至此,代码生成器项目的第二阶段就顺利结束了,后续将进入第三阶段 — 制作在线的代码生成器平台,将我们本地开发好的代码生成器和制作工具“上云”,提高项目的价值。