模版引擎技术

143 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

模版引擎是一种基于模板和要改变的数据, 并用来生成输出文本的通用工具。

我们可以基于规范性,提取同类代码的公共内容进行抽象,而将可变的内容作为参数输入,通过程序设计自动化地将动态的参数内容适宜地嵌入到模版中,进而形成文件输出。常见的比如: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函数,即可在期望的位置生成对应实体代码。