项目作用
数据库的正向工程,自动根据实体类维护表结构,支持H2和MySQL
项目前提
- 项目引入了
MyBatis-Plus
- 主键使用的是
MyBatis-Plus
的默认ID策略(雪花算法)
新增结构
AutoTable
表注解,使用在类上
import java.lang.annotation.*;
/**
* 指定实体类为表
* <p>
* 实体类中所有属性均为表中字段,下划线命名
* <p>
* 会自动增加表的主键,字段名为id,使用MyBatis-Plus的默认主键策略
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoTable {
/**
* 表名,默认为实体类名,自动转换下划线命名,自定义请遵循下划线命名
*/
String value() default "";
}
AutoTableColumn
列注解,使用在属性上
import java.lang.annotation.*;
/**
* 表列字段注解
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoTableColumn {
/**
* 表名,默认为字段名,驼峰命名自动转换下划线命名
*/
String value() default "";
/**
* 数据类型,不区分大小写,请与数据库中显示类型对应一致
* <p>
* 此处可扩展其他属性,如:
* <p>
* varchar(100) not null
* <p>
* int default 0
*/
String type() default "";
}
AutoTableColumnMap
import lombok.Data;
/**
* 数据库列字段映射
*/
@Data
public class AutoTableColumnMap {
/**
* 表名
*/
private String tableName;
/**
* 字段名
*/
private String columnName;
/**
* 数据类型
*/
private String dataType;
}
DDLMapper
import com.fan.autoTable.AutoTableColumnMap;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List;
/**
* DDL语句相关
*/
public interface DDLMapper {
/**
* 查询DDL结构
*/
@Select("${sql}")
List<AutoTableColumnMap> execSelectSql(String sql);
/**
* 执行SQL语句
*/
@Update("${sql}")
void execUpdateSql(String sql);
}
AutoTableTaskConfig
import com.fan.mapper.DDLMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
/**
* 自动维护表结构(项目启动执行)
* <p>
* 只做新增,不做修改
*/
@Slf4j
@Component
public class AutoTableTaskConfig implements ApplicationRunner {
@Resource
private Environment environment;
@Resource
private DDLMapper ddlMapper;
// 数据库字段查询语句
private static String COLUMN_SELECT_SQL =
"select table_name,column_name from information_schema.columns where table_schema=";
// 指定扫描的实体类包名
private String packName;
@Override
public void run(ApplicationArguments args) {
try {
System.out.println("┌─┐┬ ┬┌┬┐┌─┐ ┌┬┐┌─┐┌┐ ┬ ┌─┐");
System.out.println("├─┤│ │ │ │ │ │ ├─┤├┴┐│ ├┤ ");
System.out.println("┴ ┴└─┘ ┴ └─┘ ┴ ┴ ┴└─┘┴─┘└─┘");
System.out.println(" v1.0.0");
// 获取配置的数据库类型,仅支持H2、MySQL
String dbType = environment.getProperty("autoTable.dbType");
// 获取配置的数据库名称
String dbName = environment.getProperty("autoTable.dbName");
// 获取配置的实体类包位置
packName = environment.getProperty("autoTable.tablePack");
if (Objects.isNull(dbType)) {
log.info("表自动维护未指定数据库类型,任务结束");
return;
}
if (Objects.isNull(dbName)) {
log.info("表自动维护未指定数据库名称,任务结束");
return;
}
if (Objects.isNull(packName)) {
log.info("表自动维护未指定扫描包位置,任务结束");
return;
}
// 区分H2数据库和MySQL数据库查询方式
if (Objects.equals(dbType.toUpperCase(), "H2")) {
COLUMN_SELECT_SQL += "'PUBLIC'";
} else if (Objects.equals(dbType.toUpperCase(), "MYSQL")) {
COLUMN_SELECT_SQL += "'" + dbName + "'";
} else {
log.info("表自动维护不能支持该数据库类型,任务结束");
return;
}
long startTime = System.currentTimeMillis();
int res = deal();
if (res == 1) {
log.info("表结构维护完成,累计耗时:{}ms", System.currentTimeMillis() - startTime);
} else if (res == 2) {
log.info("表结构没有新增,累计耗时:{}ms", System.currentTimeMillis() - startTime);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 处理
*
* @return 0-失败、1-执行完毕、2-不需要执行
*/
private int deal() {
try {
// 获取表所有已有字段
long startTime = System.currentTimeMillis();
List<AutoTableColumnMap> dbColumnList = ddlMapper.execSelectSql(COLUMN_SELECT_SQL);
log.info("查询数据库已有表结构耗时:{}ms", System.currentTimeMillis() - startTime);
// 获取使用 AutoTable 注解类的集合
Set<Class<?>> classes = getAnnotationClasses(packName, AutoTable.class);
// 最后生成的 DDL SQL 语句
StringBuilder sql = new StringBuilder();
// 所有实体类的表名
ArrayList<String> tableNameList = new ArrayList<>();
// 数据库中存在的所有表名
List<String> dbTableNameList = dbColumnList.stream()
.map(AutoTableColumnMap::getTableName).collect(Collectors.toList());
for (Class<?> cls : classes) {
// 注解表名
AutoTable annotation = cls.getAnnotation(AutoTable.class);
// 表名,自定义优先,否则实体类名转下划线命名风格
String tableName = annotation.value().length() > 0
? annotation.value() : underscoreName(cls.getSimpleName());
// 查询注解的表名是否重复
if (tableNameList.stream().anyMatch(item -> item.equals(tableName))) {
throw new RuntimeException("表名重复:" + tableName);
} else {
tableNameList.add(tableName);
}
// 该表的所有列字段数据
ArrayList<AutoTableColumnMap> columnList = new ArrayList<>();
Field[] declaredFields = cls.getDeclaredFields();
for (Field declaredField : declaredFields) {
// 列注解
AutoTableColumn dataType = declaredField.getAnnotation(AutoTableColumn.class);
// 未指定注解的字段不做处理
if (Objects.isNull(dataType)) {
continue;
}
// 列名,自定义优先,否则字段转下划线命名风格
String columnName = dataType.value().length() > 0 ?
dataType.value() : underscoreName(declaredField.getName());
// 排除名为ID的字段,默认生成
if (Objects.equals(columnName, "id")) {
continue;
}
AutoTableColumnMap columnVo = new AutoTableColumnMap();
columnVo.setTableName(tableName);
columnVo.setColumnName(columnName);
// 数据类型
if (dataType.type().length() > 0) {
columnVo.setDataType(dataType.type().toUpperCase());
} else {
// 未指定类型的字段不做处理
continue;
}
columnList.add(columnVo);
}
// 查询数据库中是否存在该表名
boolean exist = dbTableNameList.stream().anyMatch(item -> item.equalsIgnoreCase(tableName));
if (!exist && columnList.size() > 0) {
// 表名不存在且有字段,新建表并添加字段,自动添加ID字段
sql.append("create table ").append("`").append(tableName).append("`")
.append("(`id` bigint primary key,");
for (int i = 0; i < columnList.size(); i++) {
if (i > 0) {
sql.append(",");
}
// 列名
sql.append("`").append(columnList.get(i).getColumnName()).append("` ");
// 数据类型
sql.append(columnList.get(i).getDataType());
}
sql.append(");");
} else if (exist) {
// 表名存在,进行列添加
for (AutoTableColumnMap columnVo : columnList) {
// 列名
String columnName = columnVo.getColumnName();
// 是否存在
List<AutoTableColumnMap> collect = dbColumnList.stream().filter(item ->
item.getTableName().equalsIgnoreCase(tableName)
&& item.getColumnName().equalsIgnoreCase(columnName))
.collect(Collectors.toList());
if (collect.size() == 0) {
// 字段不存在
sql.append("alter table ").append("`").append(tableName).append("` ");
// 添加字段
sql.append("add column");
// 列名
sql.append(" `").append(columnVo.getColumnName()).append("` ");
// 数据类型
sql.append(columnVo.getDataType());
sql.append(";");
}
// 不做列修改
}
}
}
// 执行DDL语句
if (sql.length() > 0) {
try {
ddlMapper.execUpdateSql(sql.toString());
return 1;
} catch (Exception e) {
log.info("表维护失败:{}", e.getMessage());
return 0;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return 2;
}
public <A extends Annotation> Set<Class<?>> getAnnotationClasses(String packageName, Class<A> annotationClass)
throws Exception {
//找用了annotationClass注解的类
Set<Class<?>> controllers = new HashSet<>();
Set<Class<?>> clsList = getClasses(packageName);
if (clsList.size() > 0) {
for (Class<?> cls : clsList) {
if (cls.getAnnotation(annotationClass) != null) {
controllers.add(cls);
}
}
}
return controllers;
}
/**
* 从包package中获取全部的Class
*/
private Set<Class<?>> getClasses(String packageName) throws Exception {
// 第一个class类的集合
Set<Class<?>> classes = new HashSet<>();
// 获取包的名字 并进行替换
String packageDirName = packageName.replace('.', '/');
// 定义一个枚举的集合 并进行循环来处理这个目录下的things
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
// 循环迭代下去
while (dirs.hasMoreElements()) {
// 获取下一个元素
URL url = dirs.nextElement();
// 获得协议的名称
String protocol = url.getProtocol();
// 若是是以文件的形式保存在服务器上
if ("file".equals(protocol)) {
// 获取包的物理路径
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
// 以文件的方式扫描整个包下的文件 并添加到集合中
addClass(classes, filePath, packageName);
} else if ("jar".equals(protocol)) {
// 若是是jar包文件
// 定义一个JarFile
JarFile jar;
try {
// 获取jar
jar = ((JarURLConnection) url.openConnection()).getJarFile();
// 今后jar包 获得一个枚举类
Enumeration<JarEntry> entries = jar.entries();
// 一样的进行循环迭代
while (entries.hasMoreElements()) {
// 获取jar里的一个实体 能够是目录 和一些jar包里的其余文件 如META-INF等文件
JarEntry entry = entries.nextElement();
String name = entry.getName();
// 若是是以/开头的
if (name.charAt(0) == '/') {
// 获取后面的字符串
name = name.substring(1);
}
// 若是前半部分和定义的包名相同
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
// 若是以"/"结尾 是一个包
if (idx != -1) {
// 获取包名 把"/"替换成"."
packageName = name.substring(0, idx).replace('/', '.');
}
// 若是能够迭代下去 而且是一个包
// 若是是一个.class文件 并且不是目录
if (name.endsWith(".class") && !entry.isDirectory()) {
// 去掉后面的".class" 获取真正的类名
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
// 添加到classes
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
return classes;
}
private void addClass(Set<Class<?>> classes, String filePath, String packageName) throws Exception {
File[] files = new File(filePath).listFiles(file
-> (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory());
assert files != null;
for (File file : files) {
String fileName = file.getName();
if (file.isFile()) {
String classesName = fileName.substring(0, fileName.lastIndexOf("."));
if (!packageName.isEmpty()) {
classesName = packageName + "." + classesName;
}
doAddClass(classes, classesName);
}
}
}
public void doAddClass(Set<Class<?>> classes, final String classesName) throws Exception {
ClassLoader classLoader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
};
classes.add(classLoader.loadClass(classesName));
}
/**
* 将驼峰式命名的字符串转换为下划线小写方式。如果转换前的驼峰式命名的字符串为空,则返回空字符串。
* 例如:HelloWorld->hello_world
*/
private static String underscoreName(String name) {
StringBuilder result = new StringBuilder();
if (name != null && name.length() > 0) {
// 下划线命名不做处理
if (name.contains("_")) {
return name;
}
// 将第一个字符处理成小写
result.append(name.substring(0, 1).toLowerCase());
// 循环处理其余字符
for (int i = 1; i < name.length(); i++) {
String s = name.substring(i, i + 1);
// 在大写字母前添加下划线(非数值)
if (s.equals(s.toUpperCase()) && !Character.isDigit(s.charAt(0))) {
result.append("_");
}
// 其他字符直接转成小写
result.append(s.toLowerCase());
}
}
return result.toString();
}
}
使用方式
配置
# 自动表维护
autoTable:
dbType: H2
dbName: fanning
tablePack: com.fan.entity
使用示例
import com.fan.autoTable.AutoTable;
import com.fan.autoTable.AutoTableColumn;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Date;
/**
* 测试实体类
*/
@Data
@AutoTable
public class TestTable implements Serializable {
@AutoTableColumn(exist = false)
private static final long serialVersionUID = -3512369734135256213L;
/**
* ID
*/
private String id;
/**
* tinyint
*/
@AutoTableColumn(type = "tinyint default 1")
private Byte sex;
/**
* smallint
*/
@AutoTableColumn(type = "smallint")
private Short count;
/**
* int
*/
@AutoTableColumn(type = "int not null")
private Integer mount;
/**
* bigint
*/
@AutoTableColumn(type = "bigint")
private Long moreTime;
/**
* float
*/
@AutoTableColumn(type = "float")
private Float number1;
/**
* double
*/
@AutoTableColumn(type = "double")
private Double number2;
/**
* decimal
*/
@AutoTableColumn(type = "decimal")
private BigDecimal number3;
/**
* char
*/
@AutoTableColumn(type = "char(20)")
private String forShort;
/**
* varchar
*/
@AutoTableColumn(type = "varchar(200)")
private String name;
/**
* boolean
*/
@AutoTableColumn(type = "boolean")
private Boolean exist;
/**
* 时间
*/
@AutoTableColumn(type = "timestamp")
private Date createTime;
/**
* 大文本
*/
@AutoTableColumn(type = "longtext")
private String html;
}
结果
附SQL类型
MySQL数据类型 对应Java数据类型 支持范围 说明
tinyint Byte -128~127
smallint Short -32768~32767
int Integer -2147483648~2147483647
bigint Long -9223372036854775808~9223372036854775807
float Float
double Double
decimal BigDecimal
char(20) 0~255字节 定长字符串
varchar(255) String 0~65535字节 变长字符串
boolean Boolean true|false MySQL存的tinyint(1)
tinytext 0~255字节 变长字符串
text 0~65535字节 变长字符串
mediumtext 0~16777215字节 变长字符串
longtext 0~4294967295 or 4GB(232−1)字节 变长字符串
tinyblob 0~255字节 变长二进制
blob 0~65535字节 变长二进制
mediumblob 0~16777215字节 变长二进制
longblob 0~4294967295 or 4GB(232−1)字节 变长二进制