前言
关于代码生成器的原理,在Python专栏里已经写过了,不过既然打造的是java版的快速开发框架,那么还是得在这里重新再写一篇,也使得这系列文章更全面一些。本次算是代码生成器原理及实现-java版。
定义
代码生成器:通过读取数据库元数据,然后定制模板,使用模板引擎输出自定义页面或文件的工具
关于数据库元数据
元数据,是指定义数据结构的数据。那么数据库元数据就是指定义数据库各类对象结构的数据 。下面以mysql为例,说明获取元数据的方式。
查询数据库名称为db_name的所有表
SELECT t.table_catalog,t.table_schema,t.table_name,table_type FROM information_schema.TABLES t where t.table_schema='db_name'
查询数据库名称为db_name,表名为tb_name的表
SELECT t.table_catalog,t.table_schema,t.table_name,table_type FROM information_schema.TABLES t where t.table_schema='db_name' and t.table_name = 'tb_name'
查询数据库名称为db_name,以sys_开头的表
SELECT t.table_catalog,t.table_schema,t.table_name,table_type FROM information_schema.TABLES t where t.table_schema='db_name' and t.table_name like 'sys_%'
常用字段说明
字段 | 说明 |
---|---|
table_catalog | 数据表登记目录 |
table_schema | 数据表所属的数据库名 |
table_name | 表名称 |
table_type | 表类型 |
获取表注释
show table status where NAME='tb_name'
常用字段说明
字段 | 说明 |
---|---|
name | 表名称 |
comment | 注释 |
获取表主键
select k.column_name from information_schema.table_constraints t
join information_schema.key_column_usage k
using (constraint_name,table_schema,table_name)
where t.constraint_type='PRIMARY KEY'
and t.table_schema='db_name' and t.table_name='tb_name'
获取某个表的所有列
SELECT t.table_schema,t.table_name,t.column_name,t.column_default,
t.is_nullable,t.data_type,t.character_maximum_length,t.numeric_precision,
t.numeric_scale,t.column_type,t.column_key, t.column_comment,t.extra
FROM information_schema.columns t
WHERE t.table_schema = 'db_name' AND t.table_name = 'tb_name'
常用字段说明
字段 | 说明 |
---|---|
table_schema | 所属的数据库名 |
table_name | 表名称 |
column_name | 列名 |
column_default | 默认值 |
is_nullable | 是否为空(YES->是,NO->否) |
data_type | 数据类型(char/varchar/int/datetime等无长度说明的定义) |
character_maximum_length | 字符串类型定义的长度 |
numeric_precision | 数值类型整型部分 |
numeric_scale | 数值类型小数点部分 |
column_type | 类似这种char(36)/int(6)有长度的定义 |
column_key | 约束条件(PRI->主键约束,UNI->唯一约束,MUL可以重复) |
column_comment | 字段注释 |
extra | auto_increment=>自增 |
元数据转换
元数据转换即将元数据转换为更为清晰的数据结构,以方便模板引擎模板语法使用。下面使用json结构描述转换后的数据结构(单表样例)。
{
"tableName": "tb_name", // 表名称
"tableCameName": "tbName", // 表小驼峰名(由表名称转换)
"tableType": "TABLE", // 表类型
"tableAlias":"xxx", // 表别名(ui层输入)
"remark": "表注释", // 表注释
"entityName": "TbName", // 实体类名称(由表名称转换)
"className": "TbName", // java类名称(由表名称转换)
"schema": "db_name", // 表所属数据库
"primaryKeys": [{ // 主键列
"columnName": "id", // 列名
"remark": "主键" // 字段注释
... // 如下同columns
}],
"columns": [{ // 表所有列
"tableName":"tb_name", // 表名
"columnName": "id", // 列名
"propertyName":"id", // 属性名
"primaryKey": true, // 是否为主键
"foreignKey": false, // 是否为外键(保留,尚未实现)
"size": 10, // 对应长度
"decimalDigits": 0 , // 小数点部分
"nullable": false, // 是否为空
"autoincrement": false, // 是否默认自增(保留,尚未实现)
"defaultValue": '', // 默认值
"remark": "主键",
"javaType": "String", // java对应数据类型(保留,py不实现)
"fullJavaType": "java.lang.String", // 同上
"dataType":"varchar", // 字段数据类型
"bigDecimal": false, // 同上
"setterMethodName": "setId", /// java set方法
"getterMethodName": "getId", // java get 方法
"date": false, // 是否日期类型
"treeProperty": false // 是否为id/parent_id模型(树)
},{
"columnName": "name", // 列名
"propertyName":"name", // 属性名
"primaryKey": false, // 是否为主键
"foreignKey": false, // 是否为外键(保留,尚未实现)
"size": 64, // 对应长度
"decimalDigits": 0 , // 小数点部分
"nullable": false, // 是否为空
"autoincrement": false, // 是否默认自增
"defaultValue": '', // 默认值
"remark": "名称",
"javaType": "String", // java对应数据类型(保留,py不实现)
"fullJavaType": "java.lang.String", // 同上
"dataType":"varchar", // 字段数据类型
"bigDecimal": false, // 同上
"setterMethodName": "setId", /// java set方法 (由列名转换)
"getterMethodName": "getId", // java get 方法 (由列名转换)
"date": false, // 是否日期类型
"treeProperty": false // 是否为id/parent_id模型(树)
}]
}
如下是sys_role表元数据-传入模板引擎的数据(其中表实体和列实体都各自新增了一些特殊处理的方法,以方便模板引擎判断逻辑新增的。)
表结构
CREATE TABLE `sys_role` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`create_time` datetime(3) DEFAULT NULL COMMENT '创建时间',
`update_time` datetime(3) DEFAULT NULL COMMENT '更新时间',
`is_deleted` tinyint(1) unsigned DEFAULT '1' COMMENT '是否删除(1->未删除|YES,2->已删除|NO)',
`name` varchar(64) CHARACTER SET utf8mb4 NOT NULL COMMENT '角色名称',
`role_key` varchar(32) DEFAULT NULL COMMENT '角色标识(唯一)',
`role_type` int(6) DEFAULT '10' COMMENT '角色类型(10->管理员|ADMIN,20->流程审核员|WORKFLOW)',
`is_enabled` tinyint(1) DEFAULT '2' COMMENT '是否启用(1->禁用|NO,2->启用|YES)',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT COMMENT='角色';
生成转换后的元数据
{
"moduleDesc": "系统管理",
"basePackage": "com.mldong",
"moduleName": "sys",
"targetProject": "D:/mldong/couse-workspace/mldong/",
"table": {
"tableName": "sys_role",
"tableCameName": "sysRole",
"tableType": "BASE TABLE",
"remark": "角色",
"entityName": "SysRole",
"primaryKeys": [
{
"columnName": "id",
"primaryKey": true,
"foreignKey": false,
"size": 20,
"decimalDigits": 0,
"nullable": false,
"autoincrement": true,
"remark": "主键",
"dataType": "bigint",
"javaProperty": "id",
"javaType": "Long",
"fullJavaType": "java.lang.Long",
"codedTypes": []
}
],
"columns": [
{
"columnName": "id",
"primaryKey": true,
"foreignKey": false,
"size": 20,
"decimalDigits": 0,
"nullable": false,
"autoincrement": true,
"remark": "主键",
"dataType": "bigint",
"javaProperty": "id",
"javaType": "Long",
"fullJavaType": "java.lang.Long",
"codedTypes": []
},
{
"columnName": "create_time",
"primaryKey": false,
"foreignKey": false,
"size": 0,
"decimalDigits": 0,
"nullable": true,
"autoincrement": false,
"remark": "创建时间",
"dataType": "datetime",
"javaProperty": "createTime",
"javaType": "Date",
"fullJavaType": "java.util.Date",
"codedTypes": []
},
{
"columnName": "update_time",
"primaryKey": false,
"foreignKey": false,
"size": 0,
"decimalDigits": 0,
"nullable": true,
"autoincrement": false,
"remark": "更新时间",
"dataType": "datetime",
"javaProperty": "updateTime",
"javaType": "Date",
"fullJavaType": "java.util.Date",
"codedTypes": []
},
{
"columnName": "is_deleted",
"primaryKey": false,
"foreignKey": false,
"size": 3,
"decimalDigits": 0,
"nullable": true,
"autoincrement": false,
"defaultValue": "1",
"remark": "是否删除(1->未删除|YES,2->已删除|NO)",
"dataType": "tinyint",
"javaProperty": "isDeleted",
"javaType": "YesNoEnum",
"fullJavaType": "java.lang.Integer",
"codedTypes": []
},
{
"columnName": "name",
"primaryKey": false,
"foreignKey": false,
"size": 64,
"decimalDigits": 0,
"nullable": false,
"autoincrement": false,
"remark": "角色名称",
"dataType": "varchar",
"javaProperty": "name",
"javaType": "String",
"fullJavaType": "java.lang.String",
"codedTypes": []
},
{
"columnName": "role_key",
"primaryKey": false,
"foreignKey": false,
"size": 32,
"decimalDigits": 0,
"nullable": true,
"autoincrement": false,
"remark": "角色标识(唯一)",
"dataType": "varchar",
"javaProperty": "roleKey",
"javaType": "String",
"fullJavaType": "java.lang.String",
"codedTypes": []
},
{
"columnName": "role_type",
"primaryKey": false,
"foreignKey": false,
"size": 10,
"decimalDigits": 0,
"nullable": true,
"autoincrement": false,
"defaultValue": "10",
"remark": "角色类型(10->管理员|ADMIN,20->流程审核员|WORKFLOW)",
"dataType": "int",
"javaProperty": "roleType",
"javaType": "RoleTypeEnum",
"fullJavaType": "java.lang.Integer",
"codedTypes": [
{
"value": 10,
"remark": "管理员",
"name": "ADMIN"
},
{
"value": 20,
"remark": "流程审核员",
"name": "WORKFLOW"
}
]
},
{
"columnName": "is_enabled",
"primaryKey": false,
"foreignKey": false,
"size": 3,
"decimalDigits": 0,
"nullable": true,
"autoincrement": false,
"defaultValue": "2",
"remark": "是否启用(1->禁用|NO,2->启用|YES)",
"dataType": "tinyint",
"javaProperty": "isEnabled",
"javaType": "YesNoEnum",
"fullJavaType": "java.lang.Integer",
"codedTypes": []
},
{
"columnName": "remark",
"primaryKey": false,
"foreignKey": false,
"size": 255,
"decimalDigits": 0,
"nullable": true,
"autoincrement": false,
"remark": "备注",
"dataType": "varchar",
"javaProperty": "remark",
"javaType": "String",
"fullJavaType": "java.lang.String",
"codedTypes": []
}
],
"catalog": "def",
"schema": "mldong"
}
}
关于模板引擎
-
java可以考虑:Freemarker、Thymeleaf、Velocity等。本文这里使用的是Freemarker。
-
nodejs可以考虑:ejs、art-template
-
python可以考虑:Jinja2
关于配置化文件
样例配置,使用yaml
database:
# jdbc驱动
driverClass: "com.mysql.cj.jdbc.Driver"
# 数据库地址
url: "jdbc:mysql://localhost:3306/mldong?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai"
# 数据库名称
dbName: "mldong"
# 用户名
username: "root"
# 密码
password: ""
# 包名
basePackage: "com.mldong"
# 作者
author: "mldong"
# 生成代码目标目录
targetProject: "D:/mldong/couse-workspace/mldong/"
# 模块名
moduleName: "sys"
# 模块描述
moduleDesc: "系统管理"
tables:
- tableName: "sys_role"
templates:
# 模板名称
- name: "实体类"
# 是否选中,选中则会生成对应代码
selected: true
# 文件存在是否覆盖
covered: true
# 模板文件名称
templateFile: "entity.ftl"
# 代码生成目录
targetPath: "mldong-mapper/src/main/java/${basePackage}/modules/${moduleName}/entity/"
# 生成文件名(同上需要占位符,代码中要转换)
targetFileName: "${table.className}.java"
# 生成文件编码
encoding: "utf-8"
处理流程
- 第一步:加载配置文件
- 第二步:读取数据库元数据
- 第三步:元数据转换
- 第四步:组装模板数据
- 第五步:使用模板引擎生成代码
开始编码
目录结构
├── mldong-admin 管理端接口
├── mldong-common 工具类及通用代码
├── mldong-generator 代码生成器
├── src/main/java
├──com.mldong.common.enums
└── ErrorEnum.java 代码生成器相关错误码
├── com.mldong.common.exception
└── GeneratorException.java 代码生成器异常类
├── com.mldong.common.util
└── StringUtil.java 字符串处理工具
├── com.mldong.generator
├── config
├── model
├── ConfigModel.java 配置模型,对应config.yml文件
├── DbConfigModel.java 数据库配置模型,对应config.yml的database
├── TableConfigModel.java 表配置模型,对应config.yml的tables[]
└── TemplateConfigMOdel.java 模板配置模型,对应config.yml的templates[]
└── GengeratorConfig.java 配置处理类,将config.yml转成配置模型对象
├── core
├── impl
MysqlDataBase.java 元数据获取,mysql实现类
├── model
├── CodedType.java 特殊注释转换=>字典类型实体
├── Column.java 列对象模型
├── Table.java 表对象模型
└── DataBase.java 元数据获取接口定义
├── engine
├── FreemarkerImpl.java Freemarker模板引擎实现
├── StringTemplateLoader.java 字符串模板处理类
└── TemplateEngine.java 模板引擎接口
└── Generator.java 代码生成主函数
└── src/main/resources
├── templates 模板目录
├── controller.ftl 控制层模型
├── dto.ftl 控制层参数实体模板
├── entity.ftl 持久层实体类模板
├── mapper.ftl 持久层模板
├── mapperXml.ftl 持久层xml模板
├── service.ftl 业务接口类模板
└── serviceImpl.ftl 业务实现类模板
├── config.yml 代码生成配置文件
└── dataType.yml 数据类型转换配置文件
核心文件说明:
mldong-generator/src/main/java/com/mldong/generator/config/GeneratorConfig.java
配置文件处理类,这里主要使用jyaml将config.yml和dataType.yml转成对应的java对象
package com.mldong.generator.config;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;
import org.ho.yaml.Yaml;
import com.mldong.generator.config.model.ConfigModel;
/**
* 配置文件处理类
* @author mldong
*
*/
public class GeneratorConfig {
/**
* 默认的代码生成配置文件名
*/
private static final String CONFIG_FILE = "config.yml";
/**
* 默认的数据类型配置文件名
*/
private static final String DATA_TYPE_FILE = "dataType.yml";
/**
* 配置模型(config.yml)
*/
private ConfigModel configModel;
/**
* 代码生成配置文件
*/
private String configPath;
/**
* 数据类型配置文件
*/
private String dataTypePath;
/**
* 类型转换(dataType.yml)
*/
private Map<String,Map<String,String>> dataType;
public GeneratorConfig(String configPath,String dataTypePath){
File configFile = new File(configPath);
if(configFile.isDirectory()) {
this.configPath = configPath+CONFIG_FILE;
} else {
this.configPath = configPath;
}
File dataTypeFile = new File(dataTypePath);
if(dataTypeFile.isDirectory()) {
this.dataTypePath = configPath+DATA_TYPE_FILE;
} else {
this.dataTypePath = dataTypePath;
}
}
/**
* 加载配置文件
* @throws FileNotFoundException
*/
@SuppressWarnings("unchecked")
public void loadConfig() throws FileNotFoundException {
File dataTypeFile = new File(dataTypePath);
this.dataType = Yaml.loadType(dataTypeFile,HashMap.class);
File configFile = new File(configPath);
this.configModel = Yaml.loadType(configFile, ConfigModel.class);
}
public Map<String, Map<String, String>> getDataType() {
return dataType;
}
public ConfigModel getConfigModel() {
return configModel;
}
}
mldong-generator/src/main/java/com/mldong/generator/core/DataBase.java
元数据获取接口定义
package com.mldong.generator.core;
import java.sql.Connection;
import java.util.List;
import com.mldong.generator.core.model.Column;
import com.mldong.generator.core.model.Table;
/**
* 数据库元数据相关接口
* @author mldong
*
*/
public interface DataBase {
/**
* 获取数据库连接对象
* @return 数据库连接对象
*/
public Connection getConnection();
/**
* 关闭数据库连接对象
*/
public void closeConnection();
/**
* 获取所有表对象
* @param tableNamePattern (sys_% 所有以sys_开头的表 )
* @return 表对象实例
*/
public List<Table> getTables(String tableNamePattern);
/**
* 获取所有的表名
* @param tableNamePattern
* @return
*/
public List<String> getTableNames(String tableNamePattern);
/**
* 根据表名获取数据表对象
*
* @param tableName 表名
* @return 表对象实例
*/
public Table getTable(String tableName);
/**
* 获取所有表内的全部列表
* @param tableName 表名
* @return 数据列对象
*/
public List<Column> getColumns(String tableName);
/**
* 获取所有表所有主键表
* @param tableName 表名
* @return
*/
public List<Column> getPrimaryKeys(String tableName);
/**
* 获取表备注信息
*
* @param tableName 表名
* @return 表备注信息
*/
public String getTableComment(String tableName);
}
mldong-generator/src/main/java/com/mldong/generator/core/impl/MysqlDataBase.java
元数据获取-mysql实现类
package com.mldong.generator.core.impl;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.mldong.common.enums.ErrorEnum;
import com.mldong.common.exception.GeneratorException;
import com.mldong.common.util.StringUtil;
import com.mldong.generator.config.GeneratorConfig;
import com.mldong.generator.config.model.ConfigModel;
import com.mldong.generator.core.DataBase;
import com.mldong.generator.core.model.CodedType;
import com.mldong.generator.core.model.Column;
import com.mldong.generator.core.model.Table;
/**
* 元数据获取-mysql实现类
* @author mldong
*
*/
public class MysqlDataBase implements DataBase{
/**
* 查询表名
*/
private final String SELECT_TABLE_NAME_SQL = "SELECT t.table_catalog,t.table_schema,t.table_name,table_type FROM information_schema.TABLES t where t.table_schema=? and t.table_name like ?";
/**
* 查询表元数据
*/
private final String SELECT_TABLE_SQL = "SELECT t.table_catalog,t.table_schema,t.table_name,table_type FROM information_schema.TABLES t where t.table_schema=? and t.table_name = ?";
/**
* 查询某个表的所有列
*/
private final String SELECT_TABLE_COLUMN_SQL = "SELECT t.table_schema,t.table_name,t.column_name,t.column_default, t.is_nullable,t.data_type,t.character_maximum_length,t.numeric_precision,t.numeric_scale,t.column_type,t.column_key, t.column_comment,t.extra FROM information_schema.columns t WHERE t.table_schema = ? AND t.table_name = ?";
/**
* 查询表注释
*/
private final String SELECT_TABLE_REMARK_SQL = "show table status where NAME=?";
private ConfigModel configModel;
private Connection connection;
private GeneratorConfig generatorConfig;
public MysqlDataBase(GeneratorConfig generatorConfig) {
this.generatorConfig = generatorConfig;
this.configModel = generatorConfig.getConfigModel();
}
@Override
public Connection getConnection() {
try {
if (connection == null || connection.isClosed()) {
Connection conn = null;
try {
Class.forName(this.configModel.getDatabase().getDriverClass());
conn = DriverManager.getConnection(
this.configModel.getDatabase().getUrl(),
this.configModel.getDatabase().getUsername(),
this.configModel.getDatabase().getPassword());
} catch (Exception e) {
e.printStackTrace();
}
return conn;
}
} catch (SQLException e) {
e.printStackTrace();
}
return connection;
}
@Override
public void closeConnection() {
try {
if (!connection.isClosed()) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public List<Table> getTables(String tableNamePattern) {
List<String> tableNames = getTableNames(tableNamePattern);
List<Table> tables = new ArrayList<Table>();
for (String tableName : tableNames) {
tables.add(getTable(tableName));
}
return tables;
}
@Override
public List<String> getTableNames(String tableNamePattern) {
Connection connection = getConnection();
try {
PreparedStatement ps = connection.prepareStatement(SELECT_TABLE_NAME_SQL);
ps.setString(1, connection.getCatalog());
ps.setString(2, tableNamePattern);
ResultSet rs = ps.executeQuery();
List<String> tableNames = new ArrayList<String>();
while (rs.next()) {
String tableName = rs.getString("TABLE_NAME");
tableNames.add(tableName);
}
return tableNames;
} catch (Exception e) {
e.printStackTrace();
}
throw new GeneratorException(ErrorEnum.notGetTable);
}
@Override
public Table getTable(String tableName) {
Connection connection = getConnection();
Table table = new Table();
try {
PreparedStatement ps = connection.prepareStatement(SELECT_TABLE_SQL);
ps.setString(1, connection.getCatalog());
ps.setString(2, tableName);
ResultSet rs = ps.executeQuery();
if (rs.next()) {
table.setTableName(tableName);
table.setTableCameName(StringUtil.getCamelCaseString(tableName,
false));
table.setEntityName(StringUtil.getCamelCaseString(tableName,
true));
table.setCatalog(rs.getString("table_catalog"));
table.setSchema(rs.getString("table_schema"));
table.setTableType(rs.getString("TABLE_TYPE"));
table.setRemark(getTableComment(tableName));
table.setColumns(getColumns(tableName));
table.setPrimaryKeys(getPrimaryKeys(tableName));
}
} catch (Exception e) {
e.printStackTrace();
}
return table;
}
@Override
public List<Column> getColumns(String tableName) {
Connection connection = getConnection();
try {
PreparedStatement ps = connection.prepareStatement(SELECT_TABLE_COLUMN_SQL);
ps.setString(1, connection.getCatalog());
ps.setString(2, tableName);
ResultSet rs = ps.executeQuery();
List<Column> columns = new ArrayList<Column>();
while (rs.next()) {
columns.add(getColumn(rs));
}
return columns;
} catch (Exception e) {
e.printStackTrace();
}
throw new GeneratorException(ErrorEnum.notGetColumn);
}
/**
* 单个列详细信息
* @param rs
* @return
* @throws SQLException
*/
private Column getColumn(ResultSet rs) throws SQLException {
try {
Column column = new Column();
// 列名
String columnName = rs.getString("COLUMN_NAME");
// 表名
// String tableName = rs.getString("TABLE_NAME");
column.setColumnName(columnName);
int size = rs.getInt("CHARACTER_MAXIMUM_LENGTH");
column.setSize(size==0?rs.getInt("NUMERIC_PRECISION"):size);
column.setNullable("YES".equals(rs.getString("IS_NULLABLE")));
column.setDefaultValue(rs.getString("COLUMN_DEFAULT"));
column.setDataType(rs.getString("DATA_TYPE"));
column.setAutoincrement("auto_increment".equals(rs.getString("EXTRA")));
column.setRemark(rs.getString("COLUMN_COMMENT"));
column.setDecimalDigits(rs.getInt("NUMERIC_SCALE"));
column.setJavaProperty(StringUtil.getCamelCaseString(columnName,
false));
String dataType = column.getDataType().toUpperCase();
Map<String,String> map = generatorConfig.getDataType().get(dataType);
if(null!=map) {
column.setJavaType(map.get("javaType"));
column.setFullJavaType(map.get("fullJavaType"));
} else {
column.setJavaType("String");
column.setFullJavaType("java.lang.String");
}
String columnKey = rs.getString("COLUMN_KEY");
column.setPrimaryKey("PRI".equals(columnKey));
column.setForeignKey("MUL".equals(columnKey));
if(column.getColumnName().startsWith("is_")) {
column.setJavaType("YesNoEnum");
} else {
if(StringUtil.isNotEmpty(column.getRemark())) {
// 不为空,判断是否为类型注解
column.setCodedTypes(remarkToCodedType(column.getRemark()));
if(!column.getCodedTypes().isEmpty()){
column.setJavaType(StringUtil.getCamelCaseString(columnName,
true)+"Enum");
}
}
}
return column;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
@Override
public List<Column> getPrimaryKeys(String tableName) {
return getColumns(tableName).stream().filter(item->{
return item.isPrimaryKey();
}).collect(Collectors.toList());
}
@Override
public String getTableComment(String tableName) {
Connection connection = getConnection();
try {
PreparedStatement ps = connection.prepareStatement(SELECT_TABLE_REMARK_SQL);
ps.setString(1, tableName);
ResultSet rs = ps.executeQuery();
String remark = "";
if (rs.next()) {
remark = rs.getString("COMMENT");
}
return remark;
} catch (Exception e) {
e.printStackTrace();
}
throw new GeneratorException(ErrorEnum.notGetComment);
}
Pattern pattern = Pattern.compile("(\\d+)->(.+?)\\|([a-zA-Z_0-9]+?)[,)]");
/**
* 注释转codedtype
* @param remark
* @return
*/
private List<CodedType> remarkToCodedType(String remark) {
List<CodedType> list = new ArrayList<>();
Matcher matcher = pattern.matcher(remark);
while(matcher.find()) {
list.add(new CodedType(Integer.parseInt(matcher.group(1)), matcher.group(2),matcher.group(3)));
}
return list;
}
}
mldong-generator/src/main/java/com/mldong/generator/engine/TemplateEngine.java
模板引擎接口
package com.mldong.generator.engine;
import java.util.Map;
import com.mldong.generator.config.model.TemplateConfigModel;
/**
* 模板引擎接口
* @author mldong
*
*/
public interface TemplateEngine {
/**
* 解析字符串
* @param model
* @param stringTemplate
* @return
*/
public String processToString(Map<String, Object> model,
String stringTemplate);
/**
* 解析文件
* @param model
* @param templateConfigModel
*/
public void processToFile(Map<String, Object> model,
TemplateConfigModel templateConfigModel);
}
mldong-generator/src/main/java/com/mldong/generator/engine/TemplateEngine.java
字符串模板加载类
package com.mldong.generator.engine;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import freemarker.cache.TemplateLoader;
/**
* 字条串模板加载
* @author mldong
*
*/
public class StringTemplateLoader implements TemplateLoader {
private String template;
public StringTemplateLoader(String template) {
this.template = template;
if (template == null) {
this.template = "";
}
}
public void closeTemplateSource(Object templateSource) throws IOException {
((StringReader) templateSource).close();
}
public Object findTemplateSource(String name) throws IOException {
return new StringReader(template);
}
public long getLastModified(Object templateSource) {
return 0;
}
public Reader getReader(Object templateSource, String encoding)
throws IOException {
return (Reader) templateSource;
}
}
mldong-generator/src/main/java/com/mldong/generator/engine/FreeMarkerImpl.java
模板引擎freemarker实体类
package com.mldong.generator.engine;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.mldong.common.enums.ErrorEnum;
import com.mldong.common.exception.GeneratorException;
import com.mldong.common.util.StringUtil;
import com.mldong.generator.config.model.TemplateConfigModel;
import freemarker.template.Configuration;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.Template;
/**
* 模板引擎解析-freemark实现
*
* @author mldong
*
*/
public class FreeMarkerImpl implements TemplateEngine {
private static final Logger LOGGER = LoggerFactory.getLogger(FreeMarkerImpl.class);
private static final String DEFAULT_ENCODING = "UTF-8";
private Configuration config;
private String classPath;
public FreeMarkerImpl(String classPath) {
this.classPath = classPath;
initConfiguration();
}
public void initConfiguration() {
try {
try {
config = new Configuration(Configuration.VERSION_2_3_28);
config.setDirectoryForTemplateLoading(new File(classPath));
config.setObjectWrapper(new DefaultObjectWrapper(Configuration.DEFAULT_INCOMPATIBLE_IMPROVEMENTS));
config.setSetting("classic_compatible", "true");
config.setSetting("whitespace_stripping", "true");
config.setSetting("template_update_delay", "1");
config.setSetting("locale", "zh_CN");
config.setSetting("default_encoding", DEFAULT_ENCODING);
config.setSetting("url_escaping_charset", DEFAULT_ENCODING);
config.setSetting("datetime_format", "yyyy-MM-dd hh:mm:ss");
config.setSetting("date_format", "yyyy-MM-dd");
config.setSetting("time_format", "HH:mm:ss");
config.setSetting("number_format", "0.######;");
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public String processToString(Map<String, Object> model,
String stringTemplate) {
try {
Configuration cfg = new Configuration(Configuration.VERSION_2_3_28);
cfg.setTemplateLoader(new StringTemplateLoader(stringTemplate));
cfg.setDefaultEncoding(DEFAULT_ENCODING);
Template template = cfg.getTemplate("");
StringWriter writer = new StringWriter();
template.process(model, writer);
return writer.toString();
} catch (Exception e) {
e.printStackTrace();
throw new GeneratorException(ErrorEnum.templateEngineProcess);
}
}
@Override
public void processToFile(Map<String, Object> model,
TemplateConfigModel templateConfigModel) {
try {
Template template = config.getTemplate(
templateConfigModel.getTemplateFile(),
templateConfigModel.getEncoding());
String targetPath = StringUtil
.packagePathToFilePath(processToString(model,
templateConfigModel.getTargetPath()));
String targetFileName = processToString(model,
templateConfigModel.getTargetFileName());
File file = new File(targetPath + File.separator + targetFileName);
// 文件存在且可覆盖 or 文件不存在==>代码生成
boolean isFileExists = file.exists();
if ((isFileExists && templateConfigModel.isCovered())
|| !isFileExists) {
File directory = new File(targetPath);
if (!directory.exists()) {
directory.mkdirs();
}
Writer out = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(file),
templateConfigModel.getEncoding()));
template.process(model, out);
out.flush();
out.close();
if(isFileExists) {
LOGGER.info("目标文件已重新生成覆盖:{}",file.getPath());
} else {
LOGGER.info("目标文件新生成:{}",file.getPath());
}
} else {
LOGGER.info("目标文件已存在,如需要重新生成,请先删除目标文件:{}",file.getPath());
}
} catch (Exception e) {
e.printStackTrace();
throw new GeneratorException(ErrorEnum.templateEngineProcess);
}
}
}
mldong-generator/src/main/java/com/mldong/generator/engine/FreeMarkerImpl.java
代码生成主函数
package com.mldong;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.Gson;
import com.mldong.generator.config.GeneratorConfig;
import com.mldong.generator.config.model.TableConfigModel;
import com.mldong.generator.config.model.TemplateConfigModel;
import com.mldong.generator.core.DataBase;
import com.mldong.generator.core.impl.MysqlDataBase;
import com.mldong.generator.core.model.Table;
import com.mldong.generator.engine.FreeMarkerImpl;
import com.mldong.generator.engine.TemplateEngine;
/**
* 代码生成主函数
* @author mldong
*
*/
public class Generator {
private static final Logger LOGGER = LoggerFactory.getLogger(Generator.class);
public static void main(String[] args) throws FileNotFoundException {
String configPath = "src/main/resources/config.yml";
String dataTypePath = "src/main/resources/dataType.yml";
String templateDir = "src/main/resources/templates";
GeneratorConfig config = new GeneratorConfig(configPath, dataTypePath);
// 加载配置
config.loadConfig();
DataBase dataBase = new MysqlDataBase(config);
// Gson gson = new GsonBuilder().setPrettyPrinting().create();
Gson gson = new Gson();
TemplateEngine templateEngine = new FreeMarkerImpl(templateDir);
// 要生成的表
List<TableConfigModel> tableConfigModelList = Arrays.asList(config.getConfigModel().getTables());
// 模板集合
List<TemplateConfigModel> templateConfigModelList = Arrays.asList(config.getConfigModel().getTemplates());
tableConfigModelList.forEach(tableConfigModel -> {
// 获取表
List<Table> tableList = dataBase.getTables(tableConfigModel.getTableName());
tableList.forEach(table -> {
LOGGER.info("元数据:{}",gson.toJson(table));
Map<String, Object> model = new HashMap<String, Object>();
String targetProject = config.getConfigModel().getTargetProject();
model.put("targetProject", targetProject);
model.put("basePackage", config.getConfigModel().getBasePackage());
model.put("moduleName", config.getConfigModel().getModuleName());
model.put("moduleDesc", config.getConfigModel().getModuleDesc());
model.put("table", table);
templateConfigModelList.forEach(templateConfigModel -> {
// 选中的才能生成代码
if(templateConfigModel.isSelected()) {
if(!templateConfigModel.getTargetPath().contains(targetProject)){
templateConfigModel.setTargetPath(targetProject+File.separator+templateConfigModel.getTargetPath());
}
templateEngine.processToFile(model, templateConfigModel);
}
});
});
});
}
}
mldong-generator/src/main/resources/config.yml
database:
# jdbc驱动
driverClass: "com.mysql.cj.jdbc.Driver"
# 数据库地址
url: "jdbc:mysql://localhost:3306/mldong?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&serverTimezone=Asia/Shanghai"
# 数据库名称
dbName: "mldong"
# 用户名
username: "root"
# 密码
password: ""
# 包名
basePackage: "com.mldong"
# 作者
author: "mldong"
# 生成代码目标目录
targetProject: "D:/mldong/couse-workspace/mldong/"
# 模块名
moduleName: "sys"
# 模块描述
moduleDesc: "系统管理"
tables:
- tableName: "sys_role"
templates:
- name: "实体类"
selected: true
covered: true
templateFile: "entity.ftl"
targetPath: "mldong-mapper/src/main/java/${basePackage}/modules/${moduleName}/entity/"
targetFileName: "${table.className}.java"
encoding: "utf-8"
- name: "持久层类"
selected: true
covered: true
templateFile: "mapper.ftl"
targetPath: "mldong-mapper/src/main/java/${basePackage}/modules/${moduleName}/mapper/"
targetFileName: "${table.className}Mapper.java"
encoding: "utf-8"
- name: "持久层xml"
selected: true
covered: true
templateFile: "mapperXml.ftl"
targetPath: "mldong-mapper/src/main/resources/mapper/${moduleName}"
targetFileName: "${table.tableName}_mapper.xml"
encoding: "utf-8"
- name: "dto参数"
selected: true
covered: false
templateFile: "dto.ftl"
targetPath: "mldong-admin/src/main/java/${basePackage}/modules/${moduleName}/dto/"
targetFileName: "${table.className}Param.java"
encoding: "utf-8"
- name: "业务接口"
selected: true
covered: false
templateFile: "service.ftl"
targetPath: "mldong-admin/src/main/java/${basePackage}/modules/${moduleName}/service/"
targetFileName: "${table.className}Service.java"
encoding: "utf-8"
- name: "业务接口实现"
selected: true
covered: false
templateFile: "serviceImpl.ftl"
targetPath: "mldong-admin/src/main/java/${basePackage}/modules/${moduleName}/service/impl/"
targetFileName: "${table.className}ServiceImpl.java"
encoding: "utf-8"
- name: "控制层"
selected: true
covered: false
templateFile: "controller.ftl"
targetPath: "mldong-admin/src/main/java/${basePackage}/modules/${moduleName}/controller/"
targetFileName: "${table.className}Controller.java"
encoding: "utf-8"
mldong-generator/src/main/resources/dataType.yml
# 数值类型
TINYINT:
javaType: Integer
fullJavaType: java.lang.Integer
SMALLINT:
javaType: Integer
fullJavaType: java.lang.Integer
MEDIUMINT:
javaType: Integer
fullJavaType: java.lang.Integer
INT:
javaType: Integer
fullJavaType: java.lang.Integer
INTEGER:
javaType: Integer
fullJavaType: java.lang.Integer
BIGINT:
javaType: Long
fullJavaType: java.lang.Long
FLOAT:
javaType: Float
fullJavaType: java.lang.Float
DOUBLE:
javaType: Double
fullJavaType: java.lang.Double
DECIMAL:
javaType: BigDecimal
fullJavaType: java.math.BigDecimal
# 日期类型
DATE:
javaType: Date
fullJavaType: java.util.Date
TIME:
javaType: Date
fullJavaType: java.util.Date
YEAR:
javaType: Integer
fullJavaType: java.lang.Integer
DATETIME:
javaType: Date
fullJavaType: java.util.Date
TIMESTAP:
javaType: Timestamp
fullJavaType: java.sql.Timestamp
# 字符串类型
CHAR:
javaType: String
fullJavaType: java.lang.String
VARCHAR:
javaType: String
fullJavaType: java.lang.String
TINYBLOB:
javaType: "byte[]"
fullJavaType: "byte[]"
TINYTEXT:
javaType: String
fullJavaType: java.lang.String
BLOB:
javaType: "byte[]"
fullJavaType: "byte[]"
TEXT:
javaType: String
fullJavaType: java.lang.String
MEDIUMBLOB:
javaType: "byte[]"
fullJavaType: "byte[]"
MEDIUMTEXT:
javaType: String
fullJavaType: java.lang.String
LONGBLOB:
javaType: "byte[]"
fullJavaType: "byte[]"
LONGTEXT:
javaType: String
fullJavaType: java.lang.String
代码生成规则说明
类名 | 描述 | 规则说明 |
---|---|---|
controller.ftl | 控制层类 | 该层可由代码生成器生成,只生成一次,再次生成不能覆盖。(selected=true,covered=false) |
dto.ftl | 接收参数实体 | 该层可由代码生成器生成,只生成一次,再次生成不能覆盖。(selected=true,covered=false) |
service.ftl | 业务层接口 | 该层可由代码生成器生成,只生成一次,再次生成不能覆盖。(selected=true,covered=false) |
serviceImpl.ftl | 业务层接口实现 | 该层可由代码生成器生成,只生成一次,再次生成不能覆盖。(selected=true,covered=false) |
mapper.ftl | 持久层 | 该层可由代码生成器生成,每次变字段的变动,都需要重新生成一次,可覆盖。(selected=true,covered=true) |
entity.ftl | 实体类 | 该层可由代码生成器生成,每次变字段的变动,都需要重新生成一次,可覆盖。(selected=true,covered=true) |
mapperXml.ftl | 持久层对应的xml | 该层可由代码生成器生成,每次变字段的变动,都需要重新生成一次,可覆盖。(selected=true,covered=true) |
小结
用java写确实会比python代码多很多,但这和语言没有关系,java其实也可以像上次py那样就一个文件就处理完成,不过那样做并不是很友好,不好扩展。如果想这样做的同学也可以试一下,主要思路就是MysqlDataBase.java类上的四条sql,然后各种配置和元数据都可以使用map。
关于代码生成器规划
本次的代码生成器的元数据来源于数据库,如果要做到快速开发,仅这些元数据还是远远不够的,所以后期会考虑加上页面辅助收集如表单类型、校验规则等元数据。这个到后面的前端篇的时候才会展开。还有,这里只做了Mysql的实现,后续会考虑对其他数据库的支持实现。
项目源码地址
- 后端
- 前端
相关文章
打造一款适合自己的快速开发框架-集成swaggerui和knife4j