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

第六章 配置能力增强

第六章 配置能力增强

创建时间: 2025年4月16日 14:44
状态: Done

前言

本笔记主要用途是学习up 主程序员鱼皮的项目:代码生成器时的一些学习心得**。

代码地址:https://github.com/Liucc-123/yuzi-generator.git
项目教程:https://www.codefather.cn/course/1790980795074654209
上一章节内容:https://editor.csdn.net/md/?articleId=147324749

前情回顾


在前面的章节中,我们开发并优化了基本的代码生成器制作工具。但目前的制作工具能力有限,只能生成基本的 ACM示例代码生成器,对于复杂的、真实的项目生成器,还无法满足制作需求。

所以本章节的目标是增强制作工具的配置能力,能够让元信息支持更加灵活的配置,从而制作更加复杂的代码生成器。

本节重点


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

重点内容有 :

  1. 生成目标 — SpringBoot 模板项目介绍
  2. 工具通用能力分析
  3. 配置能力增强开发

一、需求分析


我们第二阶段的目标是通过制作工具得到一个 Spring Boot 项目模板代码生成器,能够让开发者快速定制生成自己的 Spring Boot 项目。

SpringBoot项目模板介绍

将提前准备好的SpringBoot项目模板解压,放到 yuzi-generator-demo-projects 目录下。

SpringBoot项目模板:
在GitHub项目根目录下进行下载
在这里插入图片描述

模板能力

在该项目的 README.md 文件中,可以看到关于该项目的介绍,比如运用的技术、业务特性、业务功能等。

模板拥有的能力如下:

1)实现了用户登录、注册、注销、更新、检索、权限管理bDwqeD5mloOWGl/U9KYpZxoMy96VHRFAqbGgO+8bBwk=

2)帖子创建、删除、编辑、更新、数据库检索、ES 灵活检索

3)使用了 MySQL、Redis、Elasticsearch 数据存储

4)使用 Swagger + Knife4j 实现接口文档生成Pga3o8dlAlk5jom/XG4u38VZgjWTs6SzXy1vtLtsRaA=

5)支持全局跨域处理

项目结构

可以使用tree命令打印出模板项目的树形结构:

.
├── Dockerfile
├── README.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src├── main│   ├── java│   │   └── com│   │       └── yupi│   │           └── springbootinit│   │               ├── MainApplication.java│   │               ├── common│   │               │   ├── BaseResponse.java│   │               │   ├── DeleteRequest.java│   │               │   ├── ErrorCode.java│   │               │   ├── PageRequest.java│   │               │   └── ResultUtils.java│   │               ├── config│   │               │   ├── CorsConfig.java│   │               │   ├── JsonConfig.java│   │               │   ├── Knife4jConfig.java│   │               │   └── MyBatisPlusConfig.java│   │               ├── constant│   │               │   └── UserConstant.java│   │               ├── controller│   │               │   ├── PostController.java│   │               │   └── UserController.java│   │               ├── exception│   │               │   ├── BusinessException.java│   │               │   ├── GlobalExceptionHandler.java│   │               │   └── ThrowUtils.java│   │               ├── mapper│   │               │   ├── PostMapper.java│   │               │   └── UserMapper.java│   │               ├── model│   │               │   ├── dto│   │               │   │   ├── post│   │               │   │   │   ├── PostAddRequest.java│   │               │   │   │   ├── PostEsDTO.java│   │               │   │   │   ├── PostQueryRequest.java│   │               │   │   │   └── PostUpdateRequest.java│   │               │   │   └── user│   │               │   │       ├── UserAddRequest.java│   │               │   │       ├── UserLoginRequest.java│   │               │   │       ├── UserQueryRequest.java│   │               │   │       ├── UserRegisterRequest.java│   │               │   │       └── UserUpdateRequest.java│   │               │   ├── entity│   │               │   │   ├── Post.java│   │               │   │   └── User.java│   │               │   ├── enums│   │               │   │   └── UserRoleEnum.java│   │               │   └── vo│   │               │       └── LoginUserVO.java│   │               └── service│   │                   ├── PostService.java│   │                   ├── UserService.java│   │                   └── impl│   │                       ├── PostServiceImpl.java│   │                       └── UserServiceImpl.java│   └── resources│       ├── application.yml│       └── mapper│           ├── PostMapper.xml│           └── UserMapper.xml└── test└── java

生成器应具备的功能

基于模板具有的基本能力,我们可以分析用户可能会有哪些定制化的代码生成需求,并明确代码生成器应该具备的功能。

比如:

1)替换生成的代码包名

2)控制是否生成帖子相关功能

3)控制是否需要开启跨域

4)自定义 Knife4jConfig 接口文档配置

5)自定义 MySQL 配置信息

6)控制是否开启 Redis

7)控制是否开启 Elasticsearch

二、实现思路

通过分析上述功能,我们可以从特定的需求中挖掘出代码生成器的通用能力。

逐个分析

1)需求:替换生成的代码包名

实现思路:还是挖坑,在所有出现包名的地方进行**挖坑,**然后指定类似于basePackage 的模型参数,让用户自己输入。

通用能力:这里用到包名的地方特别多,如果是我们手动“挖坑”来制作 FTL 模板文件,一个问题是成本高,再一个可能会出现遗漏。

所以,我们要利用制作工具在自动“挖坑”并生成模板文件。

2)需求:控制是否生成帖子相关功能

实现思路:允许用户通过一个参数来控制帖子相关代码是否生成,帖子相关代码包括有:PostController、PostService、Mapper、Model、Mapper.xml等等。

通用能力:用一个参数同时控制多个文件是否生成,而不仅仅是某段代码是否生成。

3)需求:控制是否需要开启跨域

实现思路:允许用户通过一个参数控制跨域相关文件是否生成,比如CorsConfig.java文件是否生成

通用能力:用一个参数控制某个文件是否生成,而不仅仅是某段代码是否生成。

4)需求:自定义 Knife4jConfig 接口文档配置
实现思路:修改knife4jConfig文件的配置,比如 title、description、version、apis扫描包路径等。

通用能力:由于knif4j 需要配置的参数特别多,如果需要让用户全部输入,用户的使用成本会比较大,可能就不会再用你的产品。因此,可以用一个参数控制是否开启接口文档配置。如果开启,再让用户输入一组配置参数

5)需求:自定义 MySQL 配置信息

实现思路:修改 application.yml文件中MySQL 的url、username、password 相关参数。

通用能力:由于要配置的参数比较多,可以定义一组隔离的配置参数。

6)需求:控制是否开启 Redis

实现思路:修改和开启Redis相关的代码,比如application.yml、pom.xml、MainApplication.java等多个文件中的部分代码。

通用能力:用一个参数控制多个文件的代码修改(之前已经实现过)

7)需求:控制是否开启 Elasticsearch

实现思路:

  • 修改ElasticSearch相关的代码,比如 PostController、PostService、application.yml等多个文件中的部分代码。
  • 用参数控制整个PostEsDTO整个文件是否生成

通用能力:用一个参数控制同时控制多个文件的代码以及某个文件是否要生成。

实现流程

通过对生成器的功能逐个分析,我们发现,有些需求的实现思路是囊括前面的需求的,也就是说,这些需求的实现上是有前后顺序的,我们可以根据这种“顺序”一步步进行实现。

现在我们的制作工具已经有的能力是:根据某个模型参数同时控制多处代码的修改。

而根据排序,制作工具需要增强的能力有:

  1. 一个模型参数控制某个文件是否生成
  2. 一个模型参数控制多个文件是否生成
  3. 一个模型参数同时控制多处代码修改以及多个文件是否生成
  4. 定义一组相关的模型参数,控制代码修改或文件生成
  5. 定义一组相关的模型参数,并通过其他模型参数控制是否需要输入这次参数(knif4jConfig接口配置文档)

可以发现,这些能力都和制作工具的元信息配置文件相关,即我们需要增强他的能力,允许开发者通过修改元信息,得到能让用户更灵活生成代码的代码生成器。

如果实现了上述增强能力,我们就能依次实现下列需求:

  1. 控制是否开启跨域
  2. 控制是否生成帖子相关功能
  3. 控制 Redis 是否开启
  4. 控制 ElasticSearch 是否开启
  5. 自定义 MySQL 配置信息
  6. 自定义 knife4jConfig接口文档配置

到时候,想制作一个 SpringBoot 项目模板代码生成器就很简单了。

至于“替换生成的代码包名”需求,在实现方式上和上述功能有较大的差别,回放到后续章节中进行实现。

三、开发实现


下面就依次实现上面提到的通用能力。

  1. 一个模型参数控制某个文件是否生成
  2. 一个模型参数控制多个文件是否生成
  3. 一个模型参数同时控制多处代码修改以及多个文件是否生成
  4. 定义一组相关的模型参数,控制代码修改或文件生成
  5. 定义一组相关的模型参数,并通过其他模型参数控制是否需要输入这次参数(knif4jConfig接口配置文档)

1、参数控制文件生成

还是以 ACM 模板项目为例,我们的需求是:用一个模型参数 needGit 来控制是否要生成.gitignore文件。

元信息修改

修改元信息配置文件 meta.json,在 modelConfug.models下新增 needGit 模型参数:

{..."modelConfig": {"models": [{"fieldName": "needGit","type": "boolean","description": "是否生成 .gitignore 文件","defaultValue": true},...]}
}

代码生成器实现

最大的问题是:怎么通过 needGit 参数控制文件的生成呢?

首先,.gitignore文件是在哪儿生成的?

是代码生成器acm-template-pro-generator项目下的 MainGenerator生成的,如下所示:
在这里插入图片描述

因此可以想到的是,我们可以通过添加 if 块来条件控制 .gitignore文件的生成呀!

可以手动调整下代码生成器来验证此方法是否可行?

1)修改 MainGenerator 中生成.gitignore相关代码

public class MainGenerator {public static void doGenerate(DataModel model) throws TemplateException, IOException {...boolean needGit = model.isNeedGit();if(needGit) {inputPath = new File(inputRootPath, ".gitignore").getAbsolutePath();outputPath = new File(outputRootPath, ".gitignore").getAbsolutePath();StaticGenerator.copyFilesByHutool(inputPath, outputPath);}...}
}

这里,我们有两处改动:

1、方法参数 modelObject 类型改为 DataModel 类型,因为在后续程序中,需要获取 model的needGit属性。

2、添加 if 条件代码块,来判断是否生成.gitignore文件

修改主类 Main 方法,命令行参数指定—needGit=false 并运行,发现 generated 目录下没有生成 git 文件,符合预期,证明此方法可行!
在这里插入图片描述

制作工具实现

接下来我们的目标就是,增强制作工具的能力,让它能够生成上述代码(参数控制某个文件的生成)。

1)修改 MainGenerator.java.ftl模板文件,将方法参数类型改为 DataModel,修改代码如下:

public static void doGenerate(DataModel model) throws TemplateException, IOException {}

2)如何将模型参数和文件生成进行关联呢?

在上面 MainGenerator的代码中,我们先获取模型的 needGit 字段的值,然后将其作为 if 的判断条件,是否要生成git 忽略文件。

一种实现方式是,在 meta 的 fileConfig.files中添加一个条件字段 condition,值为 model 的字段,这样就可以生成同样的代码:

boolean needGit = model.isNeedGit();
if(needGit) {inputPath = new File(inputRootPath, ".gitignore").getAbsolutePath();outputPath = new File(outputRootPath, ".gitignore").getAbsolutePath();StaticGenerator.copyFilesByHutool(inputPath, outputPath);
}

修改元信息配置文件,添加 condition 字段:

{..."fileConfig": {"files": [{"inputPath": ".gitignore","outputPath": ".gitignore","type": "file","generateType": "static","condition": "needGit"},]},...
}

同步修改 Meta 类,给 FileInfo 添加 condition 字段:

@NoArgsConstructor
@Data
public static class FileInfo {private String inputPath;private String outputPath;private String type;private String generateType;private String condition;
}

3)目前还存在一个问题:if 块想要直接使用模型参数名作为变量,就需要先获取参数值。但获取对象属性的 方法有的是 getxxx、有的是 isxxx,结构不一致,导致MainGenerator.java.ftl模板文件在获取模型参数值时比较复杂。

boolean needGit = model.isNeedGit();
boolean loop = model.isLoop();
String author = model.getAuthor();
String outputText = model.getOutputText();

解决方案:一种解决方案是最简单的,就是修改DataModel 字段的访问修饰符为 public,这样就不需要调用方法获取字段值了。

补充:

还有一种方式是:使用 Lombok 插件的@Accessors 注解自定义 get/set方法代码的生成规则。将 fluent 设置为 true 表示所有的 get/set方法名称都等同于属性名称,从而保证统一。

参考官方文档:https://projectlombok.org/features/experimental/Accessors

这种方式的缺点是会影响 FreeMarker 模板获取对象的值(FreeMarker 默认是从 getXxx 获取属性值的),因此不推荐。

然后我们就可以直接通过属性名获取属性值:

boolean needGit = model.needGit;
if (needGit) {...
}

4)修改 DataModel.java.ftl 模板文件,调整字段访问修饰符为 public

@Data
public class DataModel {<#list modelConfig.models as modelInfo><#if modelInfo.description??>/*** ${modelInfo.description}*/</#if>public ${modelInfo.type} ${modelInfo.fieldName} <#if modelInfo.defaultValue??>= ${modelInfo.defaultValue?c}</#if>;</#list>
}

5)最后,修改 MainGenerator.ftl模板文件,补充 DataModel 参数的获取,以及条件判断生成.gitignore文件的逻辑。

完整代码如下:

package ${basePackage}.generator;import freemarker.template.TemplateException;import java.io.File;
import java.io.IOException;
import ${basePackage}.model.DataModel;/*** 核心模板生成器(静态+动态)*/
public class MainGenerator {public static void doGenerate(DataModel model) throws TemplateException, IOException {String inputRootPath = "${fileConfig.inputRootPath}";String outputRootPath = "${fileConfig.outputRootPath}";String inputPath;String outputPath;
<#list  modelConfig.models as modelInfo>${modelInfo.type} ${modelInfo.fieldName} = model.${modelInfo.fieldName};
</#list>
<#list fileConfig.files as fileInfo><#if fileInfo.condition??>if (${fileInfo.condition}) {inputPath = new File(inputRootPath, "${fileInfo.inputPath}").getAbsolutePath();outputPath = new File(outputRootPath, "${fileInfo.outputPath}").getAbsolutePath();<#if fileInfo.generateType == "dynamic">DynamicGenerator.doGenerate(inputPath, outputPath, model);<#else>StaticGenerator.copyFilesByHutool(inputPath, outputPath);</#if>}<#else>inputPath = new File(inputRootPath, "${fileInfo.inputPath}").getAbsolutePath();outputPath = new File(outputRootPath, "${fileInfo.outputPath}").getAbsolutePath();<#if fileInfo.generateType == "dynamic">DynamicGenerator.doGenerate(inputPath, outputPath, model);<#else>StaticGenerator.copyFilesByHutool(inputPath, outputPath);</#if></#if>
</#list>}
}

6)测试执行,代码生成器是否支持参数控制文件生成。

运行 Main 主类生成代码生成器,打开生成的代码生成器,运行其 Main 主类,检查.gitignore是否可以控制生成。

在这里插入图片描述

2、同参数控制多个文件生成

想要用同一个参数控制多个文件是否生成,最简单的方式就是直接给多个文件配置相同参数的 condition 就可以了,如下:

"files": [{"inputPath": "src/com/yupi/acm/MainTemplate.java.ftl","outputPath": "src/com/yupi/acm/MainTemplate.java","type": "file","generateType": "dynamic","condition": "needGit"},{"inputPath": ".gitignore","outputPath": ".gitignore","type": "file","generateType": "static","condition": "needGit"}
]

这种方式的优点是简单,缺点是不便于维护,如果后面文件数量多了,都是用这个 condition 字段,再要调整字段名称,需要全部做更改。

为了解决这个问题,我们可以根据生成条件,对文件进行分组

这里有两种方案:

方案一

给元信息的 fileInfo 增加 group 字段,指定每个文件分属于哪个组。

"files": [{"inputPath": "src/com/yupi/acm/MainTemplate.java.ftl","outputPath": "src/com/yupi/acm/MainTemplate.java","type": "file","generateType": "dynamic","group": "post"}
]

然后在 FileConfig增加 groupConfig 组配置,包括组名、生成条件等。示例代码:

"fileConfig": {..."groupConfig": {"groups": [{"name": "post","condition": "needCors"}]}
}

这种设计类似于库表设计,将组和文件分开定义,再通过文件指定所属组进行关联。

优点是结构清晰,可以通过读取 groupConfig 直接获取所有组的信息;

缺点是不能通过配置文件直接获取同组下的所有文件,还可能会导致生成的代码不够优雅,出现很多重复的 if 块,比如:

boolean needGit = model.needGit;if (needGit) {inputPath = new File(inputRootPath, ".gitignore").getAbsolutePath();outputPath = new File(outputRootPath, ".gitignore").getAbsolutePath();StaticGenerator.copyFilesByHutool(inputPath, outputPath);
}if (needGit) {inputPath = new File(inputRootPath, "README.md").getAbsolutePath();outputPath = new File(outputRootPath, "README.md").getAbsolutePath();StaticGenerator.copyFilesByHutool(inputPath, outputPath);
}

方式二(推荐)

将文件组当做是一个特殊的文件夹,然后将同组文件都放到该组配置下。

元信息部分代码如下:

"files": [{"inputPath": "src/com/liucc/acm/MainTemplate.java.ftl","outputPath": "src/com/liucc/acm/MainTemplate.java","type": "file","generateType": "dynamic"},{"groupKey": "git","groupName": "开源","type": "group","condition": "needGit","files": [{"inputPath": ".gitignore","outputPath": ".gitignore","type": "file","generateType": "static"},{"inputPath": "README.md","outputPath": "README.md","type": "file","generateType": "static"}]}]

在 fileInfo 中新增字段:

  • groupKey:组的唯一标识
  • groupName:组的名称
  • type:值为 group 表示这段配置是文件组
  • condition:开关,控制该组下所有文件的生成

这种方式的优点是可以直接从 fileConfig 配置中看出文件的层级关系,且可以方便获取同组文件信息、控制同组文件的生成。

缺点是:生成代码的时候,如果是文件组类别,需要多遍历一层 files。但这点儿代码开发成本是在可接受范围内。因此最终推荐方式二实现同参数控制多文件生成。

最终的实现类似于:

boolean needGit = model.needGit;// groupKey = git
if (needGit) {inputPath = new File(inputRootPath, ".gitignore").getAbsolutePath();outputPath = new File(outputRootPath, ".gitignore").getAbsolutePath();StaticGenerator.copyFilesByHutool(inputPath, outputPath);inputPath = new File(inputRootPath, "README.md").getAbsolutePath();outputPath = new File(outputRootPath, "README.md").getAbsolutePath();StaticGenerator.copyFilesByHutool(inputPath, outputPath);
}

扩展:采用这种方案后,可以将同一个文件放在不同组下,这样当任一组的生成条件满足时,都可以生成改文件。

开发实现

1)修改 Meta 类的属性,给 FileInfo 添加文件组相关属性:

@NoArgsConstructor
@Data
public static class FileInfo {private String inputPath;private String outputPath;private String type;private String generateType;private String condition;private String groupKey;private String groupName;private List<FileInfo> files;
}

2)修改FileTypeEnum枚举类,增加文件组枚举值

GROUP("文件组", "group")

3)修改MetaValidator校验器

原先对于 fileInfo 的 inputPath 校验规则是必填项,但现在fileInfo 的type 如果是 group,是可以没有 inputPath 的,因此需要调整校验器的逻辑。为了简单起见,如果type 是 group,则跳过本次校验。

部分代码如下:

for (Meta.FileConfigDTO.FileInfo fileInfo : fileInfoList) {// 如果类型是 group,就跳过校验if (FileTypeEnum.GROUP.getType().equals(fileInfo.getType())) {continue;}...
}

4)修改MainGenerator.java.ftl模板文件

多了文件组的概念后,生成文件的逻辑需要做如下调整:

  1. 判断当前 fileInfo 的condition是否为空?为空,则说明该文件没有被参数控制生成。不为空,说明是被参数控制生成的,需要进一步判断。
  2. 进一步判断groupKey 是否有值?有值,则表示是一个文件组,遍历 files 生成多个文件;没有值,表示是一个文件,只需生成一个文件。

完整代码如下:

package ${basePackage}.generator;import freemarker.template.TemplateException;import java.io.File;
import java.io.IOException;
import ${basePackage}.model.DataModel;/*** 核心模板生成器(静态+动态)*/
public class MainGenerator {public static void doGenerate(DataModel model) throws TemplateException, IOException {String inputRootPath = "${fileConfig.inputRootPath}";String outputRootPath = "${fileConfig.outputRootPath}";String inputPath;String outputPath;
<#list  modelConfig.models as modelInfo>${modelInfo.type} ${modelInfo.fieldName} = model.${modelInfo.fieldName};
</#list>
<#list fileConfig.files as fileInfo><#if fileInfo.condition??>if (${fileInfo.condition}) {<#if fileInfo.groupKey??>// 说明是一组文件 groupKey = ${fileInfo.groupKey}<#list fileInfo.files as fileInfo>inputPath = new File(inputRootPath, "${fileInfo.inputPath}").getAbsolutePath();outputPath = new File(outputRootPath, "${fileInfo.outputPath}").getAbsolutePath();<#if fileInfo.generateType == "dynamic">DynamicGenerator.doGenerate(inputPath, outputPath, model);<#else>StaticGenerator.copyFilesByHutool(inputPath, outputPath);</#if></#list><#else >// 说明是一个文件inputPath = new File(inputRootPath, "${fileInfo.inputPath}").getAbsolutePath();outputPath = new File(outputRootPath, "${fileInfo.outputPath}").getAbsolutePath();<#if fileInfo.generateType == "dynamic">DynamicGenerator.doGenerate(inputPath, outputPath, model);<#else>StaticGenerator.copyFilesByHutool(inputPath, outputPath);</#if></#if>}<#else>// 没有被参数控制生成inputPath = new File(inputRootPath, "${fileInfo.inputPath}").getAbsolutePath();outputPath = new File(outputRootPath, "${fileInfo.outputPath}").getAbsolutePath();<#if fileInfo.generateType == "dynamic">DynamicGenerator.doGenerate(inputPath, outputPath, model);<#else>StaticGenerator.copyFilesByHutool(inputPath, outputPath);</#if></#if>
</#list>}
}

目前仍存在的问题:可以看到模板文件中存在多很重复的代码,比如:

inputPath = new File(inputRootPath, "${fileInfo.inputPath}").getAbsolutePath();
outputPath = new File(outputRootPath, "${fileInfo.outputPath}").getAbsolutePath();
<#if fileInfo.generateType == "dynamic">
DynamicGenerator.doGenerate(inputPath, outputPath, model);
<#else>
StaticGenerator.copyFilesByHutool(inputPath, outputPath);
</#if>

FreeMarker 支持宏定义,将重复的代码定义为一个“组件”,支持传递不停的参数,然后可以被其他地方引用。

FreeMarker 宏定义官方文档:http://www.kerneler.com/freemarker2.3.23/ref_directive_macro.html

将文件生成逻辑定义为宏,宏代码段的名称(id)为generateFile,参数有缩进(indent)、fileInfo

最终版

package ${basePackage}.generator;import freemarker.template.TemplateException;import java.io.File;
import java.io.IOException;
import ${basePackage}.model.DataModel;<#macro generateFile indent fileInfo>
${indent}inputPath = new File(inputRootPath, "${fileInfo.inputPath}").getAbsolutePath();
${indent}outputPath = new File(outputRootPath, "${fileInfo.outputPath}").getAbsolutePath();
<#if fileInfo.generateType == "dynamic">
${indent}DynamicGenerator.doGenerate(inputPath, outputPath, model);
<#else>
${indent}StaticGenerator.copyFilesByHutool(inputPath, outputPath);
</#if>
</#macro>/*** 核心模板生成器(静态+动态)*/
public class MainGenerator {public static void doGenerate(DataModel model) throws TemplateException, IOException {String inputRootPath = "${fileConfig.inputRootPath}";String outputRootPath = "${fileConfig.outputRootPath}";String inputPath;String outputPath;
<#list  modelConfig.models as modelInfo>${modelInfo.type} ${modelInfo.fieldName} = model.${modelInfo.fieldName};
</#list>
<#list fileConfig.files as fileInfo><#if fileInfo.condition??>if (${fileInfo.condition}) {<#if fileInfo.groupKey??>// 说明是一组文件 groupKey = ${fileInfo.groupKey}<#list fileInfo.files as fileInfo><@generateFile indent="            " fileInfo=fileInfo/></#list><#else >// 说明是一个文件<@generateFile indent="            " fileInfo=fileInfo/></#if>}<#else><@generateFile indent="        " fileInfo=fileInfo/></#if>
</#list>}
}

调用 Main 方法测试执行,检查生成的 MainGenerator 是否实现同参数控制多个文件的生成:

在这里插入图片描述

3、同参数控制代码生成和文件生成

这个能力其实我们已经实现了!只要让同一个参数即出现在控制文件生成的 condition 中,又出现在FreeMarker 动态模板中作为生成代码的参数即可。

在这里插入图片描述
在这里插入图片描述

4、定义一组相关的参数

对于一个复杂的代码生成器,可能会有很多允许用户自定义的参数,比如 MySQL 的配置可能就有十几条。如果把这些参数全部按照顺序写到元信息里,那么用户就会被要求一次性填写大量的参数,增加了用户的理解和使用成本,而且配置参数之间也可能存在命名冲突,比如 MySQL 和 Redis 都会有 url 参数。

对于这种情况,最常用的就是对参数进行**分组,**和上述文件分组类似,我们也可以对数据模型进行分组,各分组下的参数相互隔离、互不影响。

TypeScript 和 FreeMarker 等很多框架都会采用类似命名空间(分组)的概念隔离变量。

元信息修改

在元信息meta.json新增字段groupKey、groupName、models 模型参数。

修改后的元信息代码如下:

"modelConfig": {"models": [{"fieldName": "needGit","type": "boolean","description": "是否生成 .gitignore 文件","defaultValue": false,"abbr": "g"},{"fieldName": "loop","type": "boolean","description": "是否生成循环","defaultValue": false,"abbr": "l"},{"groupKey": "mainTemplate","groupName": "核心模板","type": "MainTemplate","description": "用于生成核心模板文件","models": [{"fieldName": "author","type": "String","description": "作者注释","defaultValue": "liucc","abbr": "a"},{"fieldName": "outputText","type": "String","description": "输出信息","defaultValue": "sum = ","abbr": "o"}]}]}

字段解释如下:

  • groupKey:模型分组的唯一表示,由 groupKey 开启分组,必须是英文
  • groupName:分组名称
  • type:分组类型,一般就是组对应的 Java Class 类型,必须大写开头
  • models:组里面具体的模型参数信息

同步修改 Meta 实体类的属性,为ModelInfo 添加新字段:

@NoArgsConstructor
@Data
public static class ModelInfo {private String fieldName;private String type;private String description;private Object defaultValue;private String abbr;private String groupKey;private String groupName;private List<ModelInfo> models;
}

同步修改 Meta 校验器MetaValidator,如果模型的 groupKey 不为空,表示这是模型组配置,简单起见,直接跳过本次校验。修改后的部分代码如下:

for (Meta.ModelConfigDTO.ModelInfo modelInfo : modelInfoList) {// 如果类型是 group,则跳过本次校验String groupKey = modelInfo.getGroupKey();if (StrUtil.isNotEmpty(groupKey)) {continue;}
}

代码生成器实现

根据上述分组配置,先试着编写一个能够实现分组功能的代码生成器,从而进一步实现具备分组功能的制作工具。

在制作工具生成的代码生成器acm-template-pro-generator 项目中进行修改。

1)如何实现分组参数呢?

将一组参数抽取为对象参数,封装到一个类中去。比如,我们将原先 DataModel 中的authoroutputText字段抽取到 MainTemplate 类中,简单起见,MainTemplate 就定义到 DataModel 的静态内部类。

完整代码如下:

package com.liucc.model;import lombok.Data;/*** 动态模板配置*/
@Data
public class DataModel {/*** 是否生成 .gitignore 文件*/public boolean needGit = false;/*** 是否生成循环*/public boolean loop = false;/*** 核心模板*/public MainTemplate mainTemplate = new MainTemplate();/*** 用于生成核心模板文件*/@Datapublic static class MainTemplate {/*** 作者注释*/public String author = "liucc";/*** 输出信息*/public String outputText = "sum = ";// 重写 toString方法@Overridepublic String toString() {return "MainTemplate{" +"author='" + author + '\'' +", outputText='" + outputText + '\'' +'}';}}
}

2)那现在如何让用户输入的参数能自动填充到对象里去呢?

FreeMarker 框架提供了复杂参数传递的解决方案,提供了参数组特性

Picocli 参数组官方介绍:https://picocli.info/#_argument_groups

复制 GenerateCommandTestArgGroupCommand 类,用于测试参数组特性。

在TestArgGroupCommand命令类中,定义参数组属性,添加@ArgGroup 注解,用于标识分组:

@CommandLine.ArgGroup(exclusive = false, heading = "核心模板 %n")
static MainTemplateCommand mainTemplateCommand;

在类中创建静态内部类MainTemplateCommand,定义 authoroutputText 属性,并给选项注解的 name 属性添加参数组为 groupKey 前缀,比如 mainTemplate.a,从而实现同名参数组相互隔离。

代码如下:

@Data
static class MainTemplateCommand {@CommandLine.Option(names = {"-mainTemplateCommand.a", "--mainTemplateCommand.author"}, arity = "0..1", description = "作者注释", interactive = true, echo = true)private String author = "liucc";@CommandLine.Option(names = {"-mainTemplateCommand.o", "--mainTemplateCommand.outputText"}, arity = "0..1", description = "输出信息", interactive = true, echo = true)private String outputText = "sum=";
}

在命令类的run 方法中打印所有属性值,并在 main 方法中编写测试程序。

最终TestArgGroupCommand的完整代码如下:

package com.liucc.cli.command;import lombok.Data;
import picocli.CommandLine;@CommandLine.Command(name = "generate", description = "生成代码", mixinStandardHelpOptions = true)
@Data
public class TestArgGroupCommand implements Runnable {@CommandLine.Option(names = {"-g", "--needGit"}, arity = "0..1", description = "是否生成 .gitignore 文件", interactive = true, echo = true)private boolean needGit = true;@CommandLine.Option(names = {"-l", "--loop"}, arity = "0..1", description = "是否生成循环", interactive = true, echo = true)private boolean loop = false;@CommandLine.ArgGroup(exclusive = false, heading = "核心模板 %n")static MainTemplateCommand mainTemplateCommand;@Overridepublic void run() {System.out.println(needGit);System.out.println(loop);System.out.println(mainTemplateCommand);}@Datastatic class MainTemplateCommand {@CommandLine.Option(names = {"-mainTemplateCommand.a", "--mainTemplateCommand.author"}, arity = "0..1", description = "作者注释", interactive = true, echo = true)private String author = "liucc";@CommandLine.Option(names = {"-mainTemplateCommand.o", "--mainTemplateCommand.outputText"}, arity = "0..1", description = "输出信息", interactive = true, echo = true)private String outputText = "sum=";}public static void main(String[] args) {CommandLine commandLine = new CommandLine(TestArgGroupCommand.class);commandLine.execute("--help");// commandLine.execute("-l", "-mainTemplateCommand.a", "-mainTemplateCommand.o");}}

执行main 方法,输入—help 参数,发现 FreeMarker 的帮助手册已经支持自动分组:

在这里插入图片描述

测试输入各选项参数值,从输出结果可以看到用户输入的值成功传递到对象参数里了。

在这里插入图片描述

目前存在的问题:虽然已经实现了参数分组,但是还是需要用户一次性输入全部参数,仍存在使用成本。

期望:用户选择开启某个配置,程序再引导用户进一步输入相关配置信息。如果没有选择开启,那么这一组的参数用户就无需填写了。

5、定义可选开启的参数组

需求:可以根据用户输入的某个开关参数,来控制是否需要进一步输入对应的参数组。

也就是说,模型参数之间是存在前后依赖关系的,必须按照某个顺序依次引导用户进行输入。

可以先引导用户输入最外层未分组的参数,再根据用户的输入情况,判断是否要引导用户输入参数组的信息。

实现思路

如何实现呢?套娃

用一个 CommandLine 类引导用户输入外层参数信息,根据用户输入信息,触发另一个 CommandLine 引导用户完成参数组的填写。

步骤如下:

  1. 给参数组定义一个单独的Picocli Command 类(这里使用静态内部类),通过命令行接收参数组的值;
  2. 用根Command 类(TestGroupCommand)接收用户外层输入,在run 方法中打印用户填写参数的值,并根据开关参数的值判断是否触发参数组 Command,进而引导用户填写参数组配置信息。

代码生成器具体实现

有了实现思路后,先调整代码生成器,再考虑制作工具的优化。

1)ModelData 创建静态内部类,在内部类创建参数组对应的属性。

代码如下:

package com.liucc.model;import lombok.Data;/*** 动态模板配置*/
@Data
public class DataModel {/*** 是否生成 .gitignore 文件*/public boolean needGit = false;/*** 是否生成循环*/public boolean loop = false;/*** 核心模板*/public MainTemplate mainTemplate = new MainTemplate();/*** 用于生成核心模板文件*/@Datapublic static class MainTemplate {/*** 作者注释*/public String author = "liucc";/*** 输出信息*/public String outputText = "sum = ";// 重写 toString方法@Overridepublic String toString() {return "MainTemplate{" +"author='" + author + '\'' +", outputText='" + outputText + '\'' +'}';}}
}

2)编写 Picocli 命令类,在类中定义属性。

复制TestArgGroupCommandTestGroupCommand ,后续代码实现都在此类中完成。

定义对象参数,定义为静态的,便于后续静态内部类的方法能够获取到该属性。

static DataModel.MainTemplate mainTemplate = new DataModel.MainTemplate();

编写单独的命令类(静态内部类)MainTemplateCommand用于接收参数组,在run 方法中将从命令行中接收到的值传递到外层mainTemplate对象。

@CommandLine.Command(name = "mainTemplate", description = "用于生成核心模板文件", mixinStandardHelpOptions = true)
@Data
static class MainTemplateCommand implements Runnable{@CommandLine.Option(names = {"-a", "--author"}, arity = "0..1", description = "作者注释", interactive = true, echo = true)private String author = "liucc";@CommandLine.Option(names = {"-o", "--outputText"}, arity = "0..1", description = "输出信息", interactive = true, echo = true)private String outputText = "sum=";@Overridepublic void run() {mainTemplate.author = author;mainTemplate.outputText = outputText;}
}

在外层 Command 的run 方法中,打印参数的值,并根据开关参数值判断是否触发参数组 Command。示例代码:

@CommandLine.Command(name = "generate", description = "生成代码", mixinStandardHelpOptions = true)
@Data
public class TestGroupCommand implements Runnable {@CommandLine.Option(names = {"-g", "--needGit"}, arity = "0..1", description = "是否生成 .gitignore 文件", interactive = true, echo = true)private boolean needGit = true;@CommandLine.Option(names = {"-l", "--loop"}, arity = "0..1", description = "是否生成循环", interactive = true, echo = true)private boolean loop = false;static DataModel.MainTemplate mainTemplate = new DataModel.MainTemplate();@Overridepublic void run() {System.out.println(needGit);System.out.println(loop);// 如果用户开启 loop,再让用户进一步填写核心配置信息if (loop){System.out.println("请输入核心配置信息:");CommandLine commandLine = new CommandLine(MainTemplateCommand.class);commandLine.execute("-a", "-o");}System.out.println(mainTemplate);}...
}

最终的完整代码如下:

package com.liucc.cli.command;import com.liucc.model.DataModel;
import lombok.Data;
import picocli.CommandLine;@CommandLine.Command(name = "generate", description = "生成代码", mixinStandardHelpOptions = true)
@Data
public class TestGroupCommand implements Runnable {@CommandLine.Option(names = {"-g", "--needGit"}, arity = "0..1", description = "是否生成 .gitignore 文件", interactive = true, echo = true)private boolean needGit = true;@CommandLine.Option(names = {"-l", "--loop"}, arity = "0..1", description = "是否生成循环", interactive = true, echo = true)private boolean loop = false;static DataModel.MainTemplate mainTemplate = new DataModel.MainTemplate();@Overridepublic void run() {System.out.println(needGit);System.out.println(loop);// 如果用户开启 loop,再让用户进一步填写核心配置信息if (loop){System.out.println("请输入核心配置信息:");CommandLine commandLine = new CommandLine(MainTemplateCommand.class);commandLine.execute("-a", "-o");}System.out.println(mainTemplate);}@CommandLine.Command(name = "mainTemplate", description = "用于生成核心模板文件", mixinStandardHelpOptions = true)@Datastatic class MainTemplateCommand implements Runnable{@CommandLine.Option(names = {"-a", "--author"}, arity = "0..1", description = "作者注释", interactive = true, echo = true)private String author = "liucc";@CommandLine.Option(names = {"-o", "--outputText"}, arity = "0..1", description = "输出信息", interactive = true, echo = true)private String outputText = "sum=";@Overridepublic void run() {mainTemplate.author = author;mainTemplate.outputText = outputText;}}public static void main(String[] args) {CommandLine commandLine = new CommandLine(TestGroupCommand.class);
//        commandLine.execute("--help");commandLine.execute("-g", "-l");}}

制作工具实现

确定了要制作的代码生成器后,就可以增强我们的制作工具,通过配置自动生成代码生成器。

比较关键的一点是:如果根据一个模型参数,控制另一个模型组参数是否要输入?

modelInfo 添加 condition 字段,可以将其他模型参数的 filedName 作为 condition 表达式的值。

1)修改元信息 meta.json文件models 组配置,补充 condition 字段,用 loop 字段作为控制开关,示例代码如下:

{"groupKey": "mainTemplate","groupName": "核心模板","type": "MainTemplate","description": "用于生成核心模板文件","condition": "loop","models": [{"fieldName": "author","type": "String","description": "作者注释","defaultValue": "yupi","abbr": "a"},{"fieldName": "outputText","type": "String","description": "输出信息","defaultValue": "sum = ","abbr": "o"}]
}

同步修改 Meta 实体类,给 ModelInfo 补充 condition 字段:

@NoArgsConstructor
@Data
public static class ModelInfo {private String fieldName;private String type;private String description;private Object defaultValue;private String abbr;private String groupKey;private String groupName;private String condition;private List<ModelInfo> models;
}

2)修改 DataModel.java.ftl模板文件,目标是生成前面已经跑通流程的代码。

注意,重复的代码片段使用 FreeMarker 宏定义进行抽取,完整代码如下:

package ${basePackage}.model;import lombok.Data;<#macro generateModel indent modelInfo>
<#if modelInfo.description??>
${indent}/**
${indent} * ${modelInfo.description}
${indent} */
</#if>
${indent}public ${modelInfo.type} ${modelInfo.fieldName} <#if modelInfo.defaultValue??>= ${modelInfo.defaultValue?c}</#if>;
</#macro>/*** 动态模板配置*/
@Data
public class DataModel {<#list modelConfig.models as modelInfo><#--是模型组--><#if modelInfo.groupKey??>/*** ${modelInfo.groupName}*/public ${modelInfo.type} ${modelInfo.groupKey} = new ${modelInfo.type}();/*** 用于生成核心模板文件*/@Datapublic static class MainTemplate {<#list modelInfo.models as subModelInfo><@generateModel indent="        " modelInfo=subModelInfo/></#list>}<#--非模型组--><#else><@generateModel indent="    " modelInfo=modelInfo/></#if></#list>
}

3)修改MainGenerator.java.ftl模板文件

因为 DataModel 的属性层级已经调整,所以MainGenerator 中获取 DataModel 属性值的程序也需要做相应调整,需要多取一个层级:

String outputText = model.mainTemplate.outputText;
String author = model.mainTemplate.author;

模板文件 MainGenerator.java.ftl修改部分代码:

<#list  modelConfig.models as modelInfo><#--有分组--><#if modelInfo.groupKey??><#list modelInfo.models as subModelInfo>${subModelInfo.type} ${subModelInfo.fieldName} = model.${modelInfo.groupKey}.${subModelInfo.fieldName};</#list><#--无分组--><#else>${modelInfo.type} ${modelInfo.fieldName} = model.${modelInfo.fieldName};</#if>
</#list>

4)修改GeneratorCommand.java.ftl模板文件,目标是生成前面已跑通流程的代码。

这是最复杂的地方在于,如何根据模型组生成包含所有参数的 args 参数列表:

commandLine.execute("--author", "--outputText");

实现过程涉及到遍历(models)、取特定字段(fieldname)、拼接字符串(用逗号进行拼接)等逻辑,如果这些操作全部通过 FreeMarker 来实现就太复杂了。我们我们的思想是:简单逻辑使用 FreeMarker 实现,复杂逻辑通过 Java 程序实现。通过程序将变量处理好后,直接传递给 FreeMarker,FreeMarker 直接使用即可!

实现

给Meta 实体类的 ModelInfo新增中间字段allArgsStr用于处理模型组下所有命令参数的拼接,示例代码如下:

@NoArgsConstructor
@Data
public static class ModelInfo {private String fieldName;private String type;private String description;private Object defaultValue;private String abbr;private String groupKey;private String groupName;private List<ModelInfo> models;private String condition;// 中间参数// 该分组下所有参数拼接字符串private String allArgsStr;
}

在 MetaValidator中添加维护allArgsStr变量的逻辑,部分代码如下:

for (Meta.ModelConfigDTO.ModelInfo modelInfo : modelInfoList) {// 如果类型是 group,则跳过本次校验String groupKey = modelInfo.getGroupKey();if (StrUtil.isNotEmpty(groupKey)) {// 维护 allArgsStr 参数  "--author", "--outputText"String allArgsStr = modelInfo.getModels().stream().map(subModelInfo -> {return String.format("\"--%s\"", subModelInfo.getFieldName());}).collect(Collectors.joining(",")).toString();modelInfo.setAllArgsStr(allArgsStr);continue;}...}

然后我们在模板文件中就可以直接使用这个中间参数变量了。

参照上面编写的代码生成器命令类 TestGroupCommand,我们可以编写出 GenerateCommand.java.ftl模板文件了,完整代码如下:

package ${basePackage}.cli.command;import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import ${basePackage}.model.DataModel;
import ${basePackage}.generator.MainGenerator;
import lombok.Data;
import picocli.CommandLine;import java.util.concurrent.Callable;<#--宏定义-->
<#--生成选项-->
<#macro generateOption indent modelInfo>
${indent}@CommandLine.Option(names = {<#if modelInfo.abbr??>"-${modelInfo.abbr}"</#if>, <#if modelInfo.fieldName??>"--${modelInfo.fieldName}"</#if>}, arity = "0..1", <#if modelInfo.description??>description = "${modelInfo.description}"</#if>, interactive = true, echo = true)
${indent}private ${modelInfo.type} ${modelInfo.fieldName} <#if modelInfo.defaultValue??>= ${modelInfo.defaultValue?c}</#if>;
</#macro>@CommandLine.Command(name = "generate", description = "生成代码", mixinStandardHelpOptions = true)
@Data
public class GenerateCommand implements Callable<Integer> {
<#list modelConfig.models as modelInfo><#--有分组--><#if modelInfo.groupKey??>static DataModel.${modelInfo.type} ${modelInfo.groupKey} = new DataModel.${modelInfo.type}();@CommandLine.Command(name = "${modelInfo.groupKey}", description = "${modelInfo.description}", mixinStandardHelpOptions = true)@Datastatic class ${modelInfo.type}Command implements Runnable{<#list modelInfo.models as subModelInfo><@generateOption indent="        " modelInfo=subModelInfo/></#list>@Overridepublic void run() {<#list modelInfo.models as subModelInfo>${modelInfo.groupKey}.${subModelInfo.fieldName} = ${subModelInfo.fieldName};</#list>}}<#--无分组--><#else><@generateOption indent="    " modelInfo=modelInfo/></#if>
</#list>public Integer call() throws Exception {<#list modelConfig.models as modelInfo><#if modelInfo.condition??>// 如果用户开启 核心配置,再让用户进一步填写核心配置信息if (${modelInfo.condition}){System.out.println("请输入${modelInfo.groupName}信息:");CommandLine commandLine = new CommandLine(${modelInfo.type}Command.class);commandLine.execute(${modelInfo.allArgsStr});}</#if></#list>// 填充数据模型DataModel dataModel = new DataModel();BeanUtil.copyProperties(this, dataModel);<#list modelConfig.models as modelInfo><#if modelInfo.groupKey??>dataModel.${modelInfo.groupKey} = ${modelInfo.groupKey};</#if></#list>MainGenerator.doGenerate(dataModel);return 0;}
}

5)执行测试

最后测试效果,如果 loop 我们填写 true,工具会引导我们输入核心模板配置:

在这里插入图片描述

loop 填写 false,程序会直接结束,不会引导用户填写其他配置信息:
在这里插入图片描述

但目前也存在一个问题,查看代码生成器生成的目标代码时,发现对应的参数是没有值得,这是什么问题?

通过排查发现,这是因为DataModel的属性层级结构变了,但是MainTemplate.java.ftl取模型 model 字段的层级没有对应变化,导致无法正确获取模型参数值:

在这里插入图片描述

调整原始模板文件 ACM 项目中acm-template-pro中的模板文件,从 a u t h o r 改为 {author}改为 author改为{mainTemplate.author}。另一个参数类似。
在这里插入图片描述

最后重新测试执行。

通过这个例子我们也可以发现现在还存在的一个问题是:模板文件和数据模型之间是强绑定的,如果数据模型的属性层级结构变化了,模板文件也需要我们手动去调整的,否则会出现不一致问题,就很麻烦,我们将在下一章节对该问题进行优化处理。

最后


以上就是本章节的内容,通过生成 SpringBoot 项目代码生成器的目标,梳理出了制作工具应具备的增强配置能力,并且以此实现。

实现过程中涉及到多个 FreeMarker 模板文件的编写,极其需要细心和耐心。

在下一章节中,会继续增加制作工具的功能,使制作工具能够自己生成配置文件和 FTL 动态模板文件,自动和数据模型进行绑定,就不需要我们自己挖坑了,进一步提高我们制作代码生成器的效率。

掌握点


  • 掌握从个例需求梳理通用需求的方法
  • 掌握 FreeMarker 宏定义,掌握组件复用的思想
http://www.xdnf.cn/news/255115.html

相关文章:

  • C语言数据类型与内存布局
  • Linux系统中的用户分类、为什么Linux系统中有很多我没有创建的用户?
  • PyTorch_创建线性和随机张量
  • 数据中台笔记01
  • PaddleOCR移植到RK3568
  • 文章三《机器学习基础概念与框架实践》
  • 【STM32】定时器输入捕获
  • 怎么实现动态提示词,并提升准确率
  • [面试]SoC验证工程师面试常见问题(二)
  • ps将图标变清晰-cnblog
  • MATLAB绘制局部放大图
  • 【Bootstrap V4系列】 学习入门教程之 组件-警告框(Alert)
  • 【DecAlign】用于解耦多模态表征学习的分层跨模态对齐
  • Spring AI:简化人工智能功能应用程序开发
  • 对称加密算法(AES、ChaCha20和SM4)Python实现——密码学基础(Python出现No module named “Crypto” 解决方案)
  • mysql索引及数据库引擎
  • 计算方法实验三 解线性方程组的直接方法
  • C++模板知识
  • 数据库系统概论|第五章:数据库完整性—课程笔记1
  • PostgreSQL 查看表膨胀情况的方法
  • 【算法基础】冒泡排序算法 - JAVA
  • w317汽车维修预约服务系统设计与实现
  • 藏语英语中文机器翻译入门实践
  • 仿腾讯会议——主界面设计创建房间加入房间客户端实现
  • 大模型压缩技术详解(2025最新进展)
  • python入门
  • kubernetes中离线业务编排详解JobCronJob之Job控制器CronJob
  • 云计算-容器云-部署jumpserver 版本2
  • 4.0/Q2,Charls最新文章解读
  • Android和iOS测试的区别有哪些?