第二章 代码生成
前言
本笔记主要用途是学习up 主程序员鱼皮的项目:代码生成器时的一些学习心得。
本节重点
本节属于项目的第一阶段:开发本地代码生成器。重点内容包括:
- 项目初始化。
- 静态文件生成。
- 动态文件代码生成。
- FreeMarker 模板引擎入门及实战。
- 动静结合 - ACM 示例项目模板代码生成。
一、项目初始化
-
初始化根目录
-
使用 IDEA创建一个干净的文件夹
yuzi-generator
作为整个项目的根目录。 -
使用 Git 管理项目,建议在项目根目录中初始化 Git 仓库。
-
-
忽略无用提交
-
使用
.gitignore
文件忽略项目中不需要提交的文件(如IDE自动生成的工程文件)。 -
推荐使用IDE插件(如
.ignore
插件)生成.gitignore
文件,并手动添加需要忽略的目录和文件。 -
忽略文件常见配置项设置
xml### 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 等依赖。xml<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
方法,一行代码实现目录复制。 -
示例代码:
javapublic static void copyFilesByHutool(String inputPath, String outputPath) { FileUtil.copy(inputPath, outputPath, false); }
-
-
递归遍历
-
手动编写递归算法,逐个复制目录和文件。
-
示例代码:
javapublic 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 是一个开源模板引擎,用于生成动态内容。
-
模板引擎的作用
- 提供模板文件语法和解析能力。
- 将数据和模板分离,便于开发和维护。
-
模板
- 使用 FTL(FreeMarker Template Language)编写模板文件。
- 包含文本、插值(
${...}
)、FTL 指令(<#xxx>
)和注释(<#-- ... -->
)。
-
数据模型
- 为模板准备的数据,可以是 Java 对象或 HashMap。
-
Demo 实战
-
引入 FreeMarker 依赖:
xml<dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> <version>2.3.32</version> </dependency>
-
创建配置对象、加载模板、创建数据模型、指定输出路径并生成文件。
-
示例代码:
javaConfiguration 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
类,定义模板所需的参数(如作者、是否循环、输出信息)。 -
示例代码
java/** * 动态模板配置 */ @Data public class MainTemplateConfig { /** * 作者注释信息 */ private String author = "liucc"; // 默认值 /** * 是否生成循环 */ private boolean isLoop; /** * 输出信息 */ private String outputText = "求和结果:"; }
-
-
编写动态模板
-
在
resources/templates
目录下创建MainTemplate.java.ftl
文件。 -
使用 FreeMarker 语法编写模板,例如:
javapackage 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
类中实现生成逻辑。 -
示例代码:
javapublic 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 示例代码。
-
核心代码生成器(静态+动态文件生成)
javapackage 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-basic String 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); } }
-
静态文件生成器
javapackage 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); } } }
-
动态模板文件生成
javapackage 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(); } }