04-工具开发
- 工具实现思路
- 元信息定义
- 工具开发
1. 制作工具规划
1. 明确需求和业务
- 快速将一个项目制作为 可以动态定制部分内容 的代码生成器。并且以 SpringBoot 初始化项目模板为例,演示如何根据需要动态生成 Java 后端初始化项目
- 生成器制作工具、代码生成器和目标代码的关系
- 完整业务流程图
2. 实现思路
- 工具应该提供哪些能力?怎么提高代码生成器的制作效率?
- 如何动态生成命令行工具?如何动态打 jar 包?
- 如何动态生成模板文件?怎么从原始文件中抽取参数?有哪些类型的参数?
- 开发基础的工具
- 移除第一阶段 ACM 生成器的硬编码,能在已有项目模板的基础上,通过读取 人工配置 跑通生成器的核心制作流程(不用在代码中找路径、改路径)
- 配置文件增强
- 以实现 Spring Boot 生成器为目标,给配置文件增加更多参数,可以更灵活地制作更复杂的生成器
- 工具能力增强
- 给工具增加更多能力,可以帮助开发者自动生成 / 更新配置文件、FTL 动态模板文件等,进一步提高制作效率
2. 核心设计
纯人工开发一个生成器步骤:
- 基于一个要生成的项目,手动挖坑,制作 FTL 动态模板文件(最复杂)
- 编写数据模型文件
- 编写 Picocli 命令类
- 编写代码生成文件 Generator(文件路径还是硬编码“写死”的)
- 手动执行 Maven 命令打 jar 包
- 自己封装快捷执行脚本
1. 需求分析
假如已经有了一套现成的项目模板文件(包含 FTL、Data),剩下的步骤能否让工具来自动实现呢?
- 项目
acm-template
,MainTemplate.java.ftl
得到现成的项目模板文件 - 已经有了 FTL、Data,将这些信息保存为 配置文件 ,让工具 读取配置文件 来生成数据模型文件、Picocli 命令类、Generator、打 jar、封装脚本等。相当于上述(2 - 6)步骤工具来实现
2. 元信息定义
- 一般是用来描述项目的数据。eg:项目的名称、作者等
- 本质上:把在项目中硬编码的内容转为可以灵活替换的配置
1. 元信息的存储结构
- JSON 格式来存储元信息。理由:常用、通用、结构清晰、便于理解、对前端 JS 非常友好
- 将元信息文件定义为
meta.json
,放在工具项目的resources
目录下
2. 元信息的字段配置
根据元信息的作用对配置字段进行分类,便于后面按层级组织各种配置:
- 记录生成器的基本信息
- 项目名称、作者、版本号等
- 记录生成文件信息
- 输入文件路径、输出路径、文件类别(目录或文件)、生成类别(静态或动态)等
- 记录数据模型信息
- 参数的名称、描述、类型、默认值等
3. 示例配置信息
acm-template
的生成器的元信息 JSON 文件
- 能提前确认的字段就提前确认,之后尽量只新增字段、避免修改字段
- 建议在外层尽量用对象来组织字段,而不是数组。更有利于字段的扩展(数组修改会改其中的所有对象)
{
"name": "acm-template-generator",
"description": "ACM 示例模板生成器",
"basePackage": "com.listao",
"version": "1.0",
"author": "listao",
"createTime": "2023-11-22",
"fileConfig": {
"inputRootPath": "/Users/yupi/Code/acm-template",
"outputRootPath": "generated",
"type": "dir",
"files": [
{
"inputPath": "src/com/yupi/acm/MainTemplate.java.ftl",
"outputPath": "src/com/yupi/acm/MainTemplate.java",
"type": "file",
"generateType": "dynamic"
},
{
"inputPath": ".gitignore",
"outputPath": ".gitignore",
"type": "file",
"generateType": "static"
},
{
"inputPath": "README.md",
"outputPath": "README.md",
"type": "file",
"generateType": "static"
}
]
},
"modelConfig": {
"models": [
{
"fieldName": "loop",
"type": "boolean",
"description": "是否生成循环",
"defaultValue": false,
"abbr": "l"
},
{
"fieldName": "author",
"type": "String",
"description": "作者注释",
"defaultValue": "yupi",
"abbr": "a"
},
{
"fieldName": "outputText",
"type": "String",
"description": "输出信息",
"defaultValue": "sum = ",
"abbr": "o"
}
]
}
}
3. 工具开发
开发顺序遵循上面需求分析中提到的生成器制作步骤,分为:
- 项目初始化
- 读取元信息
- 生成数据模型文件
- 生成 Picocli 命令类
- 生成代码生成文件
- 程序构建 jar 包
- 程序封装脚本
- 测试验证
1. maker项目初始化
1. 代码和目录结构优化
DynamicGenerator.java
改为DynamicFileGenerator
package com.listao.maker.generator.file;
import cn.hutool.core.io.FileUtil;
import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
/**
* 动态文件生成
*/
public class DynamicFileGenerator {
/**
* 使用相对路径生成文件
*
* @param relativeInputPath 模板文件相对输入路径
* @param outputPath 输出路径
* @param model 数据模型
*/
public static void doGenerate(String relativeInputPath, String outputPath, Object model) throws IOException, TemplateException {
// new 出 Configuration 对象,参数为 FreeMarker 版本号
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
// 获取模板文件所属包和模板名称
int lastSplitIndex = relativeInputPath.lastIndexOf("/");
String basePackagePath = relativeInputPath.substring(0, lastSplitIndex);
String templateName = relativeInputPath.substring(lastSplitIndex + 1);
// 通过类加载器读取模板(jar 时必需)
// ClassTemplateLoader templateLoader = new ClassTemplateLoader(DynamicFileGenerator.class, basePackagePath);
// cfg.setTemplateLoader(templateLoader);
File templateDir = new File(relativeInputPath).getParentFile();
cfg.setDirectoryForTemplateLoading(templateDir);
// 设置模板文件使用的字符集
cfg.setDefaultEncoding("utf-8");
// 创建模板对象,加载指定模板
Template template = cfg.getTemplate(templateName);
// 文件不存在则创建文件和父目录
if (!FileUtil.exist(outputPath)) {
FileUtil.touch(outputPath);
}
// 生成
Writer out = new FileWriter(outputPath);
template.process(model, out);
// 生成文件后别忘了关闭哦
out.close();
}
/**
* 生成文件
*
* @param inputPath 模板文件输入路径
* @param outputPath 输出路径
* @param model 数据模型
*/
public static void doGenerateByPath(String inputPath, String outputPath, Object model) throws IOException, TemplateException {
// new 出 Configuration 对象,参数为 FreeMarker 版本号
Configuration configuration = new Configuration(Configuration.VERSION_2_3_32);
// 指定模板文件所在的路径
File templateDir = new File(inputPath).getParentFile();
configuration.setDirectoryForTemplateLoading(templateDir);
// 设置模板文件使用的字符集
configuration.setDefaultEncoding("utf-8");
// 创建模板对象,加载指定模板
String templateName = new File(inputPath).getName();
Template template = configuration.getTemplate(templateName);
// 文件不存在则创建文件和父目录
if (!FileUtil.exist(outputPath)) {
FileUtil.touch(outputPath);
}
// 生成
Writer out = new FileWriter(outputPath);
template.process(model, out);
// 生成文件后别忘了关闭哦
out.close();
}
}
StaticGenerator.java
改为StaticFileGenerator
。删除自定义递归方法
package com.listao.maker.generator.file;
import cn.hutool.core.io.FileUtil;
/**
* 静态文件生成
*/
public class StaticFileGenerator {
/**
* 拷贝文件(Hutool 实现,会将输入目录完整拷贝到输出目录下)
*/
public static void copyFilesByHutool(String inputPath, String outputPath) {
FileUtil.copy(inputPath, outputPath, false);
}
}
- 优化
MainGenerator.java
改为FileGenerator.java
,使语义更明确 - 将生成文件相关的类都从
maker.generator
包移动到maker.generator.file
包下,防止和后面的其他生成器混在一起 - 删除多余的无用代码
- 删除
.gitignore
文件,统一在项目根目录管理 git 文件 - 删除现有的 FTL 模板文件
- 删除单元测试文件
- 删除所有制作好的脚本文件等
- 删除
package com.listao.maker.generator.file;
import freemarker.template.TemplateException;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
/**
* 核心生成器
*/
public class FileGenerator {
/**
* 生成
*
* @param model 数据模型
*/
public static void doGenerate(Object model) throws TemplateException, IOException {
String projectPath = System.getProperty("user.dir");
// 整个项目的根路径
// File parentFile = new File(projectPath).getParentFile();
File projectFile = new File(projectPath);
// 输入路径
String inputPath = new File(projectFile, "src/main/resources/acm-template").getAbsolutePath();
String outputPath = projectPath + File.separator + ".tmp";
// 生成静态文件
StaticFileGenerator.copyFilesByHutool(inputPath, outputPath);
// 生成动态文件
String inputDynamicFilePath = projectPath + File.separator + "src/main/resources/acm-template/src/com/yupi/acm/MainTemplate.java.ftl";
String outputDynamicFilePath = outputPath + File.separator + "acm-template/src/com/yupi/acm/MainTemplate.java";
DynamicFileGenerator.doGenerate(inputDynamicFilePath, outputDynamicFilePath, model);
}
@Test
public void test() throws TemplateException, IOException {
HashMap<String, Object> map = new HashMap<>();
map.put("author", "ooxx");
map.put("loop", false);
map.put("outputText", "求和结果:");
doGenerate(map);
}
}
目录结构如下:
2. 生成文件目录结构化
- 需要生成一个完整的项目(
generator-basic
),把对应的 FTL 放到对应的包下,和原项目的文件位置一一对应 static
目录用于存放可以直接拷贝的静态文件
2. 读取元信息
1. 元信息定义
提供了一个较为完整的元信息配置文件。存放 resources
目录下
{
"name": "acm-template-generator",
"description": "ACM 示例模板生成器",
"basePackage": "com.listao",
"version": "1.0",
"author": "listao",
"createTime": "2023-11-22",
"fileConfig": {
// ...
},
"modelConfig": {
// ...
}
}
name
:代码生成器名称,项目的唯一标识description
:生成器的描述basePackage
:控制生成代码的基础包名version
:生成器的版本号,会影响 Maven 的 pom.xml 文件,从而影响 Jar 包的名称author
:作者名称createTime
:创建时间fileConfig
:是一个对象,用于控制文件的生成配置modelConfig
:是一个对象,用于控制数据模型(动态参数)信息
2. 元信息模型类
在 maker.meta
包下新建 Meta
类,用于接受 JSON 字段
JSON 文件转 Java 类代码的 IDEA 插件
- GsonFormatPlus(推荐)
- RoboPOJOGenerator
- Json2Pojo
- 打开
Meta.java
,cmd + n
选择GsonFormatPlus
- 将完整的
meta.json
复制到左侧窗口
- 点击左下角的
Setting
按钮,弹出高级配置,按照下图的规则配置
一定要人工校验,防止出现细节问题
- 将
Files
改名为FileInfo
。复数变单数,更好理解 - 将
Models
改名为ModelInfo
- 修改
ModelInfo.defaultValue
的类型为Object
,同时兼容多种不同的类型
3. 读取元信息 - 单例
String metaJson = ResourceUtil.readUtf8Str("meta.json");
Meta newMeta = JSONUtil.toBean(metaJson, Meta.class);
配置文件在运行时基本不会发生变更,项目运行期间只有一个 Meta 对象,避免重复创建对象开销 —— 单例模式
package com.listao.maker.meta;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.json.JSONUtil;
public class MetaManager {
private static volatile Meta meta;
private MetaManager() {
// 私有构造函数,防止外部实例化
}
public static Meta getMetaObject() {
if (meta == null) {
synchronized (MetaManager.class) {
if (meta == null) {
meta = initMeta();
}
}
}
return meta;
}
private static Meta initMeta() {
String metaJson = ResourceUtil.readUtf8Str("meta.json");
Meta newMeta = JSONUtil.toBean(metaJson, Meta.class);
// todo 校验和处理默认值
return newMeta;
}
}
饿汉式单例模式, 类加载时即初始化对象实例 ,从而保证在任何时候都只有一个实例
public class MetaManager {
private static final Meta meta = initMeta();
private MetaManager() {
// 私有构造函数,防止外部实例化
}
public static Meta getMetaObject() {
return meta;
}
private static Meta initMeta() {
String metaJson = ResourceUtil.readUtf8Str("meta.json");
Meta newMeta = JSONUtil.toBean(metaJson, Meta.class);
// todo 校验和处理默认值
return newMeta;
}
}
4. 调用测试
public class MetaTest {
public static void main(String[] args) {
Meta meta = MetaManager.getMetaObject();
System.out.println(meta);
}
}
3. 生成 DataModel
1. 元信息定义
fieldName
:参数名称,模型字段的唯一标识type
:参数类别,比如字符串、布尔等description
:参数的描述信息defaultValue
:参数的默认值abbr
:参数的缩写,用于生成命令行选项的缩写语法
{
// ...
"modelConfig": {
"models": [
{
"fieldName": "loop",
"type": "boolean",
"description": "是否生成循环",
"defaultValue": false,
"abbr": "l"
},
{
"fieldName": "author",
"type": "String",
"description": "作者注释",
"defaultValue": "yupi",
"abbr": "a"
},
{
"fieldName": "outputText",
"type": "String",
"description": "输出信息",
"defaultValue": "sum = ",
"abbr": "o"
}
]
}
}
2. 开发实现
工具作用:制作代码生成器,生成的代码是要对开发者可见的,而不是在 Java 程序运行时动态生成、结束后不留痕迹,所以采用 FreeMarker 实现
${modelInfo.defaultValue?c}
作用:将任何类型的变量(boolean 类型和 String 类型)都转换为字符串
package ${basePackage}.model;
import lombok.Data;
/**
* 数据模型
*/
@Data
public class DataModel {
<#list modelConfig.models as modelInfo>
<#if modelInfo.description??>
/**
* ${modelInfo.description}
*/
</#if>
private ${modelInfo.type} ${modelInfo.fieldName}<#if modelInfo.defaultValue??> = ${modelInfo.defaultValue?c}</#if>;
</#list>
}
3. 调用测试
- 定义生成器的根路径。当前项目下的
generated/生成器名称
目录- 注意:
.gitignore
文件中忽略generated
目录
- 注意:
- 定义要生成的 Java 代码根路径。需要将元信息中的
basePackage
转换为实际的文件路径 - 调用
DynamicFileGenerator
生成DataModel
文件
public class T_Main {
/**
* `@Test` 运行需要 test/resources/templates
*/
public static void main(String[] args) throws IOException, InterruptedException, TemplateException {
Meta meta = MetaManager.getMetaObject();
// 1. 输出根路径
String projectPath = System.getProperty("user.dir");
String outputPath = projectPath + "/.tmp/" + meta.getName();
if (!FileUtil.exist(outputPath)) {
FileUtil.mkdir(outputPath);
}
// 2. 读取 resources 目录
ClassPathResource classPathResource = new ClassPathResource("");
String inputResourcePath = classPathResource.getAbsolutePath();
// 3. Java 包基础路径
String outputBasePackage = meta.getBasePackage();
String outputBasePackagePath = StrUtil.join("/", StrUtil.split(outputBasePackage, "."));
String outputBaseJavaPackagePath = outputPath + "/src/main/java/" + outputBasePackagePath;
String inputFilePath;
String outputFilePath;
// 4. model/DataModel
inputFilePath = inputResourcePath + "templates/java/model/DataModel.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/model/DataModel.java";
DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);
}
}
4. 生成 Picocli
- 具体命令
GenerateCommand.java
、ListCommand.java
、ConfigCommand.java
- 命令执行器:
CommandExecutor.java
- 调用命令执行器的主类:
Main.java
1. GenerateCommand.java.ftl
package ${basePackage}.cli.command;
import cn.hutool.core.bean.BeanUtil;
import ${basePackage}.generator.MainGenerator;
import ${basePackage}.model.DataModel;
import lombok.Data;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import java.util.concurrent.Callable;
@Command(name = "generate", description = "生成代码", mixinStandardHelpOptions = true)
@Data
public class GenerateCommand implements Callable<Integer> {
<#list modelConfig.models as modelInfo>
@Option(names = {<#if modelInfo.abbr??>"-${modelInfo.abbr}", </#if>"--${modelInfo.fieldName}"}, arity = "0..1", <#if modelInfo.description??>description = "${modelInfo.description}", </#if>interactive = true, echo = true)
private ${modelInfo.type} ${modelInfo.fieldName}<#if modelInfo.defaultValue??> = ${modelInfo.defaultValue?c}</#if>;
</#list>
public Integer call() throws Exception {
DataModel dataModel = new DataModel();
BeanUtil.copyProperties(this, dataModel);
MainGenerator.doGenerate(dataModel);
return 0;
}
}
2. ListCommand.java.ftl
${fileConfig.inputRootPath}
动态生成。作用:指定模板文件的根路径
package ${basePackage}.cli.command;
import cn.hutool.core.io.FileUtil;
import picocli.CommandLine.Command;
import java.io.File;
import java.util.List;
@Command(name = "list", description = "查看文件列表", mixinStandardHelpOptions = true)
public class ListCommand implements Runnable {
public void run() {
// 输入路径
String inputPath = "${fileConfig.inputRootPath}";
List<File> files = FileUtil.loopFiles(inputPath);
for (File file : files) {
System.out.println(file);
}
}
}
3. ConfigCommand.java.ftl
package ${basePackage}.cli.command;
import cn.hutool.core.util.ReflectUtil;
import ${basePackage}.model.DataModel;
import picocli.CommandLine.Command;
import java.lang.reflect.Field;
@Command(name = "config", description = "查看参数信息", mixinStandardHelpOptions = true)
public class ConfigCommand implements Runnable {
public void run() {
// 实现 config 命令的逻辑
System.out.println("查看参数信息");
Field[] fields = ReflectUtil.getFields(DataModel.class);
// 遍历并打印每个字段的信息
for (Field field : fields) {
System.out.println("字段名称:" + field.getName());
System.out.println("字段类型:" + field.getType());
System.out.println("---");
}
}
}
4. CommandExecutor.java.ftl
package ${basePackage}.cli;
import ${basePackage}.cli.command.GenerateCommand;
import ${basePackage}.cli.command.ListCommand;
import ${basePackage}.cli.command.ConfigCommand;
import picocli.CommandLine;
import picocli.CommandLine.Command;
/**
* 命令执行器
*/
@Command(name = "${name}", mixinStandardHelpOptions = true)
public class CommandExecutor implements Runnable {
private final CommandLine commandLine;
{
commandLine = new CommandLine(this)
.addSubcommand(new GenerateCommand())
.addSubcommand(new ConfigCommand())
.addSubcommand(new ListCommand());
}
@Override
public void run() {
// 不输入子命令时,给出友好提示
System.out.println("请输入具体命令,或者输入 --help 查看命令提示");
}
/**
* 执行命令
*
* @param args
* @return
*/
public Integer doExecute(String[] args) {
return commandLine.execute(args);
}
}
5. Main
package ${basePackage};
import ${basePackage}.cli.CommandExecutor;
public class Main {
public static void main(String[] args) {
CommandExecutor commandExecutor = new CommandExecutor();
commandExecutor.doExecute(args);
}
}
6. 调用测试
// cli.command.ConfigCommand
inputFilePath = inputResourcePath + "templates/java/cli/command/ConfigCommand.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/cli/command/ConfigCommand.java";
DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);
// cli.command.GenerateCommand
inputFilePath = inputResourcePath + "templates/java/cli/command/GenerateCommand.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/cli/command/GenerateCommand.java";
DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);
// cli.command.ListCommand
inputFilePath = inputResourcePath + "templates/java/cli/command/ListCommand.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/cli/command/ListCommand.java";
DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);
// cli.CommandExecutor
inputFilePath = inputResourcePath + "templates/java/cli/CommandExecutor.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/cli/CommandExecutor.java";
DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);
// Main
inputFilePath = inputResourcePath + "templates/java/Main.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/Main.java";
DynamicFileGenerator.doGenerate(inputFilePath, outputFilePath, meta);
- 生成项目的目录结构如下:
5. 生成 pom.xml
使用 Maven 打包项目,项目的根目录下必须要有 pom.xml
项目管理文件。根据元信息动态生成的
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>${basePackage}</groupId>
<artifactId>${name}</artifactId>
<version>${version}</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- https://freemarker.apache.org/index.html -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
<!-- https://picocli.info -->
<dependency>
<groupId>info.picocli</groupId>
<artifactId>picocli</artifactId>
<version>4.7.5</version>
</dependency>
<!-- https://doc.hutool.cn/ -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.16</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<!-- https://projectlombok.org/ -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<archive>
<manifest>
<mainClass>${basePackage}.Main</mainClass> <!-- 替换为你的主类的完整类名 -->
</manifest>
</archive>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
// pom.xml
inputFilePath = inputResourcePath + File.separator + "templates/pom.xml.ftl";
outputFilePath = outputPath + File.separator + "pom.xml";
DynamicFileGenerator.doGenerate(inputFilePath , outputFilePath, meta);
6. 生成生成器文件
通过元信息自动生成模板文件的路径
1. 元信息定义
inputRootPath
:输入模板文件的根路径,即到哪里去找 FTL 模板文件(可以是相对路径)outputRootPath
:输出最终代码的根路径(可以是相对路径)type
:文件类别,目录或文件files
:子文件列表,支持递归inputPath
:输入文件的具体路径(可以是相对路径)outputPath
:输出文件的具体路径(可以是相对路径)generateType
:文件的生成类型,静态或动态
{
// ...
"fileConfig": {
"inputRootPath": "/Users/yupi/Code/acm-template",
"outputRootPath": "generated",
"type": "dir",
"files": [
{
"inputPath": "src/com/yupi/acm/MainTemplate.java.ftl",
"outputPath": "src/com/yupi/acm/MainTemplate.java",
"type": "file",
"generateType": "dynamic"
},
{
"inputPath": ".gitignore",
"outputPath": ".gitignore",
"type": "file",
"generateType": "static"
},
{
"inputPath": "README.md",
"outputPath": "README.md",
"type": "file",
"generateType": "static"
}
]
}
}
1. 文件路径规则选取
两种结构其实是可以通过编写程序相互转换的,所以目前先采用成本最低的方式
- 层层递归结构
"files": [
{
"path": "src",
"type": "dir",
"files": [
{
"path": "com",
"type": "dir",
"files": [
{
}
]
}
]
}
]
- 折叠路径结构
"files": [
{
"path": "src/com",
"type": "dir"
}
]
尽量折叠路径。原因:
- 考虑到 URL 地址的场景,路径的结构不一定需要展开
- 扁平化的路径会让 JSON 结构更清晰精简,前期更利于开发和维护
2. MainGenerator.java.ftl
package ${basePackage}.generator;
import com.yupi.model.DataModel;
import freemarker.template.TemplateException;
import java.io.File;
import java.io.IOException;
/**
* 核心生成器
*/
public class MainGenerator {
/**
* 生成
*
* @param model 数据模型
* @throws TemplateException
* @throws IOException
*/
public static void doGenerate(Object model) throws TemplateException, IOException {
String inputRootPath = "${fileConfig.inputRootPath}";
String outputRootPath = "${fileConfig.outputRootPath}";
String inputPath;
String outputPath;
<#list fileConfig.files as fileInfo>
inputPath = new File(inputRootPath, "${fileInfo.inputPath}").getAbsolutePath();
outputPath = new File(outputRootPath, "${fileInfo.outputPath}").getAbsolutePath();
<#if fileInfo.generateType == "static">
StaticGenerator.copyFilesByHutool(inputPath, outputPath);
<#else>
DynamicGenerator.doGenerate(inputPath, outputPath, model);
</#if>
</#list>
}
}
3. DynamicGenerator.java.ftl
通用的动态代码生成工具,所以不需要定制,修改下包名即可
package ${basePackage}.generator;
import cn.hutool.core.io.FileUtil;
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
/**
* 动态文件生成
*/
public class DynamicGenerator {
/**
* 生成文件
*
* @param inputPath 模板文件输入路径
* @param outputPath 输出路径
* @param model 数据模型
* @throws IOException
* @throws TemplateException
*/
public static void doGenerate(String inputPath, String outputPath, Object model) throws IOException, TemplateException {
// new 出 Configuration 对象,参数为 FreeMarker 版本号
Configuration configuration = new Configuration(Configuration.VERSION_2_3_32);
// 指定模板文件所在的路径
File templateDir = new File(inputPath).getParentFile();
configuration.setDirectoryForTemplateLoading(templateDir);
// 设置模板文件使用的字符集
configuration.setDefaultEncoding("utf-8");
// 创建模板对象,加载指定模板
String templateName = new File(inputPath).getName();
Template template = configuration.getTemplate(templateName);
// 文件不存在则创建文件和父目录
if (!FileUtil.exist(outputPath)) {
FileUtil.touch(outputPath);
}
// 生成
Writer out = new FileWriter(outputPath);
template.process(model, out);
// 生成文件后别忘了关闭哦
out.close();
}
}
4. StaticGenerator.java.ftl
通用的静态文件生成工具,所以不需要定制,修改下包名即可
package ${basePackage}.generator;
import cn.hutool.core.io.FileUtil;
/**
* 静态文件生成
*/
public class StaticGenerator {
/**
* 拷贝文件(Hutool 实现,会将输入目录完整拷贝到输出目录下)
*
* @param inputPath
* @param outputPath
*/
public static void copyFilesByHutool(String inputPath, String outputPath) {
FileUtil.copy(inputPath, outputPath, false);
}
}
5. 调用测试
// generator.DynamicGenerator
inputFilePath = inputResourcePath + File.separator + "templates/java/generator/DynamicGenerator.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/generator/DynamicGenerator.java";
DynamicFileGenerator.doGenerate(inputFilePath , outputFilePath, meta);
// generator.MainGenerator
inputFilePath = inputResourcePath + File.separator + "templates/java/generator/MainGenerator.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/generator/MainGenerator.java";
DynamicFileGenerator.doGenerate(inputFilePath , outputFilePath, meta);
// generator.StaticGenerator
inputFilePath = inputResourcePath + File.separator + "templates/java/generator/StaticGenerator.java.ftl";
outputFilePath = outputBaseJavaPackagePath + "/generator/StaticGenerator.java";
DynamicFileGenerator.doGenerate(inputFilePath , outputFilePath, meta);
7. 工具构建 jar 包
之前是通过手动执行 Maven 命令来构建 jar 包的,如果要实现程序自动构建 jar 包,只需要让程序来执行 Maven 打包命令即可
1. 开发实现
- 在本地(或服务器)安装 Maven 并配置环境变量,参考教程
- 执行
mvn -v
命令检测是否安装成功
- 执行
vim ~/.zshrc
export MAVEN_HOME=/Applications/IntelliJ\ IDEA.app/Contents/plugins/maven/lib/maven3
export PATH=$PATH:$MAVEN_HOME/bin
source ~/.zshrc
- 在
generator
目录下新建JarGenerator.java
类,编写 jar 包构建逻辑 - 程序实现的关键是:使用 Java 内置的
Process
类执行 Maven 打包命令,并获取到命令的输出信息。注意:- 不同的操作系统,执行的命令代码不同
- 修改了环境变量,idea 要重启
package com.listao.maker.generator;
import org.junit.jupiter.api.Test;
import java.io.*;
import java.util.Map;
public class JarGenerator {
public static void doGenerate(String projectDir) throws IOException, InterruptedException {
// 清理之前的构建并打包
// 1. 注意:不同操作系统,执行的命令不同
String winMavenCommand = "mvn.cmd clean package -DskipTests=true";
String otherMavenCommand = "mvn clean package -DskipTests=true";
String mavenCommand = otherMavenCommand;
// 2. 这里一定要拆分!
ProcessBuilder processBuilder = new ProcessBuilder(mavenCommand.split(" "));
processBuilder.directory(new File(projectDir));
Map<String, String> environment = processBuilder.environment();
System.out.println(environment);
Process process = processBuilder.start();
// 3. 读取命令的输出
InputStream inputStream = process.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
// 4. 等待命令执行完成
int exitCode = process.waitFor();
System.out.println("命令执行结束,退出码:" + exitCode);
}
@Test
public void test() throws IOException, InterruptedException {
doGenerate(System.getProperty("user.dir") + "/.tmp/01-local-generator");
}
}
2. 调用测试
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.listao:01-local-generator >--------------------
[INFO] Building 01-local-generator 1.0
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ 01-local-generator ---
[INFO] Deleting /Users/listao/mca/listao_generator/02-tool-generator/.tmp/01-local-generator/target
[INFO]
[INFO] --- resources:3.3.0:resources (default-resources) @ 01-local-generator ---
[INFO] skip non existing resourceDirectory /Users/listao/mca/listao_generator/02-tool-generator/.tmp/01-local-generator/src/main/resources
[INFO]
[INFO] --- compiler:3.10.1:compile (default-compile) @ 01-local-generator ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 10 source files to /Users/listao/mca/listao_generator/02-tool-generator/.tmp/01-local-generator/target/classes
[INFO]
[INFO] --- resources:3.3.0:testResources (default-testResources) @ 01-local-generator ---
[INFO] skip non existing resourceDirectory /Users/listao/mca/listao_generator/02-tool-generator/.tmp/01-local-generator/src/test/resources
[INFO]
[INFO] --- compiler:3.10.1:testCompile (default-testCompile) @ 01-local-generator ---
[INFO] No sources to compile
[INFO]
[INFO] --- surefire:3.0.0:test (default-test) @ 01-local-generator ---
[INFO] Tests are skipped.
[INFO]
[INFO] --- jar:3.3.0:jar (default-jar) @ 01-local-generator ---
[INFO] Building jar: /Users/listao/mca/listao_generator/02-tool-generator/.tmp/01-local-generator/target/01-local-generator-1.0.jar
[INFO]
[INFO] --- assembly:3.3.0:single (default) @ 01-local-generator ---
[INFO] Building jar: /Users/listao/mca/listao_generator/02-tool-generator/.tmp/01-local-generator/target/01-local-generator-1.0-jar-with-dependencies.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.750 s
[INFO] Finished at: 2025-01-25T15:28:18+08:00
[INFO] ------------------------------------------------------------------------
[WARNING]
[WARNING] Plugin validation issues were detected in 3 plugin(s)
[WARNING]
[WARNING] * org.apache.maven.plugins:maven-assembly-plugin:3.3.0
[WARNING] * org.apache.maven.plugins:maven-compiler-plugin:3.10.1
[WARNING] * org.apache.maven.plugins:maven-resources-plugin:3.3.0
[WARNING]
[WARNING] For more or less details, use 'maven.plugin.validation' property with one of the values (case insensitive): [BRIEF, DEFAULT, VERBOSE]
[WARNING]
命令执行结束,退出码:0
8. 工具封装脚本
有了 jar 包后,就可以根据 jar 包的路径,让程序来自动生成脚本文件了
1. 开发实现
- 脚本文件的内容非常简单,只有几行代码,所以不用编写 FTL 模板了,直接使用 StringBuilder 拼接字符串,然后写入文件
- 注意,如果是 Linux 系统,还需要在生成文件后,使用
PosixFilePermissions
类给文件默认添加可执行权限
public class ScriptGenerator {
public static void doGenerate(String outputPath, String jarPath) throws IOException {
// 直接写入脚本文件
// linux
StringBuilder sb = new StringBuilder();
sb.append("#!/bin/bash").append("\n");
sb.append(String.format("java -jar %s \"$@\"", jarPath)).append("\n");
FileUtil.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8), outputPath + ".sh");
// 添加可执行权限
try {
Set<PosixFilePermission> permissions = PosixFilePermissions.fromString("rwxrwxrwx");
Files.setPosixFilePermissions(Paths.get(outputPath + ".sh"), permissions);
} catch (Exception e) {
}
// windows
sb = new StringBuilder();
sb.append("@echo off").append("\n");
sb.append(String.format("java -jar %s %%*", jarPath)).append("\n");
FileUtil.writeBytes(sb.toString().getBytes(StandardCharsets.UTF_8), outputPath + ".bat");
}
public static void main(String[] args) throws IOException {
Meta meta = MetaManager.getMetaObject();
String outputPath = System.getProperty("user.dir") + "/.tmp/" + meta.getName() + "/generator";
String jarName = String.format("%s-%s-jar-with-dependencies.jar", meta.getName(), meta.getVersion());
String jarPath = "target/" + jarName;
System.out.println("outputPath = " + outputPath);
doGenerate(outputPath, jarPath);
}
}
2. 调用测试
生成脚本文件
在终端中进入生成的项目目录,运行脚本文件
➜ 01-local-generator-dist git:(main) ✗# ./generator.sh generate -h
Usage: 01-local-generator generate [-hV] [-l[=<loop>]] [--needGit[=<needGit>]]
生成代码
-h, --help Show this help message and exit.
-l, --loop[=<loop>] 是否生成循环
--needGit[=<needGit>]
是否生成 .gitignore 文件
-V, --version Print version information and exit.