一种基于数据库与渲染模板的代码生成器——模板渲染

续上篇

在前两篇文章《一种基于数据库+模板渲染的代码生成器——简介及数据库查询》以及《一种基于数据库+模板渲染的代码生成器——表结构暂存与类设计》中,我们介绍了如何通过mysql中的自有数据库information_schema查询数据库结构,以及通过另外构建数据表gen_tablegen_table_column,暂存数据结构,暂存表所生成的类名,属性等等,减少后续生成代码预览与频繁生成的数据库压力与代码复杂度。

本篇我们继续阐述代码生成器的后续逻辑——模板渲染。

几种Java中常见的模板渲染插件

字符串格式化工具

最简单的,我们知道Java自带的格式化输出:

1
System.out.printf("%f\n",pi);

一些常用的工具中也有一些非常好用的模板化字符串的方法。

以著名的开源库Hutool为例,其中的StrUtil中的函数用法如下:

1
2
String template = "{}爱{},就像老鼠爱大米";
String str = StrUtil.format(template, "我", "你"); //str -> 我爱你,就像老鼠爱大米

freemaker

这是笔者最常用的一个渲染引擎,功能非常强大,一方面可以使用它来渲染网页,邮件等显示页面,另一方面,也可以用它通过配置复杂的XML文档,导出为word等格式;当然,也可以使用其生成想要的源代码。

freemaker中的渲染使用示意图如下:

image-20220107171402344

上图是Freemaker官网上的一张示意图,在模板中设置${name}字段,在Java中通过对象或Map,设置其name值,通过freemaker即可生成指定文件。其模板文件大多格式为.ftl

一个简单的使用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**忽略try-catch等语句*/
Map<String, String> dataMap = new HashMap<>();
dataMap.put("name", "姓名");
configuration = new Configuration();
configuration.setDefaultEncoding("utf-8");
configuration.setClassForTemplateLoading(com.backstage.export.template.Template.class, "/模板存储路径");
Template t = configuration.getTemplate("模板文件.ftl");
// 输出文档路径及名称
File outFile = new File(fileName);
Writer out = null;
FileOutputStream fos = null;
fos = new FileOutputStream(outFile);
OutputStreamWriter oWriter = new OutputStreamWriter(fos, "UTF-8");
out = new BufferedWriter(oWriter);
t.process(dataMap, out);
out.close();
fos.close();

Apache Velocity

与Freemaker相似,使用上相比Freemaker来说,稍微复杂一些。

1
2
3
4
5
6
7
8
9
10
// 初始化:设置模板主路径,字符集,指定配置等
VelocityInitializer.initVelocity();
// context类似与freemaker中的map等数据对象,存储数据{"name": "落叶"}
VelocityContext context = VelocityUtils.prepareContext(data);
// 获取模板
Template tpl = Velocity.getTemplate("/模板路径/模板.file-suffix.vm", Constants.UTF8);
// 渲染
StringWriter sw = new StringWriter();
tpl.merge(context, sw);
System.out.println(sw.toString());

在我们学习的若依系统中,其使用的是Apache Velocity。

首先,查询gen_table表,将指定表的结构信息查询出来:

1
2
// 查询表信息: 
GenTable table = genTableMapper.selectGenTableById(tableId);

需要注意的是,上述查询,不仅查出来gen_table表,也包括gen_table_column表的数据,这源于GenTable对象设计为:

1
2
3
4
5
6
7
8
9
10
public class GenTable extends BaseEntity
{
// ...

/** 表列信息 */
@Valid
private List<GenTableColumn> columns;

// ...
}

然后准备模板渲染对象VelocityContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 设置模板变量信息
*
* @return 模板列表
*/
public static VelocityContext prepareContext(GenTable genTable)
{
VelocityContext velocityContext = new VelocityContext();
velocityContext.put("tplCategory", genTable.getTplCategory());
velocityContext.put("tableName", genTable.getTableName());
velocityContext.put("functionName", StringUtils.isNotEmpty(functionName) ? functionName : "【请填写功能名称】");
velocityContext.put("ClassName", genTable.getClassName());
velocityContext.put("className", StringUtils.uncapitalize(genTable.getClassName()));
velocityContext.put("moduleName", genTable.getModuleName());
// .....
velocityContext.put("columns", genTable.getColumns());
return velocityContext;
}

代码生成器的模板还是十分复杂的,我们选其中一个Domain的模板来看其中的一部分:

1
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
51
52
53
54
55
package ${packageName}.domain;

#foreach ($import in $importList)
import ${import};
#end
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.ruoyi.common.annotation.Excel;
#if($table.crud || $table.sub)
import com.ruoyi.common.core.domain.BaseEntity;
#elseif($table.tree)
import com.ruoyi.common.core.domain.TreeEntity;
#end

/**
* ${functionName}对象 ${tableName}
*
* @author ${author}
* @date ${datetime}
*/
#if($table.crud || $table.sub)
#set($Entity="BaseEntity")
#elseif($table.tree)
#set($Entity="TreeEntity")
#end
public class ${ClassName} extends ${Entity}
{
private static final long serialVersionUID = 1L;

#foreach ($column in $columns)
#if(!$table.isSuperColumn($column.javaField))
/** $column.columnComment */
#if($column.list)
#set($parentheseIndex=$column.columnComment.indexOf("("))
#if($parentheseIndex != -1)
#set($comment=$column.columnComment.substring(0, $parentheseIndex))
#else
#set($comment=$column.columnComment)
#end
#if($parentheseIndex != -1)
@Excel(name = "${comment}", readConverterExp = "$column.readConverterExp()")
#elseif($column.javaType == 'Date')
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "${comment}", width = 30, dateFormat = "yyyy-MM-dd")
#else
@Excel(name = "${comment}")
#end
#end
private $column.javaType $column.javaField;

#end
#end

}

输出:

1
2
3
4
5
// 渲染模板
StringWriter sw = new StringWriter();
Template tpl = Velocity.getTemplate(template, Constants.UTF8);
tpl.merge(context, sw);
dataMap.put(template, sw.toString());

总结

至此,我们就大概了解了一个代码生成器是如何将表结构列信息等一步一步转换为可以使用的源代码的过程。