MyBatis:正向工程
图
MyBatis

环境

  • JDK:17.0.14
  • Maven:3.9.9
  • SpringBoot:3.4.2
  • MyBatisPlus:3.5.10.1

项目作用

数据库的正向工程,自动根据实体类维护表结构,支持H2和MySQL,注意反射时使用了字符匹配,防止的是与devtools冲突获取不到实体类属性的问题

项目前提

  • 项目引入了MyBatis-Plus
  • 主键使用的是MyBatis-Plus的默认ID策略(雪花算法)

新增结构

图

创建autoTable包

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;

}

AutoTableTaskConfig

import com.fan.mapper.DDLMapper;
import jakarta.annotation.Resource;
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 java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * 自动维护表结构(项目启动执行)
 * <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) {
            log.error("自动维护表结构异常", e);
        }
    }

    /**
     * 处理
     *
     * @return 0-失败、1-执行完毕、2-不需要执行
     */
    private int deal() {
        try {
            // 获取表所有已存在的字段
            List<AutoTableColumnMap> dbColumnList = ddlMapper.execSelectSql(COLUMN_SELECT_SQL);
            // 获取使用 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).toList();
            for (Class<?> cls : classes) {
                // 注解表名
                AutoTable annotation = cls.getAnnotation(AutoTable.class);
                // 表名,自定义优先,否则实体类名转下划线命名风格
                String tableName = !annotation.value().isEmpty()
                        ? 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 = getAllDeclaredFields(cls);
                // 判断是否定义了重复的属性
                List<String> columnNameList = new ArrayList<>();
                for (Field field : declaredFields) {
                    columnNameList.add(field.getName());
                }
                if (columnNameList.stream().distinct().count() != columnNameList.size()) {
                    throw new RuntimeException("实体类 " + cls.getSimpleName() + " 存在重复属性!");
                }
                for (Field declaredField : declaredFields) {
                    // 忽略静态字段
                    if (Modifier.isStatic(declaredField.getModifiers())) {
                        continue;
                    }
                    // 忽略非private字段
                    if (!Modifier.isPrivate(declaredField.getModifiers())) {
                        continue;
                    }
                    // 列注解
                    AutoTableColumn dataType = declaredField.getAnnotation(AutoTableColumn.class);
                    // 列名,自定义优先,否则字段转下划线命名风格
                    String columnName = Objects.nonNull(dataType) && !dataType.value().isEmpty() ?
                            dataType.value() : underscoreName(declaredField.getName());
                    // 排除名为ID的字段,默认生成
                    if (Objects.equals(columnName, "id")) {
                        continue;
                    }
                    AutoTableColumnMap columnVo = new AutoTableColumnMap();
                    columnVo.setTableName(tableName);
                    columnVo.setColumnName(columnName);
                    // 数据类型
                    if (Objects.nonNull(dataType) && !dataType.type().isEmpty()) {
                        columnVo.setDataType(dataType.type().toUpperCase());
                    } else {
                        // 未指定类型的字段按默认类型处理
                        // integer类型
                        if (declaredField.getType().equals(Byte.class)) {
                            columnVo.setDataType("tinyint");
                        } else if (declaredField.getType().equals(Short.class)) {
                            columnVo.setDataType("smallint");
                        } else if (declaredField.getType().equals(Integer.class)) {
                            columnVo.setDataType("int");
                        } else if (declaredField.getType().equals(Long.class)) {
                            columnVo.setDataType("bigint");
                        } else if (declaredField.getType().equals(Float.class)) {
                            columnVo.setDataType("float");
                        } else if (declaredField.getType().equals(Double.class)) {
                            columnVo.setDataType("double");
                        } else if (declaredField.getType().equals(BigDecimal.class)) {
                            columnVo.setDataType("decimal");
                        } else if (declaredField.getType().equals(Boolean.class)) {
                            columnVo.setDataType("boolean");
                        } else if (declaredField.getType().equals(String.class)) {
                            columnVo.setDataType("varchar(255)");
                        } else if (declaredField.getType().equals(Date.class)) {
                            columnVo.setDataType("datetime");
                        } else {
                            throw new RuntimeException("未处理的字段类型:" + declaredField.getType());
                        }
                    }
                    columnList.add(columnVo);
                }
                // 查询数据库中是否存在该表名
                boolean exist = dbTableNameList.stream().anyMatch(item -> item.equalsIgnoreCase(tableName));
                if (!exist && !columnList.isEmpty()) {
                    // 表名不存在且有字段,新建表并添加字段,自动添加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))
                                .toList();
                        if (collect.isEmpty()) {
                            // 字段不存在
                            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.isEmpty()) {
                try {
                    ddlMapper.execUpdateSql(sql.toString());
                    return 1;
                } catch (Exception e) {
                    log.info("表维护失败:{}", e.getMessage());
                    return 0;
                }
            }
        } catch (Exception e) {
            log.error("自动维护表结构异常", e);
        }
        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.isEmpty()) {
            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(), StandardCharsets.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) {
                                        log.error("从包package中获取全部的Class异常", e);
                                    }
                                }
                            }
                        }
                    } catch (IOException e) {
                        log.error("从包package中获取全部的Class异常", e);
                    }
                }
            }
        } catch (IOException e) {
            log.error("从包package中获取全部的Class异常", e);
        }
        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.isEmpty()) {
            // 下划线命名不做处理
            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();
    }

    /**
     * 获取所有字段(包括非公共字段和继承的字段)
     */
    private Field[] getAllDeclaredFields(Class<?> cls) {
        List<Field> fields = new ArrayList<>();
        while (cls != null) {
            fields.addAll(Arrays.asList(cls.getDeclaredFields()));
            cls = cls.getSuperclass();
        }
        return fields.toArray(new Field[0]);
    }

}

DDLMapper

需要放在MyBatisPlus指定的Mapper包下

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);

}

使用方式

配置

# 自动表维护
autoTable:
  dbType: H2
  dbName: fanning
  tablePack: com.fan.entity

使用示例

/**
 * 实体类演示
 */
@EqualsAndHashCode(callSuper = true)
@Data
@AutoTable
public class Users extends BaseEntity implements Serializable {

    @Serial
    private static final long serialVersionUID = 5238966504809200929L;

    /**
     * 姓名(255字符以内)
     */
    private String name;

    /**
     * 年龄,-128~127
     */
    private Short age;

    /**
     * 性别
     */
    private Byte sex;

    /**
     * 手机号(11位)
     */
    @AutoTableColumn(type = "varchar(11)")
    private String phone;

    /**
     * 备注
     */
    private String content;

    /**
     * 是否新用户
     */
    private Boolean isNew;

    /**
     * 账户余额
     */
    private BigDecimal balance;

    /**
     * 大文本
     */
    @AutoTableColumn(type = "longtext")
    private String text;

}

附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)字节                       变长二进制