写在前面

在前面我们学习了SpringBoot+Vue这一套前后端分离的优秀架构,接下来我们尝试基于此实现一个代码生成器。在平时工作中,可能我们使用比较多的还是Mybatis Generate生成器,以此来根据数据表逆向生成对应的Dao层和Mapper层代码。其实生成器的核心,是使用JDBC来获取数据库中的各种元数据信息,并基于此来实现各种功能。本篇要实现的代码生成器不仅可以生成Dao层和Mapper层代码,还可以生成Service和Controller层代码,涵盖了一些基本操作,开发者要是实现一个简单的项目,几乎可以做到不写任何一行代码。

用法介绍

用户在序号1/2/3分别输入数据库用户名,密码、连接地址,然后点击序号4,如果数据库可以连接得上,那么序号5就会展示连接成功的提示信息,否则展示连接失败;如果成功接着在序号6中输入要生成的包的名称,也就是${groupId}.${artifactId},接着点击序号7来生成配置,如果成功那么序号8处就会生成对应的数据表名称、实体类名称、mapper文件名称、service名称和controller名称,开发者也可以对这些名称进行修改,之后点击序号9来生成代码,如果生成成功,那么序号10会显示代码生成成功,并在序号11处显示生成的文件地址;如果生成失败,那么序号10会显示代码生成失败,序号11没有内容。

数据库连接实现

下图是本部分需要实现的界面,用户输入用户名、密码和连接地址,然后点击测试连接按钮,后展示后台接口返回的信息:

数据库连接后端接口实现

第一步,新建一个名为code-generator的SpringBoot项目,在其POM文件中新增web、mysql和freemarker依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>

其中web用于展示页面,mysql用于连接数据库并获取连接元数据信息,freemarker用于制作生成的代码模板。

第二步,新建controller、model、service和utils包,并在model包里面新建Db实体类,这个表示用户前端输入的用户名,密码和连接地址:

1
2
3
4
5
6
7
public class Db {
private String username;
private String password;
private String url;

//getter和setter方法
}

然后在model包内新建一个名为RespBean的响应类,后端返给前端的所有数据结构均满足这个类的属性:

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
public class RespBean {
private Integer status;
private String msg;
private Object data;

public static RespBean ok(String msg, Object data){
return new RespBean(200,msg,data);
}

public static RespBean ok(String msg){
return new RespBean(200,msg,null);
}

public static RespBean error(String msg, Object data){
return new RespBean(500,msg,data);
}

public static RespBean error(String msg){
return new RespBean(500,msg,null);
}

private RespBean() {
}

private RespBean(Integer status, String msg, Object data) {
this.status = status;
this.msg = msg;
this.data = data;
}

//getter和setter方法
}

第三步,在utils包内新建一个名为DBUtils的工具类,用于返回一个数据库连接和初始化数据库信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* DB工具类,用于获取连接信息
*/
public class DBUtils {
private static Connection connection;

public static Connection getConnection(){
return connection;
}

public static Connection initDb(Db db){
if(null == connection){
try {
Class.forName("com.mysql.cj.jdbc.Driver");
connection = DriverManager.getConnection(db.getUrl(),db.getUsername(), db.getPassword());
} catch (ClassNotFoundException | SQLException e) {
e.printStackTrace();
}
}
return connection;
}
}

第四步,在controller包内新建一个名为DbController的数据库类,用于提供所需要的数据库测试,信息获取等接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class DbController {

/**
* 获取用户设置的数据库信息
* @return
*/
@PostMapping("/connect")
public RespBean connect(@RequestBody Db db){
Connection connection = DBUtils.initDb(db);
if(null == connection){
return RespBean.error("数据库连接失败");
}
return RespBean.ok("数据库连接成功");
}
}

这样数据库连接后端的连接测试接口就已经实现了。

数据库连接前端页面实现

在项目resources/static目录下新建一个名为index.html的文件,里面的代码如下所示:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>代码生成器</title>
<!--在导入Element之前导入Vue-->
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
<!--导入ElementUI样式-->
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
<!--导入Element所需的JS-->
<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
<!--导入Axios所需的JS-->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<el-row>
<el-col :span="3">
<div>
<el-tag size="mini">数据库用户名</el-tag>
</div>
</el-col>
<el-col :span="12">
<div>
<el-input size="mini" v-model="db.username"></el-input>
</div>
</el-col>
<el-col :span="9">
<div></div>
</el-col>
</el-row>

<el-row>
<el-col :span="3">
<div>
<el-tag size="mini">数据库用户密码</el-tag>
</div>
</el-col>
<el-col :span="12">
<div>
<el-input size="mini" v-model="db.password"></el-input>
</div>
</el-col>
<el-col :span="9">
<div></div>
</el-col>
</el-row>

<el-row>
<el-col :span="3">
<div>
<el-tag size="mini">数据库连接地址</el-tag>
</div>
</el-col>
<el-col :span="12">
<div>
<el-input size="mini" v-model="db.url">
<template slot="prepend">jdbc:mysql://</template>
<template slot="append">
?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
</template>
</el-input>
</div>
</el-col>
<el-col :span="9">
<div></div>
</el-col>
</el-row>

<el-row>
<el-col :span="3">
<div>
<el-button type="primary" size="mini" :disabled="!connectBtnEnabled" @click="connect">测试连接</el-button>
</div>
</el-col>
<el-col :span="3">
<div>
<div style="color: red;font-weight: bold">{{msg}}</div>
</div>
</el-col>
</el-row>
</div>

<script>
var app = new Vue({
el: "#app",
data(){
return {
msg: "数据库未连接",
connectBtnEnabled: true,
db: {
username: "root",
password: "root1234",
url: "localhost:3306/code-generator"
}
}
},
methods: {
connect(){
let _this = this;
this.db.url = "jdbc:mysql://" + this.db.url + "?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai";
axios.post('/connect', this.db)
.then(function (response) {
//response.data才是后端返回的数据
//展示成功消息
_this.msg = response.data.msg;
//还原数据库初始信息
_this.db = {
username: "root",
password: "root1234",
url: "localhost:3306/code-generator"
};
//禁用按钮
_this.connectBtnEnabled = false;
})
.catch(function (error) {
console.log(error);
});
}
}
})
</script>
</body>
</html>

简单解释一下上述代码的含义:
(1)导入ElementUI、Vue和网络请求Axios库,注意Vue需要在ElementUI之前进行导入;
(2)使用了ElementUI的栅格布局,上面三行均采用3 12 9这一布局,标签名使用el-tag元素;输入框使用el-input元素;然后由于数据库连接地址我们采用了拼接这一方式,因此需要使用el-input元素中的prepend和append分别表示输入框的前置和后置内容,注意这与prefix和suffix的区别,后者分别表示输入框头部和尾部内容;
(3)用户未点击测试连接按钮之前,该按钮可以点击,一旦点击之后该按钮就会置灰,无法再次点击,除非刷新页面。点击该按钮之后会去请求后端名为/connect的接口,然后从返回的数据中取出信息并进行展示,也就是图中的msg。这里我们使用axios向后端发起请求;
(4)请注意axios请求如果成功,那么返回的RespBean对象存在于response的data选项中,之后就可以从中取出对应的数据。这里有两个注意点,第一需要先定义一个局部变量_this用于指代提交前的初始值,因为用户传入的url是只含地址,不包含前缀和后缀,而你如果直接提交,不还原之前的数据,那么之后提交的URL都是错的,所以我们让用户提交之后,让数据还原。第二,点击提交之后按钮需要置灰,用户无法再次点击,这些都是ElementUI中的基本用法。

用户未点击连接按钮之前,页面展示如下信息:

当用户信息输入正确,点击测试连接按钮之后,页面展示如下信息:

当用户信息输入错误,点击测试连接按钮之后,页面展示如下信息:

表对应实体类/Mapper/Service/Controller名称生成实现

下图是本部分需要实现的界面,用户输入需要生成的包的前缀,点击生成配置按钮,即可显示对应表的实体类/Mapper/Service/Controller名称:

实体映射后端接口实现

由于我们需要根据数据库的表来生成与之对应的实体类/Mapper/Service/Controller名称,而数据表和Java实体类之间需要有映射关系,这些关系具体点就是数据表中某个字段与Java实体类对应属性的映射,因此需要先定义一个ColumnClass类来描述映射信息。

第一步,在model包中新建一个名为ColumnClass的类,用于描述数据表中字段与Java实体类中属性映射关系:

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
/**
* 描述数据表中字段与Java实体类中属性映射关系
*/
public class ColumnClass {
/**
* Java实体类中属性名称
*/
private String propertyName;

/**
* 数据表中字段名称
*/
private String columnName;

/**
* 数据表中字段类型
*/
private String type;

/**
* 数据表中字段是否为主键
*/
private Boolean isPrimary;

/**
* 数据表中字段是否为空
*/
private Boolean isNull;

/**
* 数据表中字段备注
*/
private String remark;

//toString、setter和getter方法
}

第二步,在model包中新建一个名为TableClass的类,用于描述数据表与Java中对应模块名称的映射关系:

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
/**
* 描述数据表与Java中对应模块名称的映射关系
*/
public class TableClass {
/**
* 数据表名称
*/
private String tableName;

/**
* 实体类名称
*/
private String modelName;

/**
* Mapper名称
*/
private String mapperName;

/**
* Service名称
*/
private String serviceName;

/**
* Controller名称
*/
private String controllerName;

/**
* 所在包的名称
*/
private String packageName;

/**
* 所对应的字段信息
*/
private List<ColumnClass> columns;

//toString、setter和getter方法
}

第三步,将下来完成“生成配置”这一按钮所请求的后端接口。由于用户传入的只有包的名称,因此不建议直接定义一个对象,而是通过Map来传值,否则还需要修改axios的默认传值方式(默认是传对象)。

在之前定义的DbController类中新增一个名为config的方法,代码如下所示:

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
@PostMapping("/config")
public RespBean config(@RequestBody Map<String,String> map){
String packageName = map.get("packageName");
try{
Connection connection = DBUtils.getConnection();
//获取数据库的元数据
DatabaseMetaData metaData = connection.getMetaData();
//获取数据库中所有的表
ResultSet tables = metaData.getTables(connection.getCatalog(), null, null, null);
//将这些表都转换为之前定义的TableClass对象
List<TableClass> tableClassList = new ArrayList<>();
while (tables.next()){
TableClass tableClass = new TableClass();
//获取表名称
String tableName = tables.getString("TABLE_NAME");
tableClass.setTableName(tableName);
tableClass.setPackageName(packageName);
//获取实体类名称(一般是表名驼峰法且首字母大写这一方式)
//表名目前是小写下划线形式,转成首字母大写驼峰形式
String modelName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, tableName);
tableClass.setModelName(modelName);
tableClass.setMapperName(modelName + "Mapper");
tableClass.setServiceName(modelName + "Service");
tableClass.setControllerName(modelName + "Controller");
tableClassList.add(tableClass);
}
return RespBean.ok("数据表信息读取成功",tableClassList);
}catch (Exception e){
e.printStackTrace();
}
return RespBean.error("数据表信息读取成功");
}

可以看到这个方法就是获取数据库连接并从中得到数据库的元数据信息,然后调用 metaData.getTables()方法得到所选择数据库的所有表,该方法定义如下:

1
2
ResultSet getTables(String catalog, String schemaPattern,
String tableNamePattern, String types[]) throws SQLException;

catalog是目录名称,它必须与存储在数据库中的目录名称匹配;如果值为””,则表示检索那些没有目录的;如果值为null,则表示不使用目录名称来缩小搜索范围。

schemaPattern是模式名称模式,它必须与存储在数据库中的模式名称匹配;如果值为””,则表示检索那些没有模式的; 如果值为null,则表示不使用模式名称来缩小搜索范围。

tableNamePattern是表名模式,它必须与存储在数据库类型中的表名匹配。types[]是表类型列表,它必须是getTableTypes()方法返回的表类型列表,这样才能包括在内; 如果值为null,则表示返回所有类型。因此此处后续三个参数均使用null值。

在得到所有的数据表之后,接下来我们就可以通过tables.getString("TABLE_NAME")方法得到表的名称,由于表的名称是小写下划线形式,而Java实体类及其他模块都是首字母大写驼峰形式,因此需要进行转换。这里使用谷歌提供的Guava工具进行转换,开发者也可以自行进行编写转换逻辑,这个很简单的。既然使用了Guava,那么就需要在POM文件中进行依赖引入:

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>20.0</version>
</dependency>

之后就是设置TableClass对象对应的属性,当然了这里我们没有对它的columns属性进行设置,这里直接使用默认值,且一般数据库字段和实体类字段习惯上也是采用表默认的小写下划线形式转成首字母大写驼峰形式。

实体映射前端页面实现

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>代码生成器</title>
<!--在导入Element之前导入Vue-->
<script src="https://unpkg.com/vue@2.6.11/dist/vue.js"></script>
<!--导入ElementUI样式-->
<link rel="stylesheet" href="https://unpkg.com/element-ui@2.13.0/lib/theme-chalk/index.css">
<!--导入Element所需的JS-->
<script src="https://unpkg.com/element-ui@2.13.0/lib/index.js"></script>
<!--导入Axios所需的JS-->
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<el-row>
<el-col :span="3">
<div>
<el-tag size="mini">数据库用户名</el-tag>
</div>
</el-col>
<el-col :span="12">
<div>
<el-input size="mini" v-model="db.username"></el-input>
</div>
</el-col>
<el-col :span="9">
<div></div>
</el-col>
</el-row>

<el-row>
<el-col :span="3">
<div>
<el-tag size="mini">数据库用户密码</el-tag>
</div>
</el-col>
<el-col :span="12">
<div>
<el-input size="mini" v-model="db.password"></el-input>
</div>
</el-col>
<el-col :span="9">
<div></div>
</el-col>
</el-row>

<el-row>
<el-col :span="3">
<div>
<el-tag size="mini">数据库连接地址</el-tag>
</div>
</el-col>
<el-col :span="12">
<div>
<el-input size="mini" v-model="db.url">
<template slot="prepend">jdbc:mysql://</template>
<template slot="append">
?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
</template>
</el-input>
</div>
</el-col>
<el-col :span="9">
<div></div>
</el-col>
</el-row>

<el-row>
<el-col :span="3">
<div>
<el-button type="primary" size="mini" :disabled="!connectBtnEnabled" @click="connect">测试连接</el-button>
</div>
</el-col>
<el-col :span="3">
<div>
<div style="color: red;font-weight: bold">{{msg}}</div>
</div>
</el-col>
<el-col :span="6">
<div>
<el-tag size="mini" style="width: 80px">请输入包名</el-tag>
<el-input v-model="packageName" size="mini" style="width: 300px"></el-input>
</div>
</el-col>
<el-col :span="3">
<div>
<el-button type="primary" size="mini" @click="config">生成配置</el-button>
</div>
</el-col>
<el-col :span="9">
<div></div>
</el-col>
</el-row>
</div>

<script>
var app = new Vue({
el: "#app",
data(){
return {
packageName: "com.kenbingthoughts.test",
msg: "数据库未连接",
connectBtnEnabled: true,
db: {
username: "root",
password: "root1234",
url: "localhost:3306/code-generator"
}
}
},
methods: {
config(){
let _this = this;
axios.post('/config', {packageName: this.packageName})
.then(function (response) {
//response.data才是后端返回的数据
//展示成功消息
_this.msg = response.data.msg;
})
.catch(function (error) {
console.log(error);
});
},
connect(){
let _this = this;
this.db.url = "jdbc:mysql://" + this.db.url + "?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai";
axios.post('/connect', this.db)
.then(function (response) {
//response.data才是后端返回的数据
//展示成功消息
_this.msg = response.data.msg;
//还原数据库初始信息
_this.db = {
username: "root",
password: "root1234",
url: "localhost:3306/code-generator"
};
//禁用按钮
_this.connectBtnEnabled = false;
})
.catch(function (error) {
console.log(error);
});
}
}
})
</script>
</body>
</html>

在里面添加如下代码,具体的看代码:

表中数据获取到之后,接下来就是通过表格展示数据了:

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
56
57
58
59
60
61
62
63
64
65
    <el-row>
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
prop="tableName"
label="数据表名称"
width="300">
</el-table-column>
<el-table-column
prop="modelName"
label="实体类名称"
width="300">
</el-table-column>
<el-table-column
prop="mapperName"
label="mapper名称"
width="300">
</el-table-column>
<el-table-column
prop="serviceName"
label="service名称"
width="300">
</el-table-column>
<el-table-column
prop="controllerName"
label="controller名称"
width="300">
</el-table-column>
</el-table>
</el-row>

<script>
var app = new Vue({
el: "#app",
data(){
return {
tableData: [],
packageName: "com.kenbingthoughts.test",
msg: "数据库未连接",
connectBtnEnabled: true,
db: {
username: "root",
password: "root1234",
url: "localhost:3306/code-generator"
}
}
},
methods: {
config(){
let _this = this;
axios.post('/config', {packageName: this.packageName})
.then(function (response) {
//response.data才是后端返回的数据
//展示成功消息
_this.tableData = response.data.data;
})
.catch(function (error) {
console.log(error);
});
},
......
}
})
</script>

之后重启项目,页面展示如下所示:

输入正确信息后,点击测试连接通过后,再点击右侧的生成配置按钮:

可以看到我们所需要的数据已经得到了,但是目前表格还不能编辑,因此我们需要对代码进行修改。其实我们要修改的只是表格中prop字段的值,因此可以使用template中的slot方式来进行替换,同时为了验证上述方式可以修改表格中的值,这里我们还定义了一个updateCode方法用于进行测试:

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
<el-row>
<el-table
:data="tableData"
style="width: 100%">
<el-table-column
prop="tableName"
label="数据表名称"
width="300">
</el-table-column>
<el-table-column
label="实体类名称"
width="300">
<template slot-scope="scope">
<el-input v-model="scope.row.modelName"></el-input>
</template>
</el-table-column>
<el-table-column
label="mapper名称"
width="300">
<template slot-scope="scope">
<el-input v-model="scope.row.mapperName"></el-input>
</template>
</el-table-column>
<el-table-column
label="service名称"
width="300">
<template slot-scope="scope">
<el-input v-model="scope.row.serviceName"></el-input>
</template>
</el-table-column>
<el-table-column
label="controller名称"
width="300">
<template slot-scope="scope">
<el-input v-model="scope.row.controllerName"></el-input>
</template>
</el-table-column>
</el-table>
<div>
<el-button @click="updateCode" type="success">修改代码</el-button>
</div>
</el-row>

然后在methods选项中新增如下配置信息:

1
2
3
4
5
6
7
8
9
updateCode(){
axios.post('/updateCode', this.tableData)
.then(function (response) {
console.log(response.data);
})
.catch(function (error) {
console.log(error);
});
},

其实这个/updateCode接口后端是不用提供的,这里只是为了测试用户是否真的修改了表格中的数据,只需通过查看用户提交的信息就能确定。之后启动项目,刷新一下首页,可以看到页面如下所示:

之后修改出现两个数据表所对应的实体类名称,然后点击修改代码按钮,可以看到提交给后端的API中实体类名称已经是修改之后的值了,这就说明前面修改数据的方法是有效的:

后端表对应实体类模板生成实现

下图是本部分需要实现的界面,用户输入需要生成的包的前缀,点击生成配置按钮,即可显示对应表的实体类/Mapper/Service/Controller名称,然后点击生成代码按钮,即可出现生成实体类的位置:

后端模板生成实现

前面我们已经得到了TableClass对象,这里面的信息就可以指导我们生成对应的模板信息,之所以有“表对应实体类/Mapper/Service/Controller名称生成实现”这一过程,是因为我们允许用户对TableClass对象进行修改,也就是说我们得到的最终对象是用户提交后的TableClass对象。接下来我们就依据这个TableClass对象来生成对应的模板文件。模板文件类型使用FreeMarker来实现,这也是比较通用的做法。

第一步,制作实体类模板,由于之前我们在ColumnClass类中定义的只是数据表中的type,而不是实体类中的type,因此这里涉及到一个类型的转换,即根据数据表中字段的类型来选择使用对应的Java数据类型。在项目的resources/templates目录下新建一个名为Model.java.ftlh的FreeMarker文件,里面的代码如下所示:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
package ${packageName}.model;

import java.util.Date;

public class ${modelName} {
<#if columns??>
<#list columns as column>
<#if column.type = 'VARCHAR'||column.type = 'TEXT'||column.type = 'CHAR'>
/**
* ${column.remark}
*/
private String ${column.propertyName?uncap_first};
</#if>
<#if column.type = 'INT'>
/**
* ${column.remark}
*/
private Integer ${column.propertyName?uncap_first};
</#if>
<#if column.type = 'BIGINT'>
/**
* ${column.remark}
*/
private Long ${column.propertyName?uncap_first};
</#if>
<#if column.type = 'DOUBLE'>
/**
* ${column.remark}
*/
private Double ${column.propertyName?uncap_first};
</#if>
<#if column.type = 'DATETIME'>
/**
* ${column.remark}
*/
private Date ${column.propertyName?uncap_first};
</#if>
<#if column.type = 'BIT'>
/**
* ${column.remark}
*/
private Boolean ${column.propertyName?uncap_first};
</#if>
</#list>
</#if>

<#if columns??>
<#list columns as column>
<#if column.type = 'VARCHAR'||column.type = 'TEXT'||column.type = 'CHAR'>
public String get${column.propertyName}(){
return this.${column.propertyName?uncap_first};
}

public void set${column.propertyName}(String ${column.propertyName?uncap_first}){
this.${column.propertyName?uncap_first} = ${column.propertyName?uncap_first};
}
</#if>
<#if column.type = 'INT'>
public Integer get${column.propertyName}(){
return this.${column.propertyName?uncap_first};
}

public void set${column.propertyName}(Integer ${column.propertyName?uncap_first}){
this.${column.propertyName?uncap_first} = ${column.propertyName?uncap_first};
}
</#if>
<#if column.type = 'BIGINT'>
public Long get${column.propertyName}(){
return this.${column.propertyName?uncap_first};
}

public void set${column.propertyName}(Long ${column.propertyName?uncap_first}){
this.${column.propertyName?uncap_first} = ${column.propertyName?uncap_first};
}
</#if>
<#if column.type = 'DATETIME'>
public Date get${column.propertyName}(){
return this.${column.propertyName?uncap_first};
}

public void set${column.propertyName}(Date ${column.propertyName?uncap_first}){
this.${column.propertyName?uncap_first} = ${column.propertyName?uncap_first};
}
</#if>
<#if column.type = 'DOUBLE'>
public Double get${column.propertyName}(){
return this.${column.propertyName?uncap_first};
}

public void set${column.propertyName}(Double ${column.propertyName?uncap_first}){
this.${column.propertyName?uncap_first} = ${column.propertyName?uncap_first};
}
</#if>
<#if column.type = 'BIT'>
public Boolean get${column.propertyName}(){
return this.${column.propertyName?uncap_first};
}

public void set${column.propertyName}(Boolean ${column.propertyName?uncap_first}){
this.${column.propertyName?uncap_first} = ${column.propertyName?uncap_first};
}
</#if>
</#list>
</#if>
}

这个类之后就是生成的实体类,这里我们导入基本的信息,注意这里面的一些变量均来自用户传入的信息。这里我们需要对用户在数据表中的字段属性和Java中的实体类属性类型进行转换,转换的规则代码中可以看到,这里就不介绍了。

第二步,在controller包内新建一个名为UpdateCodeController的类,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
@RestController
public class UpdateCodeController {
@Autowired
private UpdateCodeService updateCodeService;

@PostMapping("/updateCode")
public RespBean updateCode(@RequestBody List<TableClass> tableClassList, HttpServletRequest req){
return updateCodeService.updateCode(tableClassList,req.getServletContext().getRealPath("/"));
}
}

第三步,新建service包,并在该包中新建UpdateCodeService类,里面的代码如下所示:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
@Service
public class UpdateCodeService {

//加载FreeMarker配置文件
Configuration cfg = null;
{
//创建当前FreeMarker对应版本的配置类
cfg = new Configuration(VERSION_2_3_31);
//设置模板的存放位置
cfg.setTemplateLoader(new ClassTemplateLoader(UpdateCodeService.class,"/templates"));
//设置模板的编码格式
cfg.setDefaultEncoding("UTF-8");
}

public RespBean updateCode(List<TableClass> tableClassList, String realPath) {
try{
//定义模板
Template modelTemplate = cfg.getTemplate("Model.java.ftlh");
//获取数据库连接
Connection connection = DBUtils.getConnection();
//获取数据库元数据,需要使用里面的表名和字段名
DatabaseMetaData metaData = connection.getMetaData();
//遍历用户传进来的tableClassList信息
for (TableClass tableClass:tableClassList){
//获取指定数据表中所有的字段信息
ResultSet columns = metaData.getColumns(connection.getCatalog(), null, tableClass.getTableName(), null);
//获取指定数据表中所有的主键信息
ResultSet primaryKeys = metaData.getPrimaryKeys(connection.getCatalog(),null, tableClass.getTableName());

//定义一个集合用于存放某个数据表中的所有字段
List<ColumnClass> columnClassList = new ArrayList<>();

//遍历数据表中所有的字段信息
while (columns.next()){
//获取字段名称
String columnName = columns.getString("COLUMN_NAME");
//字段是否是否为空,值为NO或者YES
String isNullable = columns.getString("IS_NULLABLE");
//数据表中字段类型
String dataType = columns.getString("TYPE_NAME");
//数据表中字段备注
String columnComment = columns.getString("REMARKS");

//组装字段信息
ColumnClass columnClass = new ColumnClass();
//数据表中字段名称
columnClass.setColumnName(columnName);
//Java实体类中属性名称
columnClass.setPropertyName(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL,columnName));
//数据表中字段是否为空
if("YES".equals(isNullable)){
columnClass.setNull(true);
}else{
columnClass.setNull(false);
};
//数据表中字段备注
columnClass.setRemark(columnComment);
//数据表中字段类型
columnClass.setType(dataType);

//将主键遍历游标置为0,重新开始
primaryKeys.first();
while (primaryKeys.next()){
String pkName = primaryKeys.getString("COLUMN_NAME");
//如果主键名称等于字段名称,那么该字段就是主键
if(columnName.equals(pkName)){
//数据表中字段是否为主键
columnClass.setPrimary(true);
}
}
columnClassList.add(columnClass);
}
tableClass.setColumns(columnClassList);
//将包名转换成路径地址
String path = realPath + "/" + tableClass.getPackageName().replace(".","/");
//生成该表对应的实体类
update(modelTemplate,tableClass,path + "/model/");
}
return RespBean.ok("代码生成成功",realPath);
}catch (Exception e){
e.printStackTrace();
}
return RespBean.error("代码生成失败");
}

/**
* 生成对应的实体类
* @param template 实体类模板
* @param tableClass 实体类对应的TableClass对象
* @param path 实体类存放路径
*/
private void update(Template template, TableClass tableClass, String path) throws IOException, TemplateException {
//创建文件夹
File folder = new File(path);
if(!folder.exists()){
folder.mkdirs();
}
//创建对应的实体类文件(xxx.java文件)
String fileName = path + "/"+ tableClass.getModelName()+ template.getName().replace("Model","").replace("ftlh","");

FileOutputStream fos = new FileOutputStream(fileName);
OutputStreamWriter osw = new OutputStreamWriter(fos);

//将数据写入模板中
template.process(tableClass,osw);

fos.close();
osw.close();
}
}

这些方法的作用笔者已经代码中进行了详细介绍,这里就不过多介绍了。

第四步,在resources/static目录下的index.html中新增如下代码:

第五步,启动项目进行测试,刷新一下页面:

之后输入正确的信息,点击测试连接按钮后,成功的话再次点击生成配置按钮,最后点击生成代码按钮,可以看到此时页面出现了生成代码的存放位置,且对应位置下确实已经生成了对应的实体类:

本部分后端代码是整个代码生成器的核心,在了解和知悉实体类的生成原理后,其他对应模板的制作和生成差不多。

后端表对应Mapper/Service/Controller模板生成实现

后端mapper模板实现

在项目的resources/templates目录下新建一个名为Mapper.java.ftlh的FreeMarker文件,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
package ${packageName}.mapper;

import ${packageName}.model.${modelName};
import org.apache.ibatis.annotations.Mapper;
import java.util.*;

@Mapper
public interface ${mapperName} {
List<${modelName}> getAll${modelName}s();
}

后端service模板实现

在项目的resources/templates目录下新建一个名为Service.java.ftlh的FreeMarker文件,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package ${packageName}.service;

import ${packageName}.model.${modelName};
import ${packageName}.mapper.${mapperName};
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;

@Service
public class ${serviceName} {
@Autowired
private ${mapperName} ${mapperName?uncap_first};

public List<${modelName}> getAll${modelName}s(){
return ${mapperName?uncap_first}.getAll${modelName}s();
}
}

后端controller模板实现

在项目的resources/templates目录下新建一个名为Controller.java.ftlh的FreeMarker文件,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package ${packageName}.controller;

import ${packageName}.model.${modelName};
import ${packageName}.mapper.${mapperName};
import ${packageName}.service.${serviceName};
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.*;

@RestController
public class ${controllerName} {
@Autowired
private ${serviceName} ${serviceName?uncap_first};

@GetMapping("/${modelName?lower_case}s")
public List<${modelName}> getAll${modelName}s(){
return ${serviceName?uncap_first}.getAll${modelName}s();
}
}

后端mapper XML模板实现

在项目的resources/templates目录下新建一个名为Controller.java.ftlh的FreeMarker文件,里面的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="${packageName}.mapper.${mapperName}">
<resultMap id="BaseResultMap" type="${packageName}.model.${modelName}">
<#list columns as column>
<<#if column.isPrimary??>id<#else>result</#if> column="${column.columnName}" property="${column.propertyName?uncap_first}" jdbcType="<#if column.type='INT'>INTEGER<#elseif column.type='DATETIME'>TIMESTAMP<#elseif column.type='TEXT'>VARCHAR<#else>${column.type}</#if>"/>
</#list>
</resultMap>

<select id="getAll${modelName}s" resultMap="BaseResultMap">
select * from ${tableName};
</select>
</mapper>

可以看到这里我们生成的模板中都只提供了一个查询全部数据的方法,后续开发者可以自行进行扩展,而且service层没有定义对应的接口文件,而是直接使用了实现类,这个后续也可以进行升级和修改。

更新模板生成接口

修改service包下的UpdateCodeService#updateCode()方法为如下所示:

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public RespBean updateCode(List<TableClass> tableClassList, String realPath) {
try{
//定义实体类模板
Template modelTemplate = cfg.getTemplate("Model.java.ftlh");
//定义mapper接口模板
Template mapperTemplate = cfg.getTemplate("Mapper.java.ftlh");
//定义mapper XML模板
Template mapperXMLTemplate = cfg.getTemplate("Mapper.xml.ftlh");
//定义Service类模板
Template serviceTemplate = cfg.getTemplate("Service.java.ftlh");
//定义Controller类模板
Template controllerTemplate = cfg.getTemplate("Controller.java.ftlh");

//获取数据库连接
Connection connection = DBUtils.getConnection();
//获取数据库元数据,需要使用里面的表名和字段名
DatabaseMetaData metaData = connection.getMetaData();
//遍历用户传进来的tableClassList信息
for (TableClass tableClass:tableClassList){
//获取指定数据表中所有的字段信息
ResultSet columns = metaData.getColumns(connection.getCatalog(), null, tableClass.getTableName(), null);
//获取指定数据表中所有的主键信息
ResultSet primaryKeys = metaData.getPrimaryKeys(connection.getCatalog(),null, tableClass.getTableName());

//定义一个集合用于存放某个数据表中的所有字段
List<ColumnClass> columnClassList = new ArrayList<>();

//遍历数据表中所有的字段信息
while (columns.next()){
//获取字段名称
String columnName = columns.getString("COLUMN_NAME");
//字段是否是否为空,值为NO或者YES
String isNullable = columns.getString("IS_NULLABLE");
//数据表中字段类型
String dataType = columns.getString("TYPE_NAME");
//数据表中字段备注
String columnComment = columns.getString("REMARKS");

//组装字段信息
ColumnClass columnClass = new ColumnClass();
//数据表中字段名称
columnClass.setColumnName(columnName);
//Java实体类中属性名称
columnClass.setPropertyName(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL,columnName));
//数据表中字段是否为空
if("YES".equals(isNullable)){
columnClass.setNull(true);
}else{
columnClass.setNull(false);
};
//数据表中字段备注
columnClass.setRemark(columnComment);
//数据表中字段类型
columnClass.setType(dataType);

//将主键遍历游标置为0,重新开始
primaryKeys.first();
while (primaryKeys.next()){
String pkName = primaryKeys.getString("COLUMN_NAME");
//如果主键名称等于字段名称,那么该字段就是主键
if(columnName.equals(pkName)){
//数据表中字段是否为主键
columnClass.setPrimary(true);
}
}
columnClassList.add(columnClass);
}
tableClass.setColumns(columnClassList);
//将包名转换成路径地址
String path = realPath + "/" + tableClass.getPackageName().replace(".","/");

//生成该表对应的实体类
update(modelTemplate,tableClass,path + "/model/");
//生成该表对应的mapper接口类
update(mapperTemplate,tableClass,path + "/mapper/");
//生成该表对应的mapper XML
update(mapperXMLTemplate,tableClass,path + "/mapper/");
//生成该表对应的Service类
update(serviceTemplate,tableClass,path + "/service/");
//生成该表对应的Controller类
update(controllerTemplate,tableClass,path + "/controller/");
}
return RespBean.ok("代码生成成功",realPath);
}catch (Exception e){
e.printStackTrace();
}
return RespBean.error("代码生成失败");
}

其实就是读取上面新增的模板信息并生成对应的文件,因此实际上新增的代码只有下图所示的内容:

新建测试项目

运行项目,然后按照之前的操作点击生成对应的代码。接下来我们创建一个项目,来使用这个代码生成器生成的代码,看看生成的代码是否有效。注意由于你指定了包的名称,因此新建项目的包名称也就确定了:

因此新建一个名为test的项目,前缀就是com.kenbingthoughts,之后在POM文件中引入Web、MySQL和Mybatis依赖:

然后在其application.properties配置文件中新增如下配置项,即用户名、密码和连接地址:

1
2
3
4
spring.datasource.username=root
spring.datasource.password=root1234
spring.datasource.url=jdbc:mysql://localhost:3306/code-generator?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
server.port=8081

由于此处我们的Mapper接口和XML文件放在一起,因此需要在其POM文件中新增如下配置项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<!--Mapper文件和XML放在一起需要添加-->
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
</resource>
</resources>
</build>

接着启动项目,依次访问controller中的接口,可以看到接口返回正常值,这就说明生成的代码是正确的:

小结

本篇利用SpringBoot+Vue+FreeMarker实现了一个简易版的代码生成器,其实它还有很多可以改进的地方,如可以显示数据表中的字段信息详情,支持用户修改字段所对应的实体属性信息,页面布局优化,生成的地址固定等,这些等后期有空会进行升级。