前言
本篇文章主要讲解通过Velocity模版引擎 自研开发一个代码生成器 框架,可根据数据库已有的表结构生成基于MVC架构的Java代码,也可个人需求随时添加或修改要生成的代码内容,非常方便,并提供了一些设计模式使用的范例,希望对读者有所帮助。
完整版代码已上传至Github:totoro-parent
一、Velocity模版引擎介绍
Velocity模版引擎的具体使用可以参考这篇文章。Velocity模版引擎介绍,本篇文章只简单介绍一下最基础的使用。
(一)基础配置
- 在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>
- 添加代码生成执行类:
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支持下,感谢🙏
作者:龙猫帝版权所有,欢迎保留原文链接进行转载:)