打造一款适合自己的快速开发框架-代码生成器原理及实现

4,795 阅读9分钟

前言

关于代码生成器的原理,在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的实现,后续会考虑对其他数据库的支持实现。

项目源码地址

  • 后端

gitee.com/mldong/mldo…

  • 前端

gitee.com/mldong/mldo…

相关文章

打造一款适合自己的快速开发框架-先导篇

打造一款适合自己的快速开发框架-后端脚手架搭建

打造一款适合自己的快速开发框架-集成mapper

打造一款适合自己的快速开发框架-集成swaggerui和knife4j

打造一款适合自己的快速开发框架-通用类封装之统一结果返回、统一异常处理

打造一款适合自己的快速开发框架-业务错误码规范及实践

打造一款适合自己的快速开发框架-框架分层及CURD样例

打造一款适合自己的快速开发框架-mapper逻辑删除及枚举类型规范