续上篇

在前两篇文章《一种基于数据库+模板渲染的代码生成器——简介及数据库查询》以及《一种基于数据库+模板渲染的代码生成器——表结构暂存与类设计》中,我们介绍了如何通过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());

总结

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

接上篇

在上一篇文章《一种基于数据库+模板渲染的代码生成器——简介及数据库查询》中,我们介绍了代码生成器的基本概念以及常见的几种代码生成器,并阐述了通过mysql数据库中的information_schema库,查询数据库中的表结构、列结构等信息。

本篇文章将继续阐述,查询出表结构与列结构后,如何将其逐步映射为编程语言Java中的类及对象。

表与列结构暂存

在若依系统中,其实现代码生成分为两个步骤,第一步被称为“导入表”,即将所要生成代码的表的结构信息存储到到某几个指定表gen_tablegen_table_column中。

其注释分别为:

表名 注释
gen_table 代码生成业务表
gen_table_column 代码生成业务表字段

先来看一下gen_table存储了哪些信息:

执行以下SQL:

1
2
3
4
5
6
7
8
9
SELECT
COLUMN_NAME,
COLUMN_COMMENT,
DATA_TYPE
FROM
information_schema.`COLUMNS`
WHERE
TABLE_NAME = 'gen_table'
AND TABLE_SCHEMA = 'ry-vue'

查询结果如下图:

image-20220106165724085

再来看gen_table_column

1
2
3
4
5
6
7
8
9
SELECT
COLUMN_NAME,
COLUMN_COMMENT,
DATA_TYPE
FROM
information_schema.`COLUMNS`
WHERE
TABLE_NAME = 'gen_table_column'
AND TABLE_SCHEMA = 'ry-vue'

执行结果如下:

image-20220106165833766

在Java中,分别按照表gen_tablegen_table_column构建两个实体对象,即Entity,以便后续通过mybatis等对其进行保存等相关操作。

我们截图部分源码代码(其所属位置位于若依项目:ruoyi-generator/src/main/java/com/ruoyi/generator/domain路径下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class GenTable extends BaseEntity
{
private static final long serialVersionUID = 1L;

/** 编号 */
private Long tableId;

/** 表名称 */
@NotBlank(message = "表名称不能为空")
private String tableName;

/** 表描述 */
@NotBlank(message = "表描述不能为空")
private String tableComment;

/** 关联父表的表名 */
private String subTableName;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class GenTableColumn extends BaseEntity
{
private static final long serialVersionUID = 1L;

/** 编号 */
private Long columnId;

/** 归属表编号 */
private Long tableId;

/** 列名称 */
private String columnName;

/** 列描述 */
private String columnComment;
}

表名列名与类名字段名的转换

在获取到表结构与列结构数据后,其名称大多数是不符合类名创建规则的,需要做一个额外的转换,如下图:

表名与列名 Java类名与字段名
表名:sys_my_user MyUser
列名:user_name userName

对于列名,将其转换为驼峰命名法;对于表名,一是注意需要去掉某些指定前缀,二是将剩余部分转换为首字母大写的驼峰。

对于转换为驼峰的方式,我在若依系统源码中发现有两个不同的实现方式,供大家参考:

第一种,从字符串开头,通过大写标识依次拼接字符:

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

/*com.ruoyi.common.utils.StringUtils*/

private static final char SEPARATOR = '_';
/**
* 转换为驼峰命名法
*/
public static String toCamelCase(String s)
{
if (s == null)
{
return null;
}
s = s.toLowerCase();
StringBuilder sb = new StringBuilder(s.length());
boolean upperCase = false;
for (int i = 0; i < s.length(); i++)
{
char c = s.charAt(i);
if (c == SEPARATOR)
{
upperCase = true;
}
else if (upperCase)
{
sb.append(Character.toUpperCase(c));
upperCase = false;
}
else
{
sb.append(c);
}
}
return sb.toString();
}

第二种,按照下划线拆分原始字符串后,依次首字母大小,然后拼接:

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
/*com.ruoyi.common.utils.StringUtils*/

public static String convertToCamelCase(String name)
{
StringBuilder result = new StringBuilder();
// 快速检查
if (name == null || name.isEmpty())
{
// 没必要转换
return "";
}
else if (!name.contains("_"))
{
// 不含下划线,仅将首字母大写
return name.substring(0, 1).toUpperCase() + name.substring(1);
}
// 用下划线将原始字符串分割
String[] camels = name.split("_");
for (String camel : camels)
{
// 跳过原始字符串中开头、结尾的下换线或双重下划线
if (camel.isEmpty())
{
continue;
}
// 首字母大写
result.append(camel.substring(0, 1).toUpperCase());
result.append(camel.substring(1).toLowerCase());
}
return result.toString();
}

数据类型的转换

即数据库中定义的数据类型包括:varchar, int, datetime, decimal等等,这些都需要一一映射为Java中的相关对象,包括String, Integer, Date, BigDecimal等等。

暂存结果:

比如我们的表my_user,结构信息如下:

image-20220106172732427

经过转换,存储在gen_table以及gen_table_column中的数据为:

image-20220106172907984

image-20220106172927791

总结

我们便可不需要通过information_schema数据库查询信息,而仅需要通过这两个gen_表数据生成对应的类信息。通过其存储数据,我们也可以看到,表中存储了构建对象属性的类型、名称,这也便于后续直接生成对象,而不需要再次将其转换,减轻了一步操作的系统压力。

通过使用表“缓存”数据结构,的确是十分高明的方法,值得学习!

什么要编写代码生成器

在《程序员修炼之道-从小工到专家》第二章节部分,提到一种强加的重复,如项目标准要求含有重复信息的文档,或重复代码中信息的文档,书中建议通过信息存放在一处,通过代码生成器等方式生成其他重复数据,以遵守DRY原则;在这本书的第三站基本工具一节,作者提倡“编写能编写代码的代码”:Write Code That Writes Code。按照作者的划分,代码生成器分为被动代码生成器与主动代码生成器。被动代码生成器即参数化模板,例如创建源文件、编程语言之间的一次性转换、生成查找表及其他在运行时计算很昂贵的资源(如三角函数值表)。主动代码生成器可以理解为某项知识的一种表现形式,每次需要其结果时执行,如根据数据库scheme生成代码源码。

DRY原则:Don’t Repeat Yourself。不要重复你自己

区分被动/主动的问题并不重要,重要的是提醒自己可以通过代码生成器的减少重复度高的工作,将这些工作自动化、标准化,节省有限的时间,提高工作效率。

同样的,在《程序员职业规划之道》一书中,其作者也推荐使用编写代码生成器的方式,尽可能的将工作自动化。

代码生成器的实现思路

在笔者有限的工作年限内,最常见的代码生成器有:

  1. 根据数据库表结构生成对应的语言源代码文件;

    工作后使用的第一个Web框架是Jfinal,其中最常用的代码生成器为com.jfinal.plugin.activerecord.generator.Generator,根据数据库生成程序中使用的Model低层对象,包括ModelBaseModel

    image-20220104183120681

  2. 根据样式组件生成前端源码文件

    如下图,该网站据说是可以实现拖动组件后将页面转换为html代码文件。

    image-20220104183425766

一种普遍的基于数据库与渲染模板的代码生成器实现思路

这一思路是个人在学习一个开源框架——若依管理系统时学到的,其系统中的“系统工具——代码生成”可以根据数据库表、表结构生成一整套的前后端代码,包括后端实体类,Mapper类,Service接口与实现,增删改查相关接口以及相应的mybatis-XML文件,前端的样式页面以及接口请求文件等。在项目有快速开发的时候,这一套代码生成器可以极大的减轻工作量。

下图为若依系统代码生成器代码预览截图。

image-20220104184127264

以Java为例,生成器实现步骤大致分为以下几步:

  1. 通过SQL查询获取数据库表结构,列信息等;
  2. 将数据库结构映射为编程语言对象,本例即为Java中的对象;
  3. 配置各个功能版块的模板文件,比如可以使用freemaker,Apache Velocity等模板引擎;
  4. 渲染模板,依次输出文件

本篇暂且介绍第一步内容。

那么,见惯了CRUD的执行语句的你们,知道如何查询数据库中的表结构信息吗?

一个特殊的数据库:information_schema

通过查询mysql数据库中的表信息涉及到一个特殊的数据库:information_schema,我们可以通过它来获取所连接的数据库的基础信息。

information_schema中存储了一些什么信息呢?同样的,我们也需要通过information_schema来获取:

执行以下SQL:

1
SELECT * FROM information_schema.`TABLES`

比如在笔者本地数据库,查询结果如下:

image-20220105182801428

可以看到,其中罗列了本地数据库连接中的所有数据库的表信息,包括数据库名称TABLE_SCHEMA,表名TABLE_NAME,数据库引擎ENGINE,表的数据库行数TABLE_ROWS等等。

我们在学习数据库的时候也一定学习过下面的语句:

1
SELECT DATABASE()

在我们打开的数据库中执行后,截图如下:

image-20220105183438871

由此,我们可以通过下面的语句查询所在数据库的所有表信息:

1
SELECT * FROM information_schema.`TABLES` WHERE TABLE_SCHEMA = (SELECT DATABASE())

表信息有了,列信息在哪呢?

我们查看information_schema数据库,可以看到有一个表为COLUMNS,我们来看一下它存储了什么信息:

1
SELECT * FROM information_schema.`COLUMNS` WHERE TABLE_SCHEMA = (SELECT DATABASE())

执行结果如下:

image-20220105184153136

其中,我们可以清晰的看到表的所有列名信息COLUMN_NAME,列的数据类型DATA_TYPECOLUMN_TYPE,以及列的注释信息COLUMN_COMMENT

如此,我们便获取了数据库中所有的表的信息以及其列结构信息。

在若依系统中,其作者将第一步“SQL查询数据库结构”分为了两步,第一步,先将数据库结构查询出来,第二步,将这些信息另外存储在单独的表中。这样做的原因主要是在预览/生成时,可以减少系统一步操作的负载压力,并且数据库结构的变动本来也较少,将其查询“缓存”出来是一个不错的技巧。

总结

在人工智能逐渐发展的今天,随着各个领域业务的逐步完善与统一,大量的开发程序员或许或转型为AI维护程序员,代码生成器维护程序员…

接下去的文章,笔者会继续介绍如何从查询出的数据库表结构信息逐步生成所需要的代码的。

原文链接:https://coolshell.cn/articles/21672.html

  1. 架构的收益:

    1. 加快团队开发流程:

    2. 系统稳定性:数据一致性;快速从故障中恢复;减少故障次数与时间

    3. 简化与自动化降低人力成本,其次是时间成本,资金成本。

      1. 比如开发系统必须配到对应的自动化部署脚本等。
  2. 以应用服务和 API 为视角,而不是以资源和技术为视角:站在服务和对外的时间看问题,而不是技术和底层的角度

  3. 选择最主流和成熟的技术:工业化而非自己熟悉的,全球流行而非中国流行,不要自己发明轮子,绝大多数情况下选Java

  4. 完备性比性能更重要:

  5. 制定并遵循服从标准、规范和最佳实践:

2024-03-20-12-46-45-20240320124643

  1. 重视架构的扩展性和可维护性

2024-03-20-12-46-59-20240320124658

  1. 对控制逻辑全面收口:

  2. 不要迁就老旧系统的技术债务:

  3. 不要依赖自己的经验,要依赖于数据和学习:

  4. 千万要小心X-Y问题,要追问原始需求:

解决问题速看

解决的思路是将文件编码格式由UTF-8-BOM变为UTF-8格式。

第一种方式:

第一步:打开报错文件,点击IDEA右下角“FILE ENCODING”标识:如下图:

image.png

选择一个其他编码,如GBK。(我选的是GBK)。

第二步:重复步骤一,再将编码选择回来,即第二次选择UTF-8编码。

第三步:嗯,没有第三步,这时候问题应该已经解决了。

第二种方式:

使用编辑器软件/IDE(windows记事本程序除外)将文件内容复制一份重新保存,并删除旧文件。

起因

之前项目使用的均为eclipse进行项目开发,最近团队大多数都在转IDEA,从短期的使用来看,IDEA相比eclipse目前个人最后的一点是“搜索”,其他部分使用上还没有太大的感觉。

之前使用idea导入maven项目使用均正常,最近使用idea打开传统的非maven web项目总是报出来各种奇怪的错误,路径找不到是最广泛的一类,但大多也知道怎么解决了。

今天遇到了一个奇怪的错误:

image-20211230180716299

红色文字为:

1
Java: 非法字符: '\\ufeff'

一个奇怪的错误。而且点击错误文件链接,打开文件,光标锁定在文件的开头位置。

BOM

出于好奇,我们查一下这个编码\\ufeff是个什么东东。

这个涉及到一个名词:BOM。

BOM:Byte Order Mark,中文名译作“字节顺序标记”。我们知道一个UNICODE编码中一个汉字大多数占用2个字节,那个这两个字节哪个存储在存储地址高位,哪个存储在低位呢?

Unicode编码中,FEFF表明字节流是Big-Endian(大端序,内存低地址存放高位数据),FFFE则表明字节流是Little- Endian(小端序,内存的低地址存放低位数据)。

(可以巧妙区分为:内存低地址存的是低位就是小端序,内存低地址存的是高位就是大端序)

如“0x11223344”,这个变量的高字节是”0x11“,最低字节是为”0x44“,大端存储时为:

内存地址 数据
0x0010 0x11 低内存地址,高位数据
0x0011 0x22
0x0012 0x33
0x0013 0x44 高内存地址

而小端时数据的顺序则是相反的:

内存地址 数据
0x0010 0x44 低内存地址,低位数据
0x0011 0x33
0x0012 0x22
0x0013 0x11 高内存地址

UTF-8-BOM与UTF-8

而UTF-8实际上不需要使用BOM来标识字节顺序。

在使用常用编辑器,如Notepad++时,在编码一栏下拉列表中,我们可以发现,除UTF-8编码外,还有一个UTF-8-BOM编码,而实际是,UTF-8-BOM文件就是比UTF-8文件多出文件头中的三个字节。

image-20211230173212406

image-20211230173534434

我们可以在自己电脑上实验一下,新建TXT文件,然后使用编辑器软件查看分别将其设置为UTF-8与UTF-8-BOM文件时的大小,如上图所示。

Windows基于其历史原因,在使用其记事本工具打开文件时,总是会将文件设置为UTF-8-BOM格式。

所以出现问题的文件大概率是曾经使用windows记事本打开过或者创建的。

借助一些工具,我们可以更直观的理解这一点。

HEX-Editor插件

Notepadd++中的插件HEX-Editor插件可以辅助我们查看文件的十六进制编码,进而帮助理解大端小端。UTF8与UTF-8-BOM等编码格式。

HEX-Editor插件安装

点击Notepadd++菜单栏“插件”->插件管理,查找或搜索”HEX-Editor”,勾选之后,点击右侧“安装”,安装完成之后重启Notepadd++,此时再点击“插件”,可以看到“HEX-Editor”选项,鼠标移动上去之后,可以看到“View in HEX”选项,点击即可查看文件的十六进制编码。

image-20211230173648339

纯数字或字母的UTF-8与UTF-8-BOM

通过Notepadd++我们编辑一个文件,简单的输入“012”,通过操作栏“编码”分别设置为大端,小端,UTF-8,UTF-8-BOM,其十六进制编码分贝如下:

大端:

image-20211230180304094

小端:012

image-20211230180348512

UTF-8:012

image-20211230180430326

UTF-8-BOM:012

image-20211230180456561

在笔者的机器上,一个数字使用Unicode编码时占用两个字节,也就需要约定地位地址到底是存高位数据还是地位数据,此时约定是必要的;而使用UTF-8时,可以看出,其只占用一个字节,约定顺序是没有意义的。

问题来了,一个字节,8位数据肯定是不能表达出所有的中文字符的,那么中文字符又是如何在UTF-8中编码的呢?此时BOM是否需要呢?

中文的UTF-8编码有约定大小端的必要吗

“你好”的UTF-8字节信息:

image-20211230181219116

“你好”的UTF-8-BOM字节信息:

image-20211230181725585

我们通过查询“你”和“好”的UTF-8编码,发现他们分别占用三个字节:“0xE4BDA0”(你),“0xE5A5BD”(好),编码必然是完整而次序一致的,也就是说,0xE4BDA0不能按照字节逆序成“0xA0BDE4”,这样只会增加解码的计算量。所以,即使多字节情况下,UTF-8仍然不需要去约定大小端。

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

继《若依中的代码生成器-Domain代码生成篇1》,我们继续来学习若依管理系统中的代码生成器。

上次说到通过mybatis-collection机制,将数据库中的一对多数据映射为Java对象GenTable,之后又是如何一步一步的生成预览代码的呢?

我们注意到GenTable中有一个字段名为tplCategory,它可取值crudtreesub,分别表示

crud:单表;tree:树表操作;sub:主子表。暂未涉及,按下不表[手动狗头]。

模板引擎 Velocity

后续代码执行到:

1
VelocityInitializer.initVelocity();

这一句执行中,引入了一个第三方的类库org.apache.velocity.app.Velocity。经过询问度娘与谷哥,我们知道这是一个模板引擎。

通过简单阅读Velocity文档,了解到它可以通过配置模板并配合java代码实现后续内容填充。比如在掘金网站上有一篇博客介绍了如何使用Velocity:Velocity开发指南,例如,模板文件hello.vm中内容为:

1
Hi! This $name from the $project project.

通过对nameproject赋值,我们可以生成想要的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
		/* 首先,初始化运行时引擎,使用默认的配置 */
Velocity.init();
/* 创建Context对象,然后把数据放进去 */
VelocityContext context = new VelocityContext();
context.put("name", "Velocity");
context.put("project", "Jakarta");
/* 渲染模板 */
StringWriter w = new StringWriter();
Velocity.mergeTemplate("testtemplate.vm", context, w );
System.out.println(" template : " + w );
// 代码来源:
// 作者:小小小海文
// 链接:https://juejin.cn/post/6844904177949212686
// 来源:稀土掘金
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

然后,自然而然的下一步就是构造Velocity的数据以及获取模板。构造数据代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 设置模板变量信息
*
* @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());
// 省略代码......
return velocityContext;
}

寻找模板代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
 List<String> templates = new ArrayList<String>();
templates.add("vm/java/domain.java.vm");
templates.add("vm/java/mapper.java.vm");
templates.add("vm/java/service.java.vm");
templates.add("vm/java/serviceImpl.java.vm");
templates.add("vm/java/controller.java.vm");
templates.add("vm/xml/mapper.xml.vm");
templates.add("vm/sql/sql.vm");
templates.add("vm/js/api.js.vm");
if (GenConstants.TPL_CRUD.equals(tplCategory))
{
templates.add("vm/vue/index.vue.vm");
}
else if (GenConstants.TPL_TREE.equals(tplCategory))
{
templates.add("vm/vue/index-tree.vue.vm");
}
else if (GenConstants.TPL_SUB.equals(tplCategory))
{
templates.add("vm/vue/index.vue.vm");
templates.add("vm/java/sub-domain.java.vm");
}

其中特意根据表类型做了区分,我们来看看其中的domain.java.vm长什么样子?

image.png

有点复杂的样子。

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

前两天,通过文章: 《若依中的代码自动生成器研究-表查询篇》以及《若依中的代码生成器-数据库篇》对若依代码生成器的前一段代码的阅读,我们了解了若依代码生成器的一些逻辑,包括通过数据库的information_schema. TABLES查询表信息,以及information_schema. COLUMNS查询指定表的列信息,将其转换到表gen_tablegen_table_column中的数据行,以便后续查询与代码转换。

今天我们继续来看若依系统中是如何自动生成domain代码的。

我们在系统菜单“系统工具”->”代码生成”中以及可以看到我们导入的表my_user,如下图所示:

image.png

这一行右侧有5个按钮,分别是预览、编辑、删除、同步、生成代码。

预览

我们点击“预览”,在弹出窗口中可以看到可以预览生成的代码包括:domain.java, mapper.java, service.java, serviceImpl.java,controller.java, mapper.xml, sql, api.js, index.vue。

其中domain.java, mapper.java, mapper.xml, sql都是与数据库紧密相关的,domain即生成对应数据库表的类,mapper与sql中则包含数据库基本的增删改查。

我们来研究一下domain的生成逻辑。

通过F12调试,发现点击预览的接口为:/tool/gen/preview/?id, 如下图所示:

image.png

接口代码经过查找,controller中如下:

1
2
3
4
5
6
7
8
9
10
/**
* 预览代码
*/
@PreAuthorize("@ss.hasPermi('tool:gen:preview')")
@GetMapping("/preview/{tableId}")
public AjaxResult preview(@PathVariable("tableId") Long tableId) throws IOException
{
Map<String, String> dataMap = genTableService.previewCode(tableId);
return AjaxResult.success(dataMap);
}

通过逐层分解,我们找到其中的一些关键代码:

mybatis的collections一对多查询

我们知道,gen_table中的一行数据对应gen_table_column中的多行数据,那么mybatis是如何查询这种结果的呢?

这里有一个很好的示例。

其关键代码如下所图所示(源代码位于项目ruoyi-generator/resources/GenTableMapper.xml中)

image.png

具体的查询语句如下图所示:

image.png

如此,我们便将数据库中的一对多数据查询并映射为Java中的一个对象GenTable,如此便利于后续的代码生成操作。

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

继上一篇《若依中的代码自动生成器研究-表查询篇》,我们继续来学习若依系统中的代码生成逻辑。

导入表之Sql查询

在菜单栏点击“代码生成”,在右侧栏中点击“导入”按钮,在文章若依中的代码自动生成器研究-表查询篇中,我们已经一直到若依是通过查询数据库的information_schema.tables从而查询到数据库的所有表。

我们下一步的操作是,“勾选my_user表,点击确定”。操作路径示意图以及通过F12调试,查看接口请求,如下图所示。

image.png

所请求的接口是/tool/gen/importTable,我们通过idea检索后台接口,发现其位于ruoyi-generator/com.ruoyi.generator/controller/GenController下,如下图:

image.png

Controller中的代码源码如下:(我们通过调试,补充上各个值的获取数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 导入表结构(保存)
*/
@PreAuthorize("@ss.hasPermi('tool:gen:import')")
@Log(title = "代码生成", businessType = BusinessType.IMPORT)
@PostMapping("/importTable")
public AjaxResult importTableSave(String tables)
{
// 通过调试,字符串tables格式为:"my_user,table_name_1,table_name_2...."
String[] tableNames = Convert.toStrArray(tables);
// tableNames: ["my_user", "table_name_1", "table_name_2"]
// 查询表信息
List<GenTable> tableList = genTableService.selectDbTableListByNames(tableNames);
// 见下图
genTableService.importGenTable(tableList);
return AjaxResult.success();
}

image.png

我们来研究一下其中查询表的那句sql,通过在xml中搜索,其执行sql为:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT
table_name,
table_comment,
create_time,
update_time
FROM
information_schema. TABLES
WHERE
table_name NOT LIKE 'qrtz_%'
AND table_name NOT LIKE 'gen_%'
AND table_schema = (SELECT DATABASE())
AND table_name IN ('my_user')

通过这个查询将my_user表的基本信息查询出来。

那么genTableService.importGenTable(tableList);这一句又执行了哪些操作呢?我们继续来看。

导入表之导入

同过定位importGenTable,我们查询到他的执行方法,我们补充上调试过程中的参数值

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
/**
* 导入表结构
*
* @param tableList 导入表列表
*/
@Override
@Transactional
public void importGenTable(List<GenTable> tableList)
{
// tableList: [{my_user表的信息}]
// admin
String operName = SecurityUtils.getUsername();
try
{
for (GenTable table : tableList)
{
String tableName = table.getTableName(); // tableName: my_user
GenUtils.initTable(table, operName);
int row = genTableMapper.insertGenTable(table);
if (row > 0)
{
// 保存列信息
List<GenTableColumn> genTableColumns = genTableColumnMapper.selectDbTableColumnsByName(tableName);
for (GenTableColumn column : genTableColumns)
{
GenUtils.initColumnField(column, table);
genTableColumnMapper.insertGenTableColumn(column);
}
}
}
}
catch (Exception e)
{
throw new ServiceException("导入失败:" + e.getMessage());
}
}

其中有一个关键的数据结构:GenTable,它位于Domain中,对应数据库中的表gen_table,字段截图如下所示:

image.png

类似的,另外一个表gen_table_column以及其对应的domain域类为GenTableColumn,保存生成数据库表的列信息,其字段结构如下截图:该表通过字段table_id与表gen_table中的某一行关联上。

image.png

那么查询数据库表的列用的什么sql呢?

通过检查,在xml中检索到sql为:

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
SELECT
column_name,
(
CASE
WHEN (
is_nullable = 'no' && column_key != 'PRI'
) THEN
'1'
ELSE
NULL
END
) AS is_required,
(
CASE
WHEN column_key = 'PRI' THEN
'1'
ELSE
'0'
END
) AS is_pk,
ordinal_position AS sort,
column_comment,
(
CASE
WHEN extra = 'auto_increment' THEN
'1'
ELSE
'0'
END
) AS is_increment,
column_type
FROM
information_schema. COLUMNS
WHERE
table_schema = (SELECT DATABASE())
AND table_name = 'my_user'
ORDER BY
ordinal_position

是通过查询information_schema. COLUMNS并约束table_name以及数据库来查询指定表my_user的所有列。

总结

通过导入操作,将my_user转换为了gen_tablegen_table_column中的几行数据,以便后续代码的生成。

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

最近生产环境用了一个开源系统:若依,其中有一个版块很有意思,很能提高生产效率,也就是代码生成器。

其功能所处模块菜单为:系统工具->代码生成。我们来研究一下他的代码生成逻辑。

工具使用方法

  1. 建表

    使用代码生成,首先需要在数据库建立一个业务表,比如我们要建立系统的用户表:表名称为my_user, 如下图

    image.png

  2. 导入

    在若依“系统工具”->“代码生成”中,点击“导入”

    image.png

    在弹出窗口中,勾选my_user,然后点击“确定”,之后在列表界面可以看到导入的表:

    image.png

  3. 代码预览

    点击my_user后面的“预览”,即可查看预生成的代码,预览代码生成很快,几乎没有延迟。

    image.png

    而且预览代码分为很多个部分,包括domain.java, mapper.java, service.java, serviceImpl.java, controller.java, mapper.xml以及前端的.vue文件与接口请求.js文件。

代码生成逻辑研究

sql查找

我们根据各个步骤请求的接口来研究一下代码生成逻辑。

点击“导入”时,窗口弹出数据库中的表列表,我们通过F12浏览器调试,发现其调用接口为/tool/gen/db/list,在后台我们去查看这个接口。接口入口如下图,在ruoyi-generator版块中。

image.png

其SQL查询语句经过逐级查找,位于GenTableMapper.xml中:

image.png

咱们拷贝出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SELECT
table_name,
table_comment,
create_time,
update_time
FROM
information_schema. TABLES
WHERE
table_schema = (SELECT DATABASE())
AND table_name NOT LIKE 'qrtz_%'
AND table_name NOT LIKE 'gen_%'
AND table_name NOT IN (
SELECT
table_name
FROM
gen_table
)
ORDER BY
create_time DESC

其中的SELECT DATABASE()可以查询当前所在数据库的数据库名称。information_schema. TABLES会显示目前本台mysql连接上的所有数据库。字段table_comment会显示我们在创建表时写的注释,即在navcat上的这一部分:

image.png

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

要学mybatis,官方文档写的是个什么玩意哦,看不懂,还是动手试试吧

创建基础项目

image.png

记得勾上mybatis,JDBC API,MySql什么的,如果这里不打钩,之后还要去手动修改maven的pom.xml文件。

image.png

然后创建项目。从网上找一个基础的mysql配置,如下图所示,修改成自己的链接。

image.png

需要注意几点:

  1. driver-class-name设置为com.mysql.jdbc.Driver时,console中会弹出错误提示:

    1
    Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.
  2. 默认生成的pom.xml中,tomcatscope值为provided,导致项目一启动就关闭了,我们需要把这一行注释掉:

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <!--<scope>provided</scope>-->
    </dependency>

然后,我们按照服务层次,依次创建项目文件夹:controller,service以及mybatis相关的entity, mapper。

image.png

配置MyBatis

在文件application.yml中,我们简单的配置mybatis的mapper路径与实体包路径。

1
2
3
4
5
6
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.example.demo.mybatis.entity
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath*:mybatis/mapper/*Mapper.xml #xml是存放在resources目录下的

试运行

一切配置好之后,尝试运行,嗯,报错。提示找不到对应的bean:

image.png

经过检索发现,springboot要求将扫描目录放置在主程序入口的同一个包或子包中

所以我们通过IDE调整目录。目录我们稍后截图看效果。

数据库建表

我们建立一个简单的表看一下,名叫s_user, 表结构如下:

image.png

在IDE中建立他的entity,以及xml文件存放sql。

经过输入,最终,我们的程序目录如下图:

image.png

在接口中,我们简单实现代码为:

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class UserController {

@Autowired
private UserService userServiceImpl;

@RequestMapping("/")
@ResponseBody
public String GetUser(){
return userServiceImpl.findById(1).toString();
}
}

其中对应的sql是

1
select * from s_user where id = #{id}

最终页面效果:

image.png

这个跟springboot配置有关,本着文章为介绍mybatis,具体如何文字输出我们不再阐述。