02-代码生成

开发本地代码生成器

  1. 完成项目的初始化
  2. 静态文件生成
  3. 动态文件代码生成
  4. FreeMarker 模板引擎入门及实战
  5. 动静结合 - ACM 示例项目模板代码生成

1. 项目初始化

1. 初始化根目录

  • 项目包含多个阶段,本质上是多个项目
  • 让不同项目模块可以用 相对路径 寻找文件,便于整个项目的开源共享
  • 使用 Git 来管理项目。如果使用 IDEA 开发工具来创建新项目,可以直接勾选 Create Git repository ,自动初始化项目为 Git 仓库
    • 也可以进入项目根目录,执行 git init 命令创建 Git 仓库
image.png

2. 忽略无用提交

  • 需要使用 .gitignore 文件来忽略这些文件,不让它们被 Git 托管
  • IDEA 的 Settings => Plugins 中搜索《.ignore》插件并安装
image.png
  • 然后在项目根目录处选中右键,使用《.ignore》插件创建 .gitignore 文件
image.png
  • 《.ignore》插件提供了很多默认的 .gitignore 模板,根据项目类型和使用的开发工具进行选择
image.png
  • 然后可以在项目根目录看到生成的 .gitignore 文件,模板已经包含了常用的 Java 项目忽略清单
    • eg:编译后的文件、日志文件、压缩包等
image.png
  • 再手动添加几个要忽略的目录和文件(eg:打包生成的 target 目录)
a920c702761a47c580b6511d6c7846cc
  • 即使有些文件已经添加到了 .gitignore 文件中,在 IDEA 中显示的还是绿色(已被 Git 托管)状态
  • 因为这些文件已经被 Git 跟踪。而 .gitignore 文件仅影响未跟踪的文件
# 取消 Git 跟踪
git rm -rf --cached .
  • 在项目根目录中新建一个 README.md 文件,用于介绍项目、记录学习和开发过程等

3. 创建Demo工程

  • 新建一个 yuzi-generator-demo-projects 目录,统一存放所有的示例代码
60cd7b2751824cf486451ff0ff46f848

1. ACM示例代码模板

  • 一个干净的 Java 项目,没有使用 Maven 和任何第三方依赖。静态文件 README.md、代码文件 MainTemplate
d6a2e8f58eaa41d2908ec5c76a0d233f
# 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 项目。需要注意以下几点:

  1. 项目存放位置在 yuzi-generator 目录下
  2. 取消 Git 仓库勾选(因为已经在外层进行 Git 托管)
  3. 使用 Maven 管理项目
  4. JDK 选择 1.8!不要追求新版本(切换到 https://start.aliyun.com 即可选择 JDK8)
image-20241027095617638
  1. 指定 GroupId 和 ArtifactId
    • 创建好 yuzi-generator-basic 项目后, 一定要在新的 IDEA 窗口中打开项目!!!不要直接在 yuzi-generator 根工程中打开!!!(后续获取路径时出现了一些小问题)
a1013f312acc4340af9236357ee3cecd
  • 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. 需求拆解

需求进行 拆解 => 本地代码生成器 + 基于命令行的脚手架

b657d0b5055d4f04b604ec69cf1762b4
  • 本地代码生成器
    • README.md 的作用仅仅是描述项目,直接复制即可。将这类文件定义为 静态文件
    • MainTemplate.java 是开发者实际要使用的 ACM 输入模板文件,默认是包含了循环接受输入的逻辑的
      • 作为一个 基础模板 ,能够接受用户的输入从而支持定制化生成的。将这类文件定义为 动态文件
  • 如何制作基于命令行的脚手架
    • 先通过直接运行 main()、在方法中写死输入参数,实现完整的代码生成逻辑
    • 只需要把在 main() 中写死的输入参数改为读取命令行来接收,再改变执行方式,把 main() 运行改为调用 jar 包(脚本)

2. 实现步骤

  1. 生成静态文件,通过 main() 运行
  2. 生成动态文件,通过 main() 运行
  3. 同时生成静态和动态文件,通过 main() 运行,得到完整代码生成
  4. 开发命令行工具,接受用户的输入并生成完整代码
  5. 将工具封装为 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 就能找到下载源码指令了

f0852006aab247a5908e0a48886178ed

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. 明确动态生成需求

  1. 增加代码:在代码开头增加作者 @Author 注释
  2. 替换代码:修改程序输出的信息提示
  3. 可选代码:将循环读取输入改为单次读取

2. 动态生成的核心原理

使用已有的 模板引擎 技术,轻松实现模板编写和动态内容生成

  1. 用户输入参数:
author = ooxx
  1. 模板文件代码
/**
 * ACM 输入模板(多数之和)
 * @author ${author}
 */


 

  1. 将参数注入到模板文件中,生成完整代码
/**
 * ACM 输入模板(多数之和)
 * @author ooxx
 */

5. FreeMarker模板引擎

1. 什么是模板引擎

有很多现成的模板引擎技术:Java 的 Thymeleaf、FreeMarker、Velocity,前端的 Mustache 等

  • 模板引擎是一种用于生成动态内容的类库(或框架),通过将预定义的模板与特定数据合并,来生成最终的输出
  • 优点:
    • 提供现成的模板文件语法和解析能力。开发者只要按照特定要求去编写模板文件(eg:使用 ${...} 语法),模板引擎就能自动将参数注入到模板中,得到完整文件,不用再编写解析逻辑
    • 可以将数据和模板分离,让不同的开发人员独立工作(eg:后端专心开发业务逻辑提供数据,前端专心写模板等)
    • 具有一些安全特性(eg:防止跨站脚本攻击等。所以强烈大家掌握至少一种模板引擎的用法)

2. FreeMarker

  • FreeMarker 是 Apache 的开源模板引擎
    • 优点:入门简单、灵活易扩展。不用和 Spring 开发框架、Servlet 环境、第三方依赖绑定,任何 Java 项目都可以使用
  • 推荐的 FreeMarker 学习方式:FreeMarker 官方文档open in new window
6d344e2fde00484298991c6ac8529293

3. 模板引擎作用

FreeMarker 接受模板和 Java 对象,对它们进行处理,输出完整的内容

a3cdc6bfd77a473ebb92a68ba460c1de

1. 模板

  • FreeMarker 拥有自己的模板编写规则,一般用 FTL 表示 FreeMarker 模板语言
  • 4 个核心部分组成:
    1. 文本:固定的内容,会按原样输出
    2. 插值:${...} 语法来占位,尖括号中的内容在经过计算和替换后,才会输出
    3. FTL 指令:像 HTML 的标签语法,通过 <#xxx ... > 来实现各种特殊功能。<#list elements as element> 实现循环
    4. 注释:和 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. 宏定义

可以把 “宏” 理解为一个预定义的模板片段。支持给宏传入变量,来复用模板片段

<#macro card userName>
---------
${userName}
---------
</#macro>

@ 语法来使用宏

<@card userName="谷牛"/>
<@card userName="二黑"/>
---------
谷牛
---------
---------
二黑
---------

6. 内建函数

<!-- 字符串转为大写 -->
${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>

通过查阅官方文档open in new window

941305f0f0954735b326344800f73b3a
// 修改 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. 动静结合

  • 组合调用这两个生成器
    1. 先复制静态文件
    2. 再动态生成文件来覆盖
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);
    }

}