第二章 代码生成
前言
本笔记主要用途是学习up 主程序员鱼皮的项目:代码生成器时的一些学习心得。
代码地址:https://github.com/Liucc-123/yuzi-generator.git
项目教程:https://www.codefather.cn/course/1790980795074654209
本节重点
本节属于项目的第一阶段:开发本地代码生成器。重点内容包括:
- 项目初始化。
- 静态文件生成。
- 动态文件代码生成。
- FreeMarker 模板引擎入门及实战。
- 动静结合 - ACM 示例项目模板代码生成。
一、项目初始化
- 初始化根目录
-
使用 IDEA创建一个干净的文件夹
yuzi-generator
作为整个项目的根目录。 -
使用 Git 管理项目,建议在项目根目录中初始化 Git 仓库。
-
- 忽略无用提交
-
使用
.gitignore
文件忽略项目中不需要提交的文件(如IDE自动生成的工程文件)。 -
推荐使用IDE插件(如
.ignore
插件)生成.gitignore
文件,并手动添加需要忽略的目录和文件。 -
忽略文件常见配置项设置
### Custom template .idea target yuzi-generator.iml .DS_Store### Java template # Compiled class file *.class# Log file *.log# BlueJ files *.ctxt# Mobile Tools for Java (J2ME) .mtj.tmp/# Package Files # *.jar *.war *.nar *.ear *.zip *.tar.gz *.rar# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid*### JetBrains template # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839# User-specific stuff .idea/**/workspace.xml .idea/**/tasks.xml .idea/**/usage.statistics.xml .idea/**/dictionaries .idea/**/shelf# AWS User-specific .idea/**/aws.xml# Generated files .idea/**/contentModel.xml# Sensitive or high-churn files .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml .idea/**/dbnavigator.xml# Gradle .idea/**/gradle.xml .idea/**/libraries# Gradle and Maven with auto-import # When using Gradle or Maven with auto-import, you should exclude module files, # since they will be recreated, and may cause churn. Uncomment if using # auto-import. # .idea/artifacts # .idea/compiler.xml # .idea/jarRepositories.xml # .idea/modules.xml # .idea/*.iml # .idea/modules # *.iml # *.ipr# CMake cmake-build-*/# Mongo Explorer plugin .idea/**/mongoSettings.xml# File-based project format *.iws# IntelliJ out/# mpeltonen/sbt-idea plugin .idea_modules/# JIRA plugin atlassian-ide-plugin.xml# Cursive Clojure plugin .idea/replstate.xml# SonarLint plugin .idea/sonarlint/# Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties# Editor-based Rest Client .idea/httpRequests# Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser
-
如果文件已被Git跟踪,需执行
git rm -rf --cached .
命令取消跟踪。
-
- 创建 Demo 示例代码工程
- 新建
yuzi-generator-demo-projects
目录,存放所有示例代码。 - 下载并复制 ACM 示例模板代码到该目录下(可通过云盘下载)。
- 新建
- 创建本地代码生成器项目
-
在项目根目录下新建
yuzi-generator-basic
项目,使用Maven管理项目。 -
JDK选择1.8,取消Git仓库勾选(因为外层已托管)。
-
在项目的
pom.xml
文件中引入 Hutool、Apache Commons Collections、Lombok 和 JUnit 等依赖。<dependencies><!-- 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>
-
二、实现流程
目标:制作本地代码生成器,根据用户输入生成不同的 ACM 示例代码模板。
- 需求拆解
- 将需求拆解为“生成静态文件”和“生成动态文件”两个步骤。
- 静态文件:直接复制,不做改动(如
README.md
)。 - 动态文件:基于模板文件和用户输入生成代码(如
MainTemplate.java
)。
- 实现步骤
- 生成静态文件(通过 Main 方法运行)。
- 生成动态文件(通过 Main 方法运行)。
- 同时生成静态和动态文件,得到完整代码。
- 开发命令行工具,接受用户输入并生成代码。
- 将工具封装为 jar 包和脚本,供用户调用。
三、静态文件生成
静态文件是指直接复制、不做任何改动的文件。
-
现成的工具库复制目录
-
使用 Hutool 的
FileUtil.copy
方法,一行代码实现目录复制。 -
示例代码:
public static void copyFilesByHutool(String inputPath, String outputPath) {FileUtil.copy(inputPath, outputPath, false); }
-
-
递归遍历
-
手动编写递归算法,逐个复制目录和文件。
-
示例代码:
public static void copyFilesByRecursive(String inputPath, String outputPath) {File inputFile = new File(inputPath);File outputFile = new File(outputPath);try {copyFileByRecursive(inputFile, outputFile);} catch (Exception e) {System.err.println("文件复制失败");e.printStackTrace();} }/*** 文件 A => 目录 B,则文件 A 放在目录 B 下* 文件 A => 文件 B,则文件 A 覆盖文件 B* 目录 A => 目录 B,则目录 A 放在目录 B 下** 核心思路:先创建目录,然后遍历目录内的文件,依次复制* @param inputFile* @param outputFile* @throws IOException*/ private static void copyFileByRecursive(File inputFile, File outputFile) throws IOException {// 区分是文件还是目录if (inputFile.isDirectory()) {System.out.println(inputFile.getName());File destOutputFile = new File(outputFile, inputFile.getName());// 如果是目录,首先创建目标目录if (!destOutputFile.exists()) {destOutputFile.mkdirs();}// 获取目录下的所有文件和子目录File[] files = inputFile.listFiles();// 无子文件,直接结束if (ArrayUtil.isEmpty(files)) {return;}for (File file : files) {// 递归拷贝下一层文件copyFileByRecursive(file, destOutputFile);}} else {// 是文件,直接复制到目标目录下Path destPath = outputFile.toPath().resolve(inputFile.getName());Files.copy(inputFile.toPath(), destPath, StandardCopyOption.REPLACE_EXISTING);} }
-
四、动态文件生成思路
动态文件是指需要根据用户输入生成的文件。
- 明确动态生成需求
- 增加作者注释(如
@author
)。 - 修改程序输出信息。
- 支持循环读取输入或单次读取。
- 增加作者注释(如
- 动态生成的核心原理
- 使用模板引擎(如 FreeMarker)实现动态内容生成。
- 提前编写模板文件,通过用户输入的参数替换模板中的占位符。
五、FreeMarker 模板引擎入门
FreeMarker 是一个开源模板引擎,用于生成动态内容。
官方手册:http://freemarker.foofun.cn/
-
模板引擎的作用
- 提供模板文件语法和解析能力。
- 将数据和模板分离,便于开发和维护。
-
模板
- 使用 FTL(FreeMarker Template Language)编写模板文件。
- 包含文本、插值(
${...}
)、FTL 指令(<#xxx>
)和注释(<#-- ... -->
)。
-
数据模型
- 为模板准备的数据,可以是 Java 对象或 HashMap。
-
Demo 实战
-
引入 FreeMarker 依赖:
<dependency><groupId>org.freemarker</groupId><artifactId>freemarker</artifactId><version>2.3.32</version> </dependency>
-
创建配置对象、加载模板、创建数据模型、指定输出路径并生成文件。
-
示例代码:
Configuration configuration = new Configuration(Configuration.VERSION_2_3_32); configuration.setDirectoryForTemplateLoading(new File("src/main/resources/templates")); configuration.setDefaultEncoding("utf-8"); Template template = configuration.getTemplate("myweb.html.ftl"); Map<String, Object> dataModel = new HashMap<>(); dataModel.put("currentYear", 2023); Writer out = new FileWriter("myweb.html"); template.process(dataModel, out); out.close();
-
-
常用语法
- 插值:
${变量}
。 - 分支:
<#if condition>...</#if>
。 - 默认值:
${变量!默认值}
。 - 循环:
<#list items as item>...</#list>
。 - 宏定义:
<#macro name>...</#macro>
。 - 内建函数:
变量?方法
。
- 插值:
六、动态文件生成实现
实现 ACM 示例模板项目的动态生成。
-
定义数据模型
-
创建
MainTemplateConfig
类,定义模板所需的参数(如作者、是否循环、输出信息)。 -
示例代码
/*** 动态模板配置*/ @Data public class MainTemplateConfig {/*** 作者注释信息*/private String author = "liucc"; // 默认值/*** 是否生成循环*/private boolean isLoop;/*** 输出信息*/private String outputText = "求和结果:"; }
-
-
编写动态模板
-
在
resources/templates
目录下创建MainTemplate.java.ftl
文件。 -
使用 FreeMarker 语法编写模板,例如:
package com.yupi.acm;/*** ACM 输入模板(多数之和)* @author ${author}*/ public class MainTemplate {public static void main(String[] args) {Scanner scanner = new Scanner(System.in); <#if loop>while (scanner.hasNext()) { </#if>int n = scanner.nextInt();int[] arr = new int[n];for (int i = 0; i < n; i++) {arr[i] = scanner.nextInt();}int sum = 0;for (int num : arr) {sum += num;}System.out.println("${outputText}" + sum); <#if loop>} </#if>scanner.close();} }
-
-
组合生成
-
在
DynamicGenerator
类中实现生成逻辑。 -
示例代码:
public static void doGenerate(String inputPath, String outputPath, Object model) throws IOException, TemplateException {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);Writer out = new FileWriter(outputPath);template.process(model, out);out.close(); }
-
-
完善优化
- 给字符串变量设置默认值,避免模板生成时出错。
- 抽取生成逻辑为方法,提高代码复用性。
七、动静结合 - 生成完整代码
将静态文件生成和动态文件生成结合,生成完整的 ACM 示例代码。
-
核心代码生成器(静态+动态文件生成)
package com.liucc;import com.liucc.model.MainTemplateConfig; import freemarker.template.TemplateException;import java.io.File; import java.io.IOException;/*** 核心模板生成器(静态+动态)*/ public class MainGenerator {public static void main(String[] args) throws TemplateException, IOException {MainTemplateConfig model = new MainTemplateConfig();model.setAuthor("liucc");model.setLoop(true);model.setOutputText("求和结果:");doGenerate(model);}public static void doGenerate(Object model) throws TemplateException, IOException {String projectPath = System.getProperty("user.dir"); // /Users/liuchuangchuang/code/yuzi-generator/yuzi-generator-basicString parentPath = new File(projectPath).getParentFile().getAbsolutePath();// 输入路径String inputPath = parentPath + File.separatorChar + "yuzi-generator-demo-projects/acm-template";String outputPath = projectPath;// 生成静态文件StaticGenerator.copyFilesByRecursive(inputPath, outputPath);String dynamicInputPath = projectPath + File.separatorChar + "src/main/resources/templates/MainTemplate.java.ftl";String dynamicOutputPath = projectPath + File.separatorChar + "MainTemplate.java";// 生成动态文件DynamicGenerator.doGenerate(dynamicInputPath, dynamicOutputPath, model);} }
-
静态文件生成器
package com.liucc;import cn.hutool.core.io.FileUtil; import cn.hutool.core.util.ArrayUtil;import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption;public class StaticGenerator {public static void main(String[] args) {// 获取整个项目的根路径String projectPath = System.getProperty("user.dir");File parentFile = new File(projectPath).getParentFile();// 输入路径:ACM 示例代码模板目录String inputPath = new File(parentFile, "yuzi-generator-demo-projects/acm-template").getAbsolutePath();// 输出路径:直接输出到项目的根目录String outputPath = projectPath;copyFilesByRecursive(inputPath, outputPath);}/*** 拷贝文件(Hutool 实现,会将输入目录完整拷贝到输出目录下)*Ω* @param inputPath* @param outputPath*/public static void copyFilesByHutool(String inputPath, String outputPath) {FileUtil.copy(inputPath, outputPath, false);}/*** 递归拷贝文件(递归实现,会将输入目录完整拷贝到输出目录下)* @param inputPath* @param outputPath*/public static void copyFilesByRecursive(String inputPath, String outputPath) {File inputFile = new File(inputPath);File outputFile = new File(outputPath);try {copyFileByRecursive(inputFile, outputFile);} catch (Exception e) {System.err.println("文件复制失败");e.printStackTrace();}}/*** 文件 A => 目录 B,则文件 A 放在目录 B 下* 文件 A => 文件 B,则文件 A 覆盖文件 B* 目录 A => 目录 B,则目录 A 放在目录 B 下** 核心思路:先创建目录,然后遍历目录内的文件,依次复制* @param inputFile* @param outputFile* @throws IOException*/private static void copyFileByRecursive(File inputFile, File outputFile) throws IOException {// 区分是文件还是目录if (inputFile.isDirectory()) {System.out.println(inputFile.getName());File destOutputFile = new File(outputFile, inputFile.getName());// 如果是目录,首先创建目标目录if (!destOutputFile.exists()) {destOutputFile.mkdirs();}// 获取目录下的所有文件和子目录File[] files = inputFile.listFiles();// 无子文件,直接结束if (ArrayUtil.isEmpty(files)) {return;}for (File file : files) {// 递归拷贝下一层文件copyFileByRecursive(file, destOutputFile);}} else {// 是文件,直接复制到目标目录下Path destPath = outputFile.toPath().resolve(inputFile.getName());Files.copy(inputFile.toPath(), destPath, StandardCopyOption.REPLACE_EXISTING);}}}
-
动态模板文件生成
package com.liucc;import com.liucc.model.MainTemplateConfig; 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 {public static void main(String[] args) throws IOException, TemplateException {// 获取整个项目的根路径String projectPath = System.getProperty("user.dir");System.out.println("user.dir:" + projectPath);// 输入路径:FTL 示例代码模板目录String inputPath = projectPath + File.separatorChar + "src/main/resources/templates/MainTemplate.java.ftl";// 输出路径:直接输出到项目的根目录String outputPath = projectPath + File.separatorChar + "MainTemplate.java";// 读取模板配置MainTemplateConfig config = new MainTemplateConfig();config.setAuthor("liucc");config.setLoop(true);config.setOutputText("求和结果:");// 生成模板doGenerate(inputPath, outputPath, config);}/**** @param inputPath 模板读取路径* @param outputPath 动态模板生成路径* @param model 数据模型* @throws IOException* @throws TemplateException*/public static void doGenerate(String inputPath, String outputPath, Object model) throws IOException, TemplateException {// 1、new 出 Configuration 对象,参数为 FreeMarker 版本号Configuration configuration = new Configuration(Configuration.VERSION_2_3_32);File templateFile = new File(inputPath).getParentFile();// 指定模板文件所在的路径configuration.setDirectoryForTemplateLoading(templateFile);// 设置模板文件使用的字符集configuration.setDefaultEncoding("utf-8");// 数字格式设置configuration.setNumberFormat("0.##########");// 2、创建模板对象,加载指定模板String templateName = new File(inputPath).getName();Template template = configuration.getTemplate(templateName);// 3、创建数据模型 ==> model// 4、生成Writer out = new FileWriter(outputPath);template.process(model, out);// 生成文件后别忘了关闭哦out.close();} }