本文已参与「新人创作礼」活动,一起开启掘金创作之路。
模版引擎是一种基于模板和要改变的数据, 并用来生成输出文本的通用工具。
我们可以基于规范性,提取同类代码的公共内容进行抽象,而将可变的内容作为参数输入,通过程序设计自动化地将动态的参数内容适宜地嵌入到模版中,进而形成文件输出。常见的比如:
mybatis-plus-generator就是采用模版引擎技术实现的根据表结构一次性生成适用MVC的通用接口。模版引擎的实现技术有很多:
freemarker/thymeleaf/velocity/jsp/beetl,本文举例采用的是freemarker模版引擎,低代码平台使用的也正是这个模版引擎。
一、场景引入与示例代码
1.1 场景引入
举个场景,比如说对于一张新建的表,基于诸如Mybatis这样的ORM框架操作的时候需要做好映射关系,所以在项目中要再创建对应的实体,以及实体对完暴露的数据访问接口,这种操作在每新增了表的时候都需要重复进行。
1.2 示例代码
示例实体代码:
package cloud.yiwenup.sample.freemarker.template.entity;
/**
* 文件描述
*
* @Project yiwenup-sample
* @Package cloud.yiwenup.sample.freemarker.template.entity
* @Author yiwenup
* @Date 2022-07-14 16:12:01
* @Description 用户实体
*/
public class UserEntity {
private String id;
private String username;
private String password;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
二、模版抽象
一般而言,实体类中的属性都会是与数据库表列一一对应的,实体类的名称也是和数据库表有映射关系的(一般是驼峰和下划线映射),因此可以抽象实体类模块如下:
模版中使用
${}进行变量引用,这些变量都是可以配置的,通过外部对模版的使用,传入这些参数变量,以此来达到一份模版灵活产出多份代码文件还可以通过声明的方式,从外部静态引入额外的功能,比如自定义静态工具方法以及常量,在模版中使用
package ${basePackage}.entity;
/**
* 文件描述
*
* @Project yiwenup-sample
* @Package ${basePackage}.entity
* @Author yiwenup
* @Date ${.now}
* @Description ${tableComment}
*/
<#assign StringUtil=statics['cloud.yiwenup.sample.freemarker.template.util.StringUtil']>
public class ${className}Entity {
<#list props as prop>
/**
* ${prop.remark}
*/
private ${prop.type} ${prop.field};
</#list>
<#list props as prop>
<#assign fieldUppCase=StringUtil.firstWord2UppCase(prop.field)>
/**
* 获取${prop.remark}
*/
public ${prop.type} get${fieldUppCase}() {
return ${prop.field};
}
/**
* 设置${prop.remark}
*/
public void set${fieldUppCase}(${prop.type} ${prop.field}) {
this.${prop.field} = ${prop.field};
}
</#list>
}
三、模板使用
接下来,我们通过编写程序,使用上述模板,构造出最开始的示例代码。需要注意的是,下面的代码仅作示例,动态的参数都是写死的,完全可以在配置文件、数据库或者接口暴露参数的形式动态配置;并且抽象的模型也是仅作示例,模型可以拓展更多的描述性参数。
模型代码:
public static class ORM {
private String type;
private String field;
private String remark;
// 省略get/set方法
}
文件生成代码:
public static void main(String[] args) throws Exception {
// 初始化模版配置信息
Configuration configuration = new Configuration(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS);
configuration.setDefaultEncoding(StandardCharsets.UTF_8.name());
configuration.setTemplateLoader(new ClassTemplateLoader());
// 设置使用何种模版文件
Template template = configuration.getTemplate("templates/Entity.ftl");
// 动态参数放在一个 map 数据结构中,之后的模板渲染过程就是取出这些参数做动态渲染
String basePackage = "cloud.yiwenup.sample.freemarker.template";
String className = "User1";
List<ORM> props = new ArrayList<>();
props.add(new ORM("String", "id", "用户ID"));
props.add(new ORM("String", "username", "用户名"));
props.add(new ORM("String", "password", "密码"));
Map<String, Object> map = new HashMap<>();
// 这一步很重要,通过以下代码的配置,才可以在模板文件中使用外部自定义的静态工具类
map.put("statics", ((BeansWrapper)configuration.getObjectWrapper()).getStaticModels());
map.put("basePackage", basePackage);
map.put("tableComment", "用户信息表");
map.put("className", className);
map.put("props", props);
// 渲染模板文件,并将文件输出到指定位置
try(OutputStream entityOut = Files.newOutputStream(new File(getEntityFilePath(basePackage), className + "Entity.java").toPath())) {
template.process(map, new OutputStreamWriter(entityOut));
}
}
/**
* 辅助工具,用于指定生成文件的位置
*/
public static File getEntityFilePath(String basePackage) {
File file = new File("/Users/yiwenup/resource/yiwenup-sample/yiwenup-sample-freemarker-template");
List<String> files = new ArrayList<>(Arrays.asList("src", "main", "java"));
files.addAll(Arrays.asList(basePackage.split("\\.")));
files.add("entity");
return new File(file, String.join(File.separator, files));
}
自定义辅助工具类:
public class StringUtil {
public static String firstWord2UppCase(String arg) {
if (arg == null || arg.length() == 0) {
return "";
}
if (arg.length() == 1) {
return arg.toUpperCase();
}
return arg.substring(0, 1).toUpperCase() + arg.substring(1);
}
}
至此,只要运行main函数,即可在期望的位置生成对应实体代码。