当前位置: 首页 > news >正文

第八章 模板项目生成

第八章 模板项目生成

创建时间: 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

本节重点

本章节仍属于项目第二阶段 — 开发代码生成器制作工具。

本章节主要是完善制作工具,属于成果检验阶段。

本节重要内容:

  1. 模板制作工具 - Bug 修复
  2. 模板制作工具 - 易用性优化
  3. 制作 Spring Boot 项目模板生成器
  4. 测试成果
  5. 扩展思路

一、Bug修复

1、相同配置下,制作工具多次执行,配置信息generateType 会被强制变为static类型。

问题:执行TemplateMaker的main方法,第一次执行时,generateTypedynamic类型,再执行一次会变为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 ,我们程序完全可以获取得到。

优化:判断如果是非首次制作,sourceRootPathtempFilePath下第一个目录,也就是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的使用,简化对集合的操作,变量的复用,方法的抽象以减少圈复杂度。

至此,代码生成器项目的第二阶段就顺利结束了,后续将进入第三阶段 — 制作在线的代码生成器平台,将我们本地开发好的代码生成器和制作工具“上云”,提高项目的价值。

http://www.dtcms.com/a/195527.html

相关文章:

  • Matrix-Game:键鼠实时控制、实时生成的游戏生成模型(论文代码详细解读)
  • SPL做量化---MTM(动量指数)
  • Linux!启动~
  • 大模型对比
  • Grafana分布统计:Heatmap面板
  • 计算机视觉----感兴趣区域(ROI)、非极大值抑制
  • 探究电阻分压的带负载能力
  • DeepEval:快速搭建 LLM 评估框架实战「喂饭教程」
  • 机器学习数据预处理回归预测中标准化和归一化
  • C语言单链表应用详解
  • 【程序员AI入门:模型】19.开源模型工程化全攻略:从选型部署到高效集成,LangChain与One-API双剑合璧
  • 【目标检测】RT-DETR
  • PT2031K单触控单输出触摸IC
  • 【数据结构】线性表--栈
  • 网络攻防模拟:城市安全 “数字预演”
  • 记录一个为打印高清而做投喂图像增强的例子
  • leetcode2749. 得到整数零需要执行的最少操作数-medium
  • 第二十六天打卡
  • python版本管理工具-pyenv轻松切换多个Python版本
  • 求职困境:开发、AI、运维、自动化
  • Java Set系列集合详解:HashSet、LinkedHashSet、TreeSet底层原理与使用场景
  • YOLO+UI(C#)开发
  • 基于MATLAB-GUI图形界面的数字图像处理
  • 服务培训QDA 的安装调试方法,硬件模块的讲解和软件控制台使用及系统测试
  • R1 快开门式压力容器操作证备考练习题及答案
  • java输入输出类
  • 如何排查阻塞语句
  • 【Linux】iptables 命令详解
  • dify知识库支持图文回复实践
  • STM32H743IIT6_ADC采集误差分析与ADC_DMA