手写简易版代码生成器
# 需求分析
- 后台系统有一些业务逻辑是相似的,如单表的增删改查,用代码生成器快速生成重复的代码,生成后再小范围修改
- CMS系统中,有些页面访问量很大,如果都从数据库或者缓存里面查,会有性能上的下降,如果数据很少修改,则可以直接将数据生成静态页面。
# 代码生成器原理
利用模板语言的静态化功能,如freemarker, 数据+模板=静态文件
- 用户填写的数据库信息,工程搭建信息等等需要构造到实体类对象中方便操作
- 数据库表信息,数据库字段信息需要构造到实体类中
- 构造Freemarker数据模型,将数据库表对象和基本配置存入到Map集合中
- 借助Freemarker完成代码生成
- 自定义公共代码模板
# 知识点:
字符串的相关操作,文件相关操作、配置文件的相关操作
# 难点
读取数据库的元数据(数据库、表、字段),然后根据一些生成规则,这里主要是表的生成规则,生成出符合业务需求的代码,常规业务场景有: 生成model、service、controller、sql、视图层(增加、编辑、列表)、数据的JSR303验证等等。 复杂一点的生成器,可以多表关联生成,如主子表、树形表等。
常规业务的代码生成如: 人人开源;
复杂的代码生成如:jeesite、 jeecg
等。
# 数据库元数据相关知识
# 数据库元数据(DatabaseMetaData
)
是由Connection对象通过getMetaData方法获取而来,主要封装了是对数据库本身的一些整体综合信息,例如数据库的产品名称,数据库的版本号,数据库的URL,是否支持事务等等。
以下有一些关于DatabaseMetaData
的常用方法:
- getDatabaseProductName:获取数据库的产品名称
- getDatabaseProductName:获取数据库的版本号
- getUserName:获取数据库的用户名
- getURL:获取数据库连接的URL
- getDriverName:获取数据库的驱动名称
- driverVersion:获取数据库的驱动版本号
- isReadOnly:查看数据库是否只允许读操作
- supportsTransactions:查看数据库是否支持事务
具体可以参考jdk1.8的文档CHM.
# 参数元数据(ParameterMetaData)
是由PreparedStatement对象通过getParameterMetaData方法获取而来,主要是针对PreparedStatement对象和其预编译的SQL命令语句提供一些信息,ParameterMetaData能提供 占位符参数的个数,获取指定位置占位符的SQL类型等等,以下有一些关于ParameterMetaData的常用方法:
- getParameterCount:获取预编译SQL语句中占位符参数的个数
@Test
public void test() throws Exception {
String sql = "select * from bs_user where id=?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setString(1, "1063705482939731968");
//获取ParameterMetaData对象
ParameterMetaData paramMetaData = pstmt.getParameterMetaData();
//获取参数个数
int paramCount = paramMetaData.getParameterCount();
System.out.println(paramCount);
}
2
3
4
5
6
7
8
9
10
11
# 结果集元数据(ResultSetMetaData):
是由ResultSet对象通过getMetaData方法获取而来,主要是针对由数据库执行的SQL脚本命令获取的结果集对象ResultSet中提供的一些信息,比如结果集中的列数、指定列的名称、指定 列的SQL类型等等,可以说这个是对于框架来说非常重要的一个对象。 以下有一些关于ResultSetMetaData的常用方法:
- getColumnCount:获取结果集中列项目的个数
- getColumnType:获取指定列的SQL类型对应于Java中Types类的字段
- getColumnTypeName:获取指定列的SQL类型
- getClassName:获取指定列SQL类型对应于Java中的类型(包名加类名)
参考文章:数据库元数据MetaData - fjdingsd - 博客园 (cnblogs.com) (opens new window)
# jdbc操作数据库
@Slf4j
public class DbUtil {
private static ThreadLocal<Connection> connectionTl = new ThreadLocal<Connection>();
private static Connection connection;
/**
* 初始化
* @param db
* @return
*/
public static Connection initDB(Db db) throws ClassNotFoundException, SQLException {
String driver = StrUtil.emptyToDefault(db.getDriver(), "com.mysql.cj.jdbc.Driver");
//
String url = StrUtil.format("jdbc:mysql://{}:{}/{}?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=CONVERT_TO_NULL&serverTimezone=Asia/Shanghai",
StrUtil.emptyToDefault(db.getHost(), "localhost"), ObjectUtil.defaultIfNull(db.getPort(), 3306), db.getDbname());
String username = db.getUsername();
String password = db.getPassword();
log.debug("数据库连接信息:"+db);
//classLoader,加载对应驱动
Class.forName(driver);
connection = (Connection) DriverManager.getConnection(url, username, password);
connectionTl.set(connection);
return connection;
}
public static Connection getConnection() {
return connection;
}
/**
* 获取所有的表名
* @param connection
* @return
* @throws SQLException
*/
public static List<String> getTableNames(Connection connection) throws SQLException {
DatabaseMetaData metaData = connection.getMetaData();
String catalog = connection.getCatalog();
ResultSet tables = metaData.getTables(catalog, null, null, null);
List<String> tableNameLists = new ArrayList<>();
while (tables.next()) {
String table_name = tables.getString("TABLE_NAME");
// 将表转成驼峰形式
String modelName = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, table_name);
tableNameLists.add(table_name);
}
log.debug(StrUtil.format("获取数据库:{} 的所有表格,{}",catalog,tableNameLists));
return tableNameLists;
}
/**
* 获取表的所有列
*
* @return
*/
public static List<ColumnClass> getColumns(String tableName) throws SQLException {
DatabaseMetaData metaData = connection.getMetaData();
// 找出表的所有列
//catalog 其实也就是数据库名
final String catalog = connection.getCatalog();
ResultSet columns = metaData.getColumns(catalog, null, tableName, null);
// 找出表的主键,有可能有多个
ResultSet primaryKeys = metaData.getPrimaryKeys(catalog, null, tableName);
ArrayList<ColumnClass> columnLists = Lists.newArrayList();
while (columns.next()) {
String column_name = columns.getString("COLUMN_NAME");
String type_name = columns.getString("TYPE_NAME");
String remarks = columns.getString("REMARKS");
ColumnClass columnClass = new ColumnClass();
columnClass.setRemark(remarks);
columnClass.setColumnName(column_name);
columnClass.setType(type_name);
columnClass.setPropertyName(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, column_name));
columnClass.setIsPrimary(false);
columnLists.add(columnClass);
}
while (primaryKeys.next()) {
String pkName = primaryKeys.getString("COLUMN_NAME");
if (StrUtil.isNotBlank(pkName)) {
columnLists.stream().forEach(columnClass -> {
if (pkName.equals(columnClass.getColumnName())) {
columnClass.setIsPrimary(true);
}
});
}
}
log.debug(StrUtil.format("获取{}数据表:{} 的所有列",catalog,tableName));
return columnLists;
}
public static void main(String[] args) throws SQLException, ClassNotFoundException {
Db db = new Db();
db.setDriver("");
db.setPort(3306);
db.setHost("localhost");
db.setDbname("nacos");
db.setUsername("root");
db.setPassword("123456");
Connection connection = initDB(db);
List<String> tableNames = getTableNames(connection);
List<ColumnClass> configInfo = getColumns("his_config_info");
System.out.println(tableNames);
System.out.println(configInfo);
}
}
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
参考文章:
通过jdbc获取数据库中的表结构 主键 各个表字段类型及应用生成实体类 - 若 ♂ 只如初见 - 博客园 (cnblogs.com) (opens new window)
# 写模板
有freemarker的模板,也有vm模板,如果生成mybatis的model和xml,需要判断很多条件。
package ${packageName}.service;
import ${packageName}.model.${modelName};
import ${packageName}.mapper.${mapperName};
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
@Service
public class ${serviceName}{
@Autowired
${mapperName} ${mapperName?uncap_first};
public List<${modelName}> getAll${modelName}s(){
return ${mapperName?uncap_first}.getAll${modelName}s();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# freemarker用到的指令
字符串使用的内建函数:
- html: 字符串中所有的特殊 HTML 字符都需要用实体引用来代替(比如<代替<)。
- cap_first:字符串的第一个字母变为大写形式
- lower_case:字符串的小写形式
- upper_case:字符串的大写形式
- trim:去掉字符串首尾的空格
${column.propertyName?uncap_first}
# 写生成代码的业务
private void generate(Template template, TableClass tableClass, String path) throws IOException, TemplateException {
File folder = new File(path);
if (!folder.exists()) {
folder.mkdirs();
}
String fileName = path + "/" + tableClass.getModelName() + template.getName().replace(".ftl", "").replace("Model", "");
@Cleanup
FileOutputStream fos = new FileOutputStream(fileName);
@Cleanup
OutputStreamWriter out = new OutputStreamWriter(fos);
template.process(tableClass,out);
}
2
3
4
5
6
7
8
9
10
11
12
# 安装ojdbc6驱动
[ojdbc6最新版下载](Central Repository: com/oracle/database/jdbc/ojdbc6 (maven.org) (opens new window))
mvn install:install-file -Dfile=ojdbc6-11.2.0.4.jar -DgroupId=com.oracle -DartifactId=ojdbc6 -Dversion=11.2.0.4 -Dpackaging=jar
<dependency
<groupId>com.oracle</groupId>
<artifactId>ojdbc6</artifactId>
<version>11.2.0.4</version>
</dependency>
2
3
4
5
6
7
# 数据类型的一些坑
数据库里面的字段类型,要对应成model属性里面的java类型,在mybatis当中,可能还与mybatis的jdbyType类型不一样,也要转换对应,如INT字段类型,mybatis里面就没有
BIGINT UNSIGNED
、INT UNSIGNED
等LONGTEXT
长文本型
参考项目:
江南一点雨/generate_code (gitee.com) (opens new window)