开发本地代码生成器原创
# 1 项目的初始化
# 1、初始化根目录
由于我们的项目包含多个阶段,本质上是多个项目,所以为了统一管理整个项目,我们创建一个干净的 yuzi-generator 空文件夹,作为整个项目的根目录,后续各阶段的项目和目录都放到它之下。
这样做还有一个好处,就是让不同项目模块可以用 相对路径 寻找文件,便于整个项目的开源共享。
建议大家养成习惯,使用 Git 来管理项目。如果使用 IDEA 开发工具来创建新项目,可以直接勾选 Create Git repository ,工具会自动帮你初始化项目为 Git 仓库。
如下图:
当然,也可以进入项目根目录,执行 git init 命令创建 Git 仓库。
# 2、忽略无用提交
创建好新项目后,使用 IDEA 开发工具打开项目,进入底部的 Git 标签,会发现很多和项目无关的 IDEA 自动生成的工程文件被添加到了 Git 托管。
但我们是不希望提交这些文件的,没有意义,所以需要使用 .gitignore 文件来忽略这些文件,不让它们被 Git 托管。
如何编写 .gitignore 文件呢?
其实很简单,不用自己编写!我们在 IDEA 的 Settings => Plugins 中搜索 .ignore 插件并安装:
然后在项目根目录处选中右键,使用 .ignore 插件创建 .gitignore 文件:
.ignore 插件提供了很多默认的 .gitignore 模板,根据自己的项目类型和使用的开发工具进行选择,此处我们选择 Java 和 JetBrains 模板:
然后可以在项目根目录看到生成的 .gitignore 文件,模板已经包含了常用的 Java 项目忽略清单,比如编译后的文件、日志文件、压缩包等:
让我们再手动添加几个要忽略的目录和文件,比如打包生成的 target 目录:
但是,我们会发现,即使有些文件已经添加到了 .gitignore 文件中,在 IDEA 中显示的还是绿色(已被 Git 托管)状态。如下图:
这是因为这些文件已经被 Git 跟踪。而 .gitignore 文件仅影响未跟踪的文件,如果文件已经被 Git 跟踪,那么 .gitignore 文件对它们没有影响。
所以我们需要打开终端,在项目根目录下执行如下命令,取消 Git 跟踪:
执行效果如图:
可以看到文件变成了红色(未被 Git 托管)或黄色(被忽略)状态:
然后,让我们将 .gitignore 文件添加到 Git 暂存区,让它能够被 Git 管理。
项目根目录就初始化完成了,在项目根目录中新建一个 README.md 文件,用于介绍项目、记录自己的学习和开发过程等
# 2 静态文件生成
# 1、使用Hutool (opens new window)
优点:方便快捷,一行代码即可wanc
缺点:只能复制整个文件夹,不能随意复制
现在我们想复制目录下的所有文件,可以直接使用 Hutool 的 copy 方法,方法信息如下图,一定要格外注意输入参数的含义
先编写一个公开的静态方法 copyFilesByHutool,方法中的核心代码就一行,直接调用 Hutool 提供的 FileUtil.copy 方法,就能实现指定目录下所有文件的复制!
/**
* 拷贝文件(Hutool 实现,会将输入目录完整拷贝到输出目录下)
* @param inputPath 输入
* @param outputPath 输出
*/
public static void copyFileByHutool(String inputPath, String outputPath) {
FileUtil.copy(inputPath, outputPath, false);
}
2
3
4
5
6
7
8
然后编写一个 Main 方法来调用这个方法即可,完整复制我们之前准备好的 ACM 示例代码模板(建议使用相对路径)。 示例代码如下:
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();
// 输出路径:直接输出到项目的根目录
copyFileByHutool(inputPath, projectPath);
}
2
3
4
5
6
7
8
9
注意,上述代码中,我们通过 System.getProperty("user.dir") 获取到的路径是否是根路径,执行后就能看到项目目录下成功复制了完整的目录
# 2、递归遍历文件
优点:可随意复制任何想要的文件,不限制
缺点:需要自己实现,比价麻烦,可能还会有点小bug
# 文件操作 API
1)拷贝文件:
Files.copy(src.toPath(), dest.toPath(), optionList.toArray(new CopyOption[0]));
2)创建多级文件夹(哪怕中间有目录不存在):
File dest;
dest.mkdirs()
2
3)判断是否为目录:
File dest;
dest.isDirectory()
2
4)文件是否存在:
File dest;
dest.exists()
2
掌握这些 API,就能完成检测目录、创建目录、复制文件的操作了。
# 示例代码
递归算法的实现还是有一定复杂度的。核心思路就是先在目标位置创建和源项目相同的目录,然后依次遍历源目录下的所有子文件并复制;如果子文件又是一个目录,则再遍历子文件下的所有 “孙” 文件,如此循环往复。
/**
* 递归拷贝文件(递归实现,会将输入目录完整拷贝到输出目录下)
* @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("文件复制失败");
}
}
/**
* 文件 A => 目录 B,则文件 A 放在目录 B 下
* 文件 A => 文件 B,则文件 A 覆盖文件 B
* 目录 A => 目录 B,则目录 A 放在目录 B 下
* <p>
* 核心思路:先创建目录,然后遍历目录内的文件,依次复制
* @param inputFile 输入文件
* @param outputFile 输出文件
*/
private static void copyFileByRecursive(File inputFile, File outputFile) throws IOException {
// 判断inputFile是否为目录
if (inputFile.isDirectory()) {
// 创建outputFile的子目录
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.toPath().getFileName());
Files.copy(inputFile.toPath(), destPath, StandardCopyOption.REPLACE_EXISTING);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 3 动态文件代码生成
核心步骤: 1 定义数据模型 2 编写动态模板 3 组合生成 4 完善优化
# 1、定义数据模型
针对上述需求,我们在 com.yupi.model 包下新建一个模板配置对象,用来接收要传递给模板的参数。 注意要根据替换需求选择参数的类型,比如可选生成用 boolean、字符串替换用 String,示例代码如下:
/**
* 动态模版配置
*/
@Data
public class MainTemplateConfig {
/**
* 是否生成循环
*/
private boolean loop;
/**
* 作者注释
*/
private String author;
/**
* 输出信息
*/
private String outputText;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 2、编写动态模板
在 resources/templates 目录下新建 FTL 模板文件 MainTemplate.ftl(模板和上面定义的数据模型名称保持一致)。制作模板的方法很简单:先复制原始代码,再挖坑。 完整动态模板代码如下:
/**
* 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();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
其中,我们使用插值表达式 ${author} 接受作者名称,使用 <#if loop> ... </#if> 分支来控制是否生成循环代码,使用 ${outputText} 控制输出信息。
# 3、组合生成
在 Main 方法中编写生成逻辑,依次完成:创建 Configuration 对象、模板对象、创建数据模型、指定输出路径、执行生成。
public class DynamicGenerator {
public static void main(String[] args) throws IOException, TemplateException, freemarker.template.TemplateException {
String projectPath = System.getProperty("user.dir");
String inputPath = projectPath + File.separator +"/src/main/resources/templates/MainTemplate.ftl";
String outputPath = projectPath + File.separator + "src/main/resources/templates/";
MainTemplateConfig mainTemplateConfig = new MainTemplateConfig();
mainTemplateConfig.setAuthor("liyao");
mainTemplateConfig.setLoop(false);
mainTemplateConfig.setOutputText("总和: ");
doGenerate(inputPath, outputPath, mainTemplateConfig);
}
public static void doGenerate(String inputPath, String outputPath, Object model) throws IOException, TemplateException, freemarker.template.TemplateException {
// new 出 Configuration 对象,参数为 FreeMarker 版本号
Configuration configuration = new Configuration(Configuration.VERSION_2_3_32);
// 指定模板文件所在的路径
configuration.setDirectoryForTemplateLoading(new File(inputPath).getParentFile());
// 设置模板文件使用的字符集
configuration.setDefaultEncoding("utf-8");
// 创建模板对象,加载指定模板
Template template = configuration.getTemplate("MainTemplate.ftl");
// 创建数据模型
MainTemplateConfig mainTemplateConfig = new MainTemplateConfig();
mainTemplateConfig.setAuthor("yupi");
// 不使用循环
mainTemplateConfig.setLoop(false);
mainTemplateConfig.setOutputText("求和结果:");
// 生成
Writer out =
new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(new File(outputPath + "MainTemplate.java").toPath()),
StandardCharsets.UTF_8));
template.process(model, out);
// 生成文件后别忘了关闭哦
out.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 4、完善优化
经过测试发现,如果数据模型的字符串变量不设置任何值,那么会报如下错误
所以建议给所有字符串指定一个默认值,这里有两种方法:
1)方法 1,直接给 POJO 设置默认值
private String outputText = "sum = ";
2)方法 2,使用 FreeMarker 的默认值操作符
System.out.println("${outputText!'sum = '}" + sum);
# 4 动静结合 - 生成完整代码
编写一个类,组合调用这两个生成器,先复制静态文件、再动态生成文件来覆盖即可。
public class MainGenerator {
public static void doGenerate(Object model) throws TemplateException, IOException {
String projectPath = System.getProperty("user.dir"); // 获取当前目录的路径
File parentFile = new File(projectPath).getParentFile(); // 获取当前目录上一级目录的路径
//生成静态文件
String inputStaticPath = new File(parentFile, "yuzi-generator-demo-projects/").getAbsolutePath();
String outputStaticPath = projectPath + File.separator + "acm-template/";
StaticGenerator.copyFilesByRecursive(inputStaticPath, outputStaticPath);
//生成动态文件
String inputPath = projectPath + File.separator +"/src/main/resources/templates/MainTemplate.ftl";
String outputPath = projectPath + File.separator + "src/main/resources/templates/";
DynamicGenerator.doGenerate(inputPath, outputPath, model);
}
/**
* 主程序入口
*
* @param args 命令行参数
* @throws TemplateException 模板异常
* @throws IOException 输入输出异常
*/
public static void main(String[] args) throws TemplateException, IOException {
// 创建MainTemplateConfig对象
MainTemplateConfig mainTemplateConfig = new MainTemplateConfig();
// 设置作者为"liyao"
mainTemplateConfig.setAuthor("liyao");
// 设置循环为false
mainTemplateConfig.setLoop(false);
// 设置输出文本为"总和: "
mainTemplateConfig.setOutputText("总和: ");
// 调用doGenerate方法生成模板
doGenerate(mainTemplateConfig);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
- 01
- element-plus多文件手动上传 原创11-03
- 02
- TrueLicense 创建及安装证书 原创10-25
- 03
- 手动修改迅捷配置 原创09-03
- 04
- 安装 acme.sh 原创08-29
- 05
- zabbix部署 原创08-20