02-代码生成
开发本地代码生成器
- 完成项目的初始化
- 静态文件生成
- 动态文件代码生成
- FreeMarker 模板引擎入门及实战
- 动静结合 - ACM 示例项目模板代码生成
1. 项目初始化
1. 初始化根目录
- 项目包含多个阶段,本质上是多个项目
- 让不同项目模块可以用 相对路径 寻找文件,便于整个项目的开源共享
- 使用 Git 来管理项目。如果使用 IDEA 开发工具来创建新项目,可以直接勾选
Create Git repository
,自动初始化项目为 Git 仓库- 也可以进入项目根目录,执行
git init
命令创建 Git 仓库
- 也可以进入项目根目录,执行
2. 忽略无用提交
- 需要使用
.gitignore
文件来忽略这些文件,不让它们被 Git 托管 - IDEA 的 Settings => Plugins 中搜索《.ignore》插件并安装
- 然后在项目根目录处选中右键,使用《.ignore》插件创建
.gitignore
文件
- 《.ignore》插件提供了很多默认的
.gitignore
模板,根据项目类型和使用的开发工具进行选择
- 然后可以在项目根目录看到生成的
.gitignore
文件,模板已经包含了常用的 Java 项目忽略清单- eg:编译后的文件、日志文件、压缩包等
- 再手动添加几个要忽略的目录和文件(eg:打包生成的 target 目录)
- 即使有些文件已经添加到了
.gitignore
文件中,在 IDEA 中显示的还是绿色(已被 Git 托管)状态 - 因为这些文件已经被 Git 跟踪。而
.gitignore
文件仅影响未跟踪的文件
# 取消 Git 跟踪
git rm -rf --cached .
- 在项目根目录中新建一个
README.md
文件,用于介绍项目、记录学习和开发过程等
3. 创建Demo工程
- 新建一个
yuzi-generator-demo-projects
目录,统一存放所有的示例代码
1. ACM示例代码模板
- 一个干净的 Java 项目,没有使用 Maven 和任何第三方依赖。静态文件
README.md
、代码文件MainTemplate
# ACM 模板代码
> 作者:程序员谷牛
提供 ACM 模式的 Java 示例代码
package com.yupi.acm;
import java.util.Scanner;
/**
* ACM 输入模板(多数之和)
*/
public class MainTemplate {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
// 读取输入元素个数
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("Sum: " + sum);
}
scanner.close();
}
}
4. 创建本地代码生成器项目
使用 IDEA 开发工具,在项目根目录中新建工程,创建 yuzi-generator-basic
项目。需要注意以下几点:
- 项目存放位置在
yuzi-generator
目录下 - 取消 Git 仓库勾选(因为已经在外层进行 Git 托管)
- 使用 Maven 管理项目
- JDK 选择 1.8!不要追求新版本(切换到
https://start.aliyun.com
即可选择 JDK8)
- 指定 GroupId 和 ArtifactId
- 创建好
yuzi-generator-basic
项目后, 一定要在新的 IDEA 窗口中打开项目!!!不要直接在yuzi-generator
根工程中打开!!!(后续获取路径时出现了一些小问题)
- 创建好
pom.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>
2. 实现流程
制作本地代码生成器(基于命令行的脚手架),根据用户的输入,生成不同的 ACM 示例代码模板
1. 需求拆解
需求进行 拆解 => 本地代码生成器 + 基于命令行的脚手架
- 本地代码生成器
README.md
的作用仅仅是描述项目,直接复制即可。将这类文件定义为 静态文件MainTemplate.java
是开发者实际要使用的 ACM 输入模板文件,默认是包含了循环接受输入的逻辑的- 作为一个 基础模板 ,能够接受用户的输入从而支持定制化生成的。将这类文件定义为 动态文件
- 如何制作基于命令行的脚手架
- 先通过直接运行
main()
、在方法中写死输入参数,实现完整的代码生成逻辑 - 只需要把在
main()
中写死的输入参数改为读取命令行来接收,再改变执行方式,把main()
运行改为调用 jar 包(脚本)
- 先通过直接运行
2. 实现步骤
- 生成静态文件,通过
main()
运行 - 生成动态文件,通过
main()
运行 - 同时生成静态和动态文件,通过
main()
运行,得到完整代码生成 - 开发命令行工具,接受用户的输入并生成完整代码
- 将工具封装为 jar 包和脚本,供用户调用
3. 静态文件生成
输入一个项目的目录,在另一个位置生成一模一样的项目文件
1. 工具库复制目录
- Hutool 是一个功能非常齐全的工具集,包含了 HTTP 请求、日期时间处理、集合类处理、文件处理、JSON 处理等能够大幅提高开发效率的工具类
- 优点:非常简单
- 缺点:不够灵活,只能整个目录生成,如果想忽略目录中的某个文件,就得生成后再删除
/**
* 静态文件生成
*/
public class StaticGenerator {
@Test
public void staticFile() {
// 获取整个项目的根路径(当前 @Test 项目)
String projectPath = System.getProperty("user.dir");
// File parentFile = new File(projectPath).getParentFile();
// 输入路径:代码模板目录
String inputPath = projectPath + File.separator + "src/main/resources/acm-template";
// 输出路径:直接输出到项目的根目录
String outputPath = new File(projectPath, ".tmp").getAbsolutePath();
copyFilesByHutool(inputPath, outputPath);
}
/**
* 拷贝文件(Hutool 实现,会将输入目录完整拷贝到输出目录下)
*/
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 下
* <p>
* 核心思路:先创建目录,然后遍历目录内的文件,依次复制
*/
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);
}
}
}
2. 递归遍历
- 手动编写递归算法依次遍历所有目录和文件
- 参考
FileUtil.copy()
源码
/**
* 执行拷贝<br>
* 拷贝规则为:
* <pre>
* 1、源为文件,目标为已存在目录,则拷贝到目录下,文件名不变
* 2、源为文件,目标为不存在路径,则目标以文件对待(自动创建父级目录)比如:/dest/aaa,如果aaa不存在,则aaa被当作文件名
* 3、源为文件,目标是一个已存在的文件,则当{@link #setOverride(boolean)}设为true时会被覆盖,默认不覆盖
* 4、源为目录,目标为已存在目录,当{@link #setCopyContentIfDir(boolean)}为true时,只拷贝目录中的内容到目标目录中,否则整个源目录连同其目录拷贝到目标目录中
* 5、源为目录,目标为不存在路径,则自动创建目标为新目录,然后按照规则4复制
* 6、源为目录,目标为文件,抛出IO异常
* 7、源路径和目标路径相同时,抛出IO异常
* </pre>
*
* @return 拷贝后目标的文件或目录
* @throws IORuntimeException IO异常
*/
@Override
public File copy() throws IORuntimeException {
final File src = this.src;
File dest = this.dest;
// check
Assert.notNull(src, "Source File is null !");
if (false == src.exists()) {
throw new IORuntimeException("File not exist: " + src);
}
Assert.notNull(dest, "Destination File or directiory is null !");
if (FileUtil.equals(src, dest)) {
throw new IORuntimeException("Files '{}' and '{}' are equal", src, dest);
}
if (src.isDirectory()) {// 复制目录
if(dest.exists() && false == dest.isDirectory()) {
// 源为目录,目标为文件,抛出IO异常
throw new IORuntimeException("Src is a directory but dest is a file!");
}
if(FileUtil.isSub(src, dest)) {
throw new IORuntimeException("Dest is a sub directory of src !");
}
final File subTarget = isCopyContentIfDir ? dest : FileUtil.mkdir(FileUtil.file(dest, src.getName()));
internalCopyDirContent(src, subTarget);
} else { // 复制文件
dest = internalCopyFile(src, dest);
}
return dest;
}
//----------------------------------------------------------------------------------------- Private method start
/**
* 拷贝目录内容,只用于内部,不做任何安全检查<br>
* 拷贝内容的意思为源目录下的所有文件和目录拷贝到另一个目录下,而不拷贝源目录本身
*
* @param src 源目录
* @param dest 目标目录
* @throws IORuntimeException IO异常
*/
private void internalCopyDirContent(File src, File dest) throws IORuntimeException {
if (null != copyFilter && false == copyFilter.accept(src)) {
//被过滤的目录跳过
return;
}
if (false == dest.exists()) {
//目标为不存在路径,创建为目录
//noinspection ResultOfMethodCallIgnored
dest.mkdirs();
} else if (false == dest.isDirectory()) {
throw new IORuntimeException(StrUtil.format("Src [{}] is a directory but dest [{}] is a file!", src.getPath(), dest.getPath()));
}
final String[] files = src.list();
if(ArrayUtil.isNotEmpty(files)){
File srcFile;
File destFile;
for (String file : files) {
srcFile = new File(src, file);
destFile = this.isOnlyCopyFile ? dest : new File(dest, file);
// 递归复制
if (srcFile.isDirectory()) {
internalCopyDirContent(srcFile, destFile);
} else {
internalCopyFile(srcFile, destFile);
}
}
}
}
按两下 shift,然后输入 sources 就能找到下载源码指令了
1. 文件操作API
// 1. 拷贝文件
Files.copy(src.toPath(), dest.toPath(), optionList.toArray(new CopyOption[0]));
// 2. 创建多级文件夹(哪怕中间有目录不存在)
File dest;
dest.mkdirs()
// 3. 判断是否为目录
File dest;
dest.isDirectory()
// 4. 文件是否存在
dest.exists()
2. 示例代码
- 递归算法的实现还是有一定复杂度的。核心思路:先在目标位置创建和源项目相同的目录,然后依次遍历源目录下的所有子文件并复制;如果子文件又是一个目录,则再遍历子文件下的所有 “孙” 文件,如此循环往复
- 优点:更灵活,可以依次对每一个文件进行处理
- 缺点:需要自己实现,不仅麻烦,还可能出现小 Bug
/**
* 递归拷贝文件(递归实现,会将输入目录完整拷贝到输出目录下)
*/
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 下
* <p>
* 核心思路:先创建目录,然后遍历目录内的文件,依次复制
*/
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);
}
}
3. 扩展思路
自己实现递归遍历,就可以很轻松地得到目录的完整结构树信息,可以由此制作出文件对比工具、目录分析工具、目录总结工具等
4. 动态文件生成思路
根据用户的输入参数动态生成文件
1. 明确动态生成需求
- 增加代码:在代码开头增加作者
@Author
注释 - 替换代码:修改程序输出的信息提示
- 可选代码:将循环读取输入改为单次读取
2. 动态生成的核心原理
使用已有的 模板引擎 技术,轻松实现模板编写和动态内容生成
- 用户输入参数:
author = ooxx
- 模板文件代码
/**
* ACM 输入模板(多数之和)
* @author ${author}
*/
- 将参数注入到模板文件中,生成完整代码
/**
* ACM 输入模板(多数之和)
* @author ooxx
*/
5. FreeMarker模板引擎
1. 什么是模板引擎
有很多现成的模板引擎技术:Java 的 Thymeleaf、FreeMarker、Velocity,前端的 Mustache 等
- 模板引擎是一种用于生成动态内容的类库(或框架),通过将预定义的模板与特定数据合并,来生成最终的输出
- 优点:
- 提供现成的模板文件语法和解析能力。开发者只要按照特定要求去编写模板文件(eg:使用
${...}
语法),模板引擎就能自动将参数注入到模板中,得到完整文件,不用再编写解析逻辑 - 可以将数据和模板分离,让不同的开发人员独立工作(eg:后端专心开发业务逻辑提供数据,前端专心写模板等)
- 具有一些安全特性(eg:防止跨站脚本攻击等。所以强烈大家掌握至少一种模板引擎的用法)
- 提供现成的模板文件语法和解析能力。开发者只要按照特定要求去编写模板文件(eg:使用
2. FreeMarker
- FreeMarker 是 Apache 的开源模板引擎
- 优点:入门简单、灵活易扩展。不用和 Spring 开发框架、Servlet 环境、第三方依赖绑定,任何 Java 项目都可以使用
- 推荐的 FreeMarker 学习方式:FreeMarker 官方文档
3. 模板引擎作用
FreeMarker 接受模板和 Java 对象,对它们进行处理,输出完整的内容
1. 模板
- FreeMarker 拥有自己的模板编写规则,一般用 FTL 表示 FreeMarker 模板语言
- 4 个核心部分组成:
- 文本:固定的内容,会按原样输出
- 插值:
${...}
语法来占位,尖括号中的内容在经过计算和替换后,才会输出 - FTL 指令:像 HTML 的标签语法,通过
<#xxx ... >
来实现各种特殊功能。<#list elements as element>
实现循环 - 注释:和 HTML 注释类似,使用
<#-- ... -->
语法,注释中的内容不会输出
2. 数据模型
- 为模板准备的所有数据整体统称为 数据模型
- 在 FreeMarker 中,数据模型一般是树形结构,可以是复杂的 Java 对象、也可以是 HashMap 等更通用的结构
{
"currentYear": 2023,
"menuItems": [
{
"url": "https://listao.cn",
"label": "ooxx",
},
{
"url": "https://ooxx.com",
"label": "谷牛简历",
}
]
}
4. Demo实战
1. 引入依赖
Maven 项目
<!-- https://freemarker.apache.org/index.html -->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
Spring Boot 项目
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
2. 模板
<!DOCTYPE html>
<html>
<head>
<title>谷牛官网</title>
</head>
<body>
<h1>欢迎来到谷牛官网</h1>
<ul>
<#-- 循环渲染导航条 -->
<#list menuItems as item>
<li><a href="${item.url}">${item.label}</a></li>
</#list>
</ul>
<#-- 底部版权信息(注释部分,不会被输出)-->
<footer>
${currentYear} 谷牛官网. All rights reserved.
</footer>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>谷牛官网</title>
</head>
<body>
<h1>欢迎来到谷牛官网</h1>
<ul>
<li><a href="https://codefather.cn">ooxx导航</a></li>
<li><a href="https://laoyujianli.com">ooxx简历</a></li>
</ul>
<footer>
2,023 谷牛官网. All rights reserved.
</footer>
</body>
</html>
import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import java.io.FileWriter;
import java.io.Writer;
/**
* FreeMarker
*/
public class FreeMarkerTest {
@Test
public void test() throws IOException, TemplateException {
// FreeMarker 版本号
Configuration configuration = new Configuration(Configuration.VERSION_2_3_32);
// 指定模板文件所在的路径
configuration.setDirectoryForTemplateLoading(new File("src/main/resources/templates"));
// 设置模板文件使用的字符集
configuration.setDefaultEncoding("utf-8");
configuration.setNumberFormat("0.######");
// 创建模板对象,加载指定模板
Template template = configuration.getTemplate("myweb.html.ftl");
// 创建数据模型
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("currentYear", 2023);
List<Map<String, Object>> menuItems = new ArrayList<>();
Map<String, Object> menuItem1 = new HashMap<>();
menuItem1.put("url", "https://codefather.cn");
menuItem1.put("label", "ooxx导航");
Map<String, Object> menuItem2 = new HashMap<>();
menuItem2.put("url", "https://laoyujianli.com");
menuItem2.put("label", "ooxx简历");
menuItems.add(menuItem1);
menuItems.add(menuItem2);
dataModel.put("menuItems", menuItems);
// 生成
Writer out = new FileWriter("myweb.html");
template.process(dataModel, out);
// 生成文件后关闭
out.close();
}
}
5. 常用语法
1. 插值
基本语法 ${xxx}
。但插值还有很多(eg:传递表达式),不建议
${100 + money}
2. 分支和判空
<#if user == "谷牛">
我是谷牛
<#else>
我是猪皮
</#if>
常用场景就是判空(eg:要判断 user 参数是否存在)
<#if user??>
存在用户
<#else>
用户不存在
</#if>
3. 默认值
- FreeMarker 对变量的空值校验很严格,如果模板中某个对象为空,FreeMarker 会报错,而导致模板生成中断
- 为空的参数都设置默认值
${user!"用户为空"}
4. 循环
<#list users as user>
${user}
</#list>
5. 宏定义
可以把 “宏” 理解为一个预定义的模板片段。支持给宏传入变量,来复用模板片段
- card 是宏的名称,userName 是宏接受的参数
- 自定义指令
<#macro card userName>
---------
${userName}
---------
</#macro>
用 @
语法来使用宏
<@card userName="谷牛"/>
<@card userName="二黑"/>
---------
谷牛
---------
---------
二黑
---------
6. 内建函数
- 内建函数大全参考
- FreeMarker 为了提高开发者处理参数效率,而提供的的语法糖,可以通过
?
来调用内建函数
<!-- 字符串转为大写 -->
${userName?upper_case}
<!-- 输出序列长度 -->
${myList?size}
<!-- 循环语法中依次输出元素的下标,使用循环表达式自带的 `index` 内建函数 -->
<#list users as user>
${user?index}
</#list>
7. 其他
命名空间,其实就相当于 Java 中的包,用于隔离代码、宏、变量等
8. 问题解决示例
- 发现数字中间加了一个逗号分割符
- 因为 FreeMarker 使用 Java 平台的本地化敏感的数字格式信息
<footer>
2,023 谷牛官网. All rights reserved.
</footer>
// 修改 configuration 配置类的 number_format 设置,即可调整默认生成的数字格式
configuration.setNumberFormat("0.######");
6. 动态文件生成
1. 定义数据模型
可以使用 HashMap
,不如定义对象更清晰、更规范
package com.listao.model;
import lombok.Data;
/**
* 动态模版配置
*/
@Data
public class DataModel {
/**
* 是否生成循环
*/
private boolean loop;
/**
* 作者注释
*/
private String author = "listao";
/**
* 输出信息
*/
private String outputText = "sum = ";
}
2. 编写动态模板
先复制原始代码,再挖坑。模板和上面定义的数据模型名称保持一致
- 使用插值表达式
${author}
接受作者名称 - 使用
<#if loop> ... </#if>
分支,控制是否生成循环代码 - 使用
${outputText}
控制输出信息
package com.listao.acm;
import java.util.Scanner;
/**
* 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 = '}" + sum);
<#if loop>
}
</#if>
scanner.close();
}
}
3. 调用Freemarker引擎
package com.listao.generator;
/**
* 动态文件生成
*/
public class DynamicGenerator {
@Test
public void test() throws IOException, TemplateException {
// 采用 main(),取决于 idea 打开的项目根路径
// String projectPath = System.getProperty("user.dir") + "/01-local-generator";
String projectPath = System.getProperty("user.dir");
String inputPath = projectPath + File.separator + "src/main/resources/templates/MainTemplate.java.ftl";
String outputPath = projectPath + File.separator + ".tmp/MainTemplate.java";
DataModel model = new DataModel();
model.setAuthor("listao");
model.setLoop(false);
model.setOutputText("求和结果:");
doGenerate(inputPath, outputPath, model);
}
/**
* 生成文件
*
* @param inputPath 模板文件输入路径
* @param outputPath 输出路径
* @param model 数据模型
*/
public static void doGenerate(String inputPath, String outputPath, Object model) throws IOException, TemplateException {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_32);
// 指定模板文件所在的路径
File templateDir = new File(inputPath).getParentFile();
cfg.setDirectoryForTemplateLoading(templateDir);
// 设置模板文件使用的字符集
cfg.setDefaultEncoding("utf-8");
// 创建模板对象,加载指定模板
String templateName = new File(inputPath).getName();
Template template = cfg.getTemplate(templateName);
// 文件不存在则创建文件和父目录
if (!FileUtil.exist(outputPath)) {
FileUtil.touch(outputPath);
}
// 生成
Writer out = new FileWriter(outputPath);
template.process(model, out);
out.close();
}
}
4. 完善优化
数据模型的字符串变量不设置任何值,报错:
Oct 27, 2024 2:10:20 PM freemarker.log._JULLoggerFactory$JULLogger error
SEVERE: Error executing FreeMarker template
FreeMarker template error:
The following has evaluated to null or missing:
==> outputText [in template "MainTemplate.java.ftl" at line 32, column 35]
所有字符串指定一个默认值
// 1. 直接给 POJO 设置默认值:
private String outputText = "sum = ";
// 2. 使用 FreeMarker 的默认值操作符
System.out.println("${outputText!'sum = '}" + sum);
7. 动静结合
- 组合调用这两个生成器
- 先复制静态文件
- 再动态生成文件来覆盖
package com.listao.generator;
/**
* 核心生成器
*/
public class MainGenerator {
@Test
public void test() throws TemplateException, IOException {
DataModel dataModel = new DataModel();
dataModel.setAuthor("ooxx");
dataModel.setLoop(false);
dataModel.setOutputText("求和结果:");
doGenerate(dataModel);
}
public static void doGenerate(Object model) throws TemplateException, IOException {
// String projectPath = System.getProperty("user.dir");
String projectPath = "/Users/listao/mca/listao_generator/01-local-generator";
// 整个项目的根路径
// 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";
// 1. 生成静态文件
StaticGenerator.copyFilesByHutool(inputPath, outputPath);
// 2. 生成动态文件
String inputDynamicFilePath = projectPath + File.separator + "src/main/resources/templates/MainTemplate.java.ftl";
String outputDynamicFilePath = outputPath + File.separator + "acm-template/src/com/yupi/acm/MainTemplate.java";
DynamicGenerator.doGenerate(inputDynamicFilePath, outputDynamicFilePath, model);
System.out.println("outputPath = " + outputPath);
}
}