使用Velocity模版引擎编写自研代码生成器

前言

本篇文章主要讲解通过Velocity模版引擎 自研开发一个代码生成器 框架,可根据数据库已有的表结构生成基于MVC架构的Java代码,也可个人需求随时添加或修改要生成的代码内容,非常方便,并提供了一些设计模式使用的范例,希望对读者有所帮助。

完整版代码已上传至Github:totoro-parent

一、Velocity模版引擎介绍

Velocity模版引擎的具体使用可以参考这篇文章。Velocity模版引擎介绍,本篇文章只简单介绍一下最基础的使用。

(一)基础配置

  1. 在Maven项目pom.xml文件中添加依赖:
xml 复制代码
<!--   生成器默认版本依赖  -->
<dependency>
    <groupId>org.apache.velocity</groupId>
    <artifactId>velocity-engine-core</artifactId>
    <version>2.3</version>
</dependency>

<!-- guava工具类包 -->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>
  1. 添加代码生成执行类:
java 复制代码
package org.totoro.generator.processor;

import com.google.common.io.Files;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.Velocity;

import java.io.File;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Properties;

/**
 * Velocity模版引擎执行器
 * @author ChangLF 2023/07/21
 */
@Slf4j
public class VelocityProcessor {

    static {
        // 初始化配置,默认加载classpath下模版文件
        Properties prop = new Properties();
        prop.put("resource.loader.file.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
        Velocity.init(prop);
    }

    /**
     * 是否文件覆盖,默认为true
     */
    private static boolean FILE_OVERRIDE = true;

    /**
     * Velocity替换模版
     * @param velocityContext 替换的内容
     * @param templateName classpath下模版文件名称,例:template/Entity.java.vm
     * @param pathname 要生成的文件全路径名称
     * @author ChangLF 2023/7/24 08:37
     **/
     public static void process(VelocityContext velocityContext, String templateName, String pathname) {
        if (Objects.isNull(velocityContext) || StringUtils.isBlank(templateName) || StringUtils.isBlank(pathname)) {
            return;
        }

        StringWriter sw = new StringWriter();
        // 执行模版内容与velocityContext变量上下文替换
        Template template = Velocity.getTemplate(templateName, "UTF-8");
        template.merge(velocityContext, sw);
        File file = new File(pathname);
        // 创建父级文件夹
        file.getParentFile().mkdirs();
        if (file.exists() && !FILE_OVERRIDE) {
            log.warn("{}文件已存在,跳过生成", pathname);
            return;
        }
        try {
            Files.write(sw.toString().getBytes(StandardCharsets.UTF_8), file);
        } catch (IOException e) {
            log.warn("{}生成失败", pathname, e);
        }
    }

    public static void setFileOverride(boolean fileOverride) {
        FILE_OVERRIDE = fileOverride;
    }
}

上述代码主要提供了一个方法,可以传入VelocityContext变量替换上下文,templateName模板的文件路径,pathname为要生成的文件全路径名,若要生成在当前项目中,可指定为

arduino 复制代码
System.getProperty("user.dir") + "/src/main/java" + packageName + className

之后Velocity会获取到模版内容,然后通过变量替换将模版中的内容生成为新的代码内容,并生成到指定的文件pathname路径下面。

(二)变量替换

1. 变量引用

语法 描述
${变量名} 若上下文中没有对应的变量,则输出字符串"${变量名}"
${变量名.属性} 若上下文中没有对应的变量,则输出字符串"${变量名.属性}"

2. 流程控制

指令 语法 描述
#set #set($变量 = 值) 在页面中声明定义变量
#if/#elseif/#else 下面演示 进行逻辑判断
#foreach 下面演示 遍历循环数组或者集合

3. 范例

模板文件,文件名为template/Entity.java.vm,将此文件放在classpath/template路径下

swift 复制代码
public class ${entityName} {

#foreach ($column in $columns)
#if($column.columnName == $pk.columnName)
    /**
     * $column.columnComment
     */
    @TableId("$column.columnName")
    private $column.attrType $column.attrName;

#elseif($column.columnName.equalsIgnoreCase($logicDeleteColumn))
    /**
     * $column.columnComment
     */
    @TableLogic
    @TableField("$column.formatColumnName")
    private Boolean $logicDeleteProperty;

#else
    /**
     * $column.columnComment
     */
    @TableField("$column.formatColumnName")
    private $column.attrType $column.attrName;

#end#end

#set($index1 = $columns.size() + 1)

    /**
     * 创建人
     */
    @Size(min = $index1)
    private String createUser;

}

VelocityContext需传入的内容:

ini 复制代码
VelocityContext context = new VelocityContext();
context.put("tableName", tableDTO.getTableName());
String className = tableDTO.getClassName();
context.put("className", className);
String attrName = tableDTO.getAttrName();
context.put("attrName", attrName);
context.put("tableComment", tableDTO.getTableComment());
context.put("author", baseConfig.getAuthor());
context.put("date", baseConfig.getGeneratorDate());

List<ColumnDTO> columnDTOList = new ArrayList();
context.put("columns", columnDTOList);

VelocityContext内维护了一个hashMap,可将变量名和值以Key-Value方式存储。

通过以上内容,我们知道了Velocity的功能主要提供了一个变量替换的功能,并支持对象的引用,foreach, if else流程控制等功能,我们可以编写自己的模版,进行变量替换即可生成想要的代码。

二、获取数据库表以及字段信息

本文主要对MySQL数据库进行分析,其他数据库可自行研究。

在MySQL中information_schema数据库主要保存了数据库的表信息以及字段信息,我们可以通过下面的SQL获取${tableNameQuery}中传入的表以及字段信息,如字段名,字段类型,字段默认值,是否可为null等等。

首先添加MySQL驱动。

xml 复制代码
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.32</version>
</dependency>

(一)获取表结构information_schema.sql,放在classpath下。

csharp 复制代码
select t1.table_name,
       t2.table_comment,
       t1.column_name,
       t1.data_type,
       t1.column_type,
       t1.column_comment,
       t1.column_key,
       t1.is_nullable,
       t1.column_default
from information_schema.columns t1
inner join information_schema.tables t2
where t2.table_schema = t1.table_schema
  and t2.table_name = t1.table_name
  and t1.table_schema = (SELECT database())
  and ${tableNameQuery}
order by t1.table_name, t1.ordinal_position;

以下方法为读取上述Sql文件,并替换要生成的表名信息,通过JDBC连接执行替换变量后的SQL,在返回值中获取表信息和字段信息, 并存储在Map<TableDTO, List< ColumnDTO>>中。

vbscript 复制代码
/**
 * 获取数据库表以及字段信息
 * @param baseConfig 数据库配置
 * @author ChangLF 2023/7/21 11:36
 * @return java.util.Map<org.totoro.generator.javabean.dto.TableDTO,java.util.List<org.totoro.generator.javabean.dto.ColumnDTO>>
 **/
public static Map<TableDTO, List<ColumnDTO>> getTableColumn(BaseConfig baseConfig) {
    try {
        // 拼接sql
        String sql = Resources.toString(Resources.getResource("information_schema.sql"), StandardCharsets.UTF_8);
        Set<String> tables = baseConfig.getTables();
        // 若为空查询所有表,若不为空则拼接sql
        if (!CollectionUtils.isEmpty(tables)) {
            StringJoiner tableConcat = new StringJoiner(",", "t1.table_name in (", ")");
            tables.stream().map(t -> "'" + t + "'").forEach(tableConcat::add);
            sql = sql.replace("${tableNameQuery}", tableConcat.toString());
        } else {
            sql = sql.replace("${tableNameQuery}", "true");
        }
        // 加载驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 获取数据库连接,try-with-resource关闭资源
        try (Connection conn = DriverManager.getConnection(baseConfig.getJdbcUrl(), baseConfig.getUserName(), baseConfig.getPassword());
             Statement statement = conn.createStatement();
             ResultSet resultSet = statement.executeQuery(sql)) {
            Map<TableDTO, List<ColumnDTO>> tableDTOListMap = new HashMap<>();
            while (resultSet.next()) {
                TableDTO tableDTO = new TableDTO()
                        .setTableName(resultSet.getString("table_name"))
                        .setTableComment(resultSet.getString("table_comment"));
                List<ColumnDTO> columnDTOList = tableDTOListMap.computeIfAbsent(tableDTO, k -> new ArrayList<>());
                ColumnDTO columnDTO = new ColumnDTO()
                        .setColumnName(resultSet.getString("column_name"))
                        .setDataType(resultSet.getString("data_type"))
                        .setColumnType(resultSet.getString("column_type"))
                        .setColumnComment(resultSet.getString("column_comment"))
                        .setColumnKey(resultSet.getString("column_key"))
                        .setIsNullable(resultSet.getString("is_nullable"))
                        .setColumnDefault(resultSet.getString("column_default"));
                columnDTOList.add(columnDTO);
            }
            return tableDTOListMap;
        }
    } catch (Exception e) {
        throw new RuntimeException("获取数据库信息失败", e);
    }
}

至此,我们就拿到了我们想要的数据库表信息。通过创建代码模板即可实现变量替换生成代码。

三、生成策略类、配置类、启动类

通过上述的代码我们已经能够生成需要的代码类了,为了让我们的代码生成器框架看起来更加高级一点,我们可以合理的使用面向对象三大特性封装、继承、多态 ,通过使用策略模式,模块方法模式,抽象工厂模式等将代码生成,可更加灵活生成代码。

(一)生成策略接口

typescript 复制代码
public interface GeneratorStrategy {

    /**
     * 获取替换模板上下文
     *
     * @param baseConfig         基础配置
     * @param tableDTOListEntry  表信息
     * @param generatorConfigArr 其他各种配置
     * @return org.apache.velocity.VelocityContext 替换模板的上下文
     * @author ChangLF 2023/7/25 08:54
     **/
    VelocityContext getVelocityContext(BaseConfig baseConfig, Map.Entry<TableDTO, List<ColumnDTO>> tableDTOListEntry,
                                       GeneratorConfig... generatorConfigArr);

    /**
     * 获取模板所在文件的路径名称,需在classpath下
     *
     * @return java.lang.String
     * @author ChangLF 2023/7/25 08:57
     **/
    String getTemplate();

    /**
     * 获取要生成的文件全路径名称
     *
     * @param packageConfig 基础配置
     * @param className     生成的文件类名
     * @return java.lang.String
     * @author ChangLF 2023/7/25 08:58
     **/
    String getPathname(PackageConfig packageConfig, String className);

    /**
     * 生成代码执行器,调用
     *
     * @param baseConfig         基础配置
     * @param tableDTOListEntry  表信息
     * @param generatorConfigArr 生成的配置文件
     * @author ChangLF 2023/7/25 08:59
     **/
    default void execute(BaseConfig baseConfig, Map.Entry<TableDTO, List<ColumnDTO>> tableDTOListEntry,
                         GeneratorConfig... generatorConfigArr) {
        VelocityProcessor.process(this.getVelocityContext(baseConfig, tableDTOListEntry, generatorConfigArr),
                this.getTemplate(), this.getPathname(baseConfig.getPackageConfig(), tableDTOListEntry.getKey().getClassName()));
    }
}    

我们通过编写一个通用代码生成接口,提供三个抽象方法供下层实现类去实现,用来获取变量替换的上下文VelocityContext,模版的文件路径名getTemplate(), 要生成的文件全路径名称getPathname() ,即可获取代码生成所需的全部信息,并提供一个默认方法实现execute(),将上述信息统一传递给代码生成的执行器中,即可生成代码。

totoro-parent中已提供了以下实现类,可生成对应的代码。

  • ControllerGenStrategy
  • MapperGenStrategy、MapperXmlGenStrategy
  • EntityGenStrategy、PageReqDTOGenStrategy、ReqDTOGenStrategy、VOGenStrategy
  • ServiceGenStrategy、ServiceImplGenStrategy

(二)生产策略工厂接口

1. 编写用于生产策略的抽象工厂接口。
php 复制代码
public interface GeneratorConfigFactory {

    /**
     * 获取生成策略类
     * @author ChangLF 2024/1/9 15:29
     * @return java.util.List<java.lang.Class<? extends org.totoro.generator.strategy.GeneratorStrategy>>
     **/
    List<Class<? extends GeneratorStrategy>> getStrategy();
}

getStrategy()方法可返回一个Class对象的集合,要求返回的Class原始对象必须继承上述的GeneratorStrategy策略接口,具体的工厂类可以实现返回不同的生成策略。

2. 具体实现类范例
kotlin 复制代码
@Builder
public class MapperConfigFactory implements GeneratorConfigFactory {

    @Override
    public List<Class<? extends GeneratorStrategy>> getStrategy() {
        return Lists.newArrayList(MapperGenStrategy.class, MapperXmlGenStrategy.class);
    }
}


@Builder
public class ServiceConfigFactory implements GeneratorConfigFactory {

    @Override
    public List<Class<? extends GeneratorStrategy>> getStrategy() {
        return Lists.newArrayList(ServiceGenStrategy.class, ServiceImplGenStrategy.class);
    }
}

MapperConfigFactory工厂类实现了getStrategy()方法,返回了MapperGenStrategy.class, MapperXmlGenStrategy.class策略,ServiceConfigFactory工厂类返回了ServiceGenStrategy.class, ServiceImplGenStrategy.class实现类。当我们使用这些工厂对象时即可获取不同数量和种类的生成策略。

(三)基础配置类

生成代码前,我们还需要一些基础配置信息供用户填写

scss 复制代码
@Data
@Builder
public class BaseConfig {

    /**
     * 数据库连接
     */
    @NotBlank
    private String jdbcUrl;

    /**
     * 数据库用户名
     */
    @NotBlank
    private String userName;

    /**
     * 数据库密码
     */
    @NotBlank
    private String password;

    /**
     * 指定table,非必填,未填写时代表schema下所有的table
     */
    private Set<String> tables;

    /**
     * 为生成的代码指定作者
     */
    @NotBlank
    private String author;

    /**
     * 包路径配置
     */
    @Valid
    @NotNull
    private PackageConfig packageConfig;

    /**
     * 是否文件覆盖,默认为true
     */
    @NotNull
    private Boolean fileOverride;

    /**
     * 生成注释日期,可指定,默认为当前时间yyyy/mm/dd格式
     */
    private String generatorDate;

}


@Data
@Builder
public class PackageConfig {

    /**
     * 输出父文件路径
     */
    @NotBlank
    private String javaFileDir;

    /**
     * 父包名
     */
    @NotBlank
    private String parentPackage;

    /**
     * Entity类的包名
     */
    @NotBlank
    private String entityPackage;

    /**
     * vo类的包名
     */
    @NotBlank
    private String voPackage;

    /**
     * reqDTO类的包名
     */
    @NotBlank
    private String reqDTOPackage;

    /**
     * mapper接口的包名
     */
    @NotBlank
    private String mapperPackage;

    /**
     * mapper.xml所在目录的绝对路径
     */
    @NotBlank
    private String mapperXmlDirectoryPath;

    /**
     * service接口的包名
     */
    @NotBlank
    private String servicePackage;

    /**
     * serviceImpl接口的包名
     */
    @NotBlank
    private String serviceImplPackage;

    /**
     * controller接口的包名
     */
    @NotBlank
    private String controllerPackage;

}

(四)启动类

这里我们编写一个代码生成的启动类,用于让用户配置数据库连接信息,包路径,要生成的策略类信息等等。

scss 复制代码
public class GeneratorCodeBoot {

    public static void main(String[] args) {
        // 要生成的表信息
        Set<String> tables = Sets.newHashSet("table1", "table2");
        // 当前项目路径
        String home = System.getProperty("user.dir");
        // 数据库连接配置,必须有
        BaseConfig baseConfig = BaseConfig.builder()
                .jdbcUrl("jdbc:mysql://127.0.0.1:3306/${your_database}?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai")
                .userName("${your_username}")
                .password("${your_password}")
                // 指定生成的表,非必填,为空代表该数据库下所有表
                .tables(tables)
                .author("ChangLF")
                // 若为false,文件已存在则不生成
                .fileOverride(true)
                .packageConfig(PackageConfig.builder()
                        // java文件输出的父文件路径, 直接输出到项目代码中,请注意文件覆盖问题
                        .javaFileDir(home + "/src/main/java")
                        // java文件输出的父文件路径, 输出到外部/generator文件夹下,需自行拷贝代码
//                        .javaFileDir(home + "/generator")
                        .parentPackage("org.totoro.demo")
                        .entityPackage("entity")
                        .reqDTOPackage("javabean.dto")
                        .voPackage("javabean.vo")
                        .mapperPackage("mapper")
                        .servicePackage("service")
                        .serviceImplPackage("serviceImpl")
                        .controllerPackage("controller")
                        // 独立于javaFileDir
                        .mapperXmlDirectoryPath(home + "/src/main/resources/mapper")
                        .build())
                .build();
        // entity配置,必须有
        EntityConfigFactory entityConfig = EntityConfigFactory.builder()
                .logicDeleteColumnName("is_delete")
                .logicDeletePropertyName("deleteFlag")
                .build();
        // mapper配置,必须有
        MapperConfigFactory mapperConfig = MapperConfigFactory.builder()
                .build();
        // 可没有,若没有则不生成对应代码
        ServiceConfigFactory serviceConfig = ServiceConfigFactory.builder()
                .build();
        // 可没有,若没有则不生成对应代码
        ControllerConfigFactory controllerConfig = ControllerConfigFactory.builder()
                .build();

        // 生成代码,不填写config则不生成对应的代码
        GeneratorProcessor.process(baseConfig, entityConfig, mapperConfig, serviceConfig, controllerConfig);
    }

}

GeneratorProcessor.process为代码生成的执行器,支持传入可变参数,可让用户传入不同数量的ConfigFactory,来生成对应的类。

scss 复制代码
@Slf4j
public class GeneratorProcessor {

    /**
     * 开始生成代码
     *
     * @param generatorConfigFactoryArr 核心配置
     * @author ChangLF 2023/7/21 11:37
     **/
    public static void process(BaseConfig baseConfig, GeneratorConfigFactory... generatorConfigFactoryArr) {
        log.info("开始生成代码");
        // 校验参数
        ValidatorUtils.validateBean(baseConfig);
        ValidatorUtils.validateBean(generatorConfigFactoryArr);
        if (StringUtils.isBlank(baseConfig.getGeneratorDate())) {
            baseConfig.setGeneratorDate(new SimpleDateFormat("yyyy/MM/dd").format(new Date()));
        }
        VelocityProcessor.setFileOverride(baseConfig.getFileOverride());

        // 获取表字段信息
        Map<TableDTO, List<ColumnDTO>> tableColumnMap = InformationSchemaProcessor.getTableColumn(baseConfig);
        // 装饰表参数
        InformationSchemaProcessor.decorate(tableColumnMap);

        // 根据配置获取代码生成策略
        List<GeneratorStrategy> strategyList = new ArrayList<>();
        Stream.of(generatorConfigFactoryArr).map(GeneratorConfigFactory::getStrategy)
                .forEach(list -> list.forEach(s -> strategyList.add(GeneratorSingletonFactory.doCreateBean(s))));

        // 按表顺序生成代码,若中间失败则前面生成的表仍成功
        for (Map.Entry<TableDTO, List<ColumnDTO>> tableDTOListEntry : tableColumnMap.entrySet()) {
            for (GeneratorStrategy generatorStrategy : strategyList) {
                generatorStrategy.execute(baseConfig, tableDTOListEntry, generatorConfigFactoryArr);
            }
        }
    }

}
css 复制代码
    List< GeneratorStrategy> strategyList = new ArrayList<>();
    Stream.of(generatorConfigFactoryArr).map(GeneratorConfigFactory::getStrategy)
            .forEach(list -> list.forEach(s -> strategyList.add(GeneratorSingletonFactory.doCreateBean(s))));

这两行代码用来获取生成的策略对象。GeneratorSingletonFactory.doCreateBean(s)方法为单例工厂模式,用来根据Class获取单例的对象。

(五)单例工厂模式生产对象

java 复制代码
@Slf4j
public final class GeneratorSingletonFactory {

    private static final Map<Class<?>, Object> OBJECT_MAP = new ConcurrentHashMap<>();

    private GeneratorSingletonFactory() {
        throw new UnsupportedOperationException("don't instance me");
    }

    /**
     * 通过Class获取单例对象,默认根据无参构造函数初始化
     * @param tClass class对象
     * @author ChangLF 2024/1/9 16:01
     * @return T
     **/
    public static <T> T getBean(Class<T> tClass) {
        Objects.requireNonNull(tClass, "class can't be null");
        Object obj = OBJECT_MAP.get(tClass);
        if (obj != null) {
            return (T) obj;
        }
        return doCreateBean(tClass);
    }

    /**
     * 创建单例对象
     * @param tClass class对象
     * @author ChangLF 2023/7/24 09:05
     * @return org.generator.strategy.GeneratorStrategy
     * @throws if {@link Class#getDeclaredConstructor} can't find, throw NoSuchMethodException
     **/
    private static <T> T doCreateBean(Class<T> tClass) {
        synchronized (tClass) {
            Object obj = OBJECT_MAP.get(tClass);
            if (obj != null) {
                return (T) obj;
            }
            try {
                Object newInstance = tClass.getDeclaredConstructor().newInstance();
                OBJECT_MAP.put(tClass, newInstance);
                return (T) newInstance;
            } catch (InstantiationException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
                log.warn("初始化实例{}失败", tClass, e);
                return null;
            }
        }
    }

}

至此,代码生成器的全部流程已完成。

执行过程为GeneratorCodeBoot#main(启动参数配置) ---> GeneratorProcessor#process(开始生成) -> InformationSchemaProcessor#getTableColumn(获取表及字段信息) -> GeneratorConfigFactory#getStrategy(获取生成策略) -> GeneratorStrategy#execute(遍历生成策略执行) -> VelocityProcessor#process(生成代码)

五、总结

本篇文章主要介绍了Velocity模版引擎的使用,MySQL数据库表字段信息的获取,代码生成器的设计模式,希望对读者有代码编写的启发,可自行编写一个自己的代码生成器。

完整版代码已上传至Github:totoro-parent,可Star支持下,感谢🙏
作者:龙猫帝

原文链接:juejin.cn/spost/73217...

版权所有,欢迎保留原文链接进行转载:)

相关推荐
P.H. Infinity12 分钟前
【RabbitMQ】04-发送者可靠性
java·rabbitmq·java-rabbitmq
生命几十年3万天16 分钟前
java的threadlocal为何内存泄漏
java
caridle28 分钟前
教程:使用 InterBase Express 访问数据库(五):TIBTransaction
java·数据库·express
^velpro^33 分钟前
数据库连接池的创建
java·开发语言·数据库
苹果醋337 分钟前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx
秋の花41 分钟前
【JAVA基础】Java集合基础
java·开发语言·windows
小松学前端44 分钟前
第六章 7.0 LinkList
java·开发语言·网络
Wx-bishekaifayuan1 小时前
django电商易购系统-计算机设计毕业源码61059
java·spring boot·spring·spring cloud·django·sqlite·guava
customer081 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源
全栈开发圈1 小时前
新书速览|Java网络爬虫精解与实践
java·开发语言·爬虫