环境
- JDK:17.0.17
- Maven:3.9.9
- SpringBoot:3.5.15
项目作用
数据库的正向工程,自动根据实体类维护表结构,仅支持H2数据库
主键策略
代码写的是自增策略,从10001开始
新增结构

创建autoTable包
AutoTable
import java.lang.annotation.*;
/**
* 指定实体类为表(H2 数据库)
* <p>
* 实体类中带 @AutoTableColumn 的属性均为表中字段,下划线命名
* <p>
* 会自动增加表的主键,字段名为 id,自增策略
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoTable {
/**
* 表名,默认为实体类名,自动转换下划线命名,自定义请遵循下划线命名
*/
String value() default "";
}
AutoTableColumn
import java.lang.annotation.*;
/**
* 表列字段注解(H2 数据库)
*
* <pre>
* H2 支持的数据类型:
* 字符串 varchar(n)、char(n)、clob 示例: varchar(100)、varchar(255)
* 整数 tinyint、smallint、int、bigint 示例: int、bigint
* 布尔 boolean 示例: boolean
* 浮点 float、double、real 示例: double、float
* 定点数 decimal(p,s)、numeric(p,s) 示例: decimal(10,2)
* 日期时间 date、time、datetime、timestamp 示例: datetime
* 二进制 binary(n)、varbinary(n)、blob 示例: blob
* 其他 uuid、json、array、enum 示例: uuid
* </pre>
*
* <p>不指定 value 时,根据 Java 字段类型自动推断:
* String→varchar(255)、int/Integer→int、long/Long→bigint、
* BigDecimal→decimal(10,2)、LocalDateTime→datetime、byte[]→blob 等</p>
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoTableColumn {
/**
* H2 数据类型,如 varchar(100)、int、decimal(10,2)、boolean、datetime、blob 等
* <p>留空则根据 Java 类型自动推断</p>
*/
String value() default "";
}
AutoTableTaskConfig
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.io.File;
import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.sql.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/**
* 自动维护表结构(项目启动执行),仅适配 H2
* <p>
* 只做新增,不做修改
* <p>
* 基于 JDBC,依赖 spring-boot 和 lombok
*/
@Slf4j
@Component
@Order(1)
public class AutoTableTaskConfig implements ApplicationRunner {
@Resource
private Environment environment;
private String packName;
/** Java 类型 → H2 类型默认映射 */
private static final Map<Class<?>, String> JAVA_TO_H2 = new HashMap<>();
static {
JAVA_TO_H2.put(String.class, "varchar(255)");
JAVA_TO_H2.put(Integer.class, "int");
JAVA_TO_H2.put(int.class, "int");
JAVA_TO_H2.put(Long.class, "bigint");
JAVA_TO_H2.put(long.class, "bigint");
JAVA_TO_H2.put(Short.class, "smallint");
JAVA_TO_H2.put(short.class, "smallint");
JAVA_TO_H2.put(Byte.class, "tinyint");
JAVA_TO_H2.put(byte.class, "tinyint");
JAVA_TO_H2.put(Boolean.class, "boolean");
JAVA_TO_H2.put(boolean.class, "boolean");
JAVA_TO_H2.put(Float.class, "float");
JAVA_TO_H2.put(float.class, "float");
JAVA_TO_H2.put(Double.class, "double");
JAVA_TO_H2.put(double.class, "double");
JAVA_TO_H2.put(BigDecimal.class, "decimal(10,2)");
JAVA_TO_H2.put(BigInteger.class, "bigint");
JAVA_TO_H2.put(LocalDate.class, "date");
JAVA_TO_H2.put(LocalTime.class, "time");
JAVA_TO_H2.put(LocalDateTime.class, "datetime");
JAVA_TO_H2.put(java.util.Date.class, "datetime");
JAVA_TO_H2.put(byte[].class, "blob");
JAVA_TO_H2.put(Byte[].class, "blob");
}
@Override
public void run(ApplicationArguments args) {
try {
System.out.println("┌─┐┬ ┬┌┬┐┌─┐ ┌┬┐┌─┐┌┐ ┬ ┌─┐");
System.out.println("├─┤│ │ │ │ │ │ ├─┤├┴┐│ ├┤ ");
System.out.println("┴ ┴└─┘ ┴ └─┘ ┴ ┴ ┴└─┘┴─┘└─┘");
System.out.println(" v1.3.0");
packName = environment.getProperty("autoTable.tablePack");
if (Objects.isNull(packName)) {
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);
}
}
/** 检测 H2 版本 */
private void checkH2Version() {
try (Connection conn = getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT H2VERSION() FROM DUAL")) {
if (rs.next()) {
log.info("H2 版本:{}", rs.getString(1));
}
} catch (SQLException e) {
log.warn("无法获取 H2 版本", e);
}
}
/** 核心处理逻辑 */
private int deal() {
Set<Class<?>> classes;
try {
classes = getAnnotationClasses(packName);
} catch (Exception e) {
log.error("扫描实体类失败", e);
return 0;
}
if (classes.isEmpty()) {
log.info("未找到 @AutoTable 注解的类");
return 2;
}
checkH2Version();
StringBuilder sql = new StringBuilder();
ArrayList<String> tableNameList = new ArrayList<>();
try (Connection conn = getConnection()) {
for (Class<?> cls : classes) {
// 读取 @AutoTable
AutoTable autoTable = cls.getAnnotation(AutoTable.class);
String tableName = autoTable.value().isEmpty()
? underscoreName(cls.getSimpleName())
: autoTable.value();
if (tableNameList.stream().anyMatch(item -> item.equals(tableName))) {
throw new RuntimeException("表名重复:" + tableName);
}
tableNameList.add(tableName);
// 收集实体类列定义
ArrayList<ColumnMap> columnList = buildColumnList(cls, tableName);
// 按表查询已有列(H2 默认存储大写)
List<ColumnMap> existingColumns = queryTableColumns(conn, tableName.toUpperCase());
if (existingColumns.isEmpty()) {
// 表不存在 → 建表
if (!columnList.isEmpty()) {
sql.append(buildCreateTable(tableName, columnList));
}
} else {
// 表存在 → 补列
for (ColumnMap col : columnList) {
boolean exists = existingColumns.stream()
.anyMatch(e -> e.columnName.equalsIgnoreCase(col.columnName));
if (!exists) {
sql.append(buildAddColumn(tableName, col));
}
}
}
}
if (!sql.isEmpty()) {
executeDdl(conn, sql.toString());
return 1;
}
return 2;
} catch (SQLException e) {
log.error("数据库操作异常", e);
return 0;
}
}
/** 从实体类字段构建列定义列表 */
private ArrayList<ColumnMap> buildColumnList(Class<?> cls, String tableName) {
ArrayList<ColumnMap> columnList = new ArrayList<>();
Field[] declaredFields = getAllDeclaredFields(cls);
// 检查属性名重复
List<String> fieldNames = new ArrayList<>();
for (Field field : declaredFields) {
fieldNames.add(field.getName());
}
if (fieldNames.stream().distinct().count() != fieldNames.size()) {
throw new RuntimeException("实体类 " + cls.getSimpleName() + " 存在重复属性!");
}
for (Field field : declaredFields) {
AutoTableColumn atc = field.getAnnotation(AutoTableColumn.class);
if (atc == null) {
continue;
}
String columnName = underscoreName(field.getName());
if ("id".equals(columnName)) {
continue;
}
// 类型:注解值优先,否则 Java 类型推断
String h2Type = atc.value().isEmpty()
? JAVA_TO_H2.getOrDefault(field.getType(), "varchar(255)")
: atc.value();
ColumnMap col = new ColumnMap();
col.tableName = tableName;
col.columnName = columnName;
col.dataType = h2Type;
columnList.add(col);
}
return columnList;
}
/** 生成 CREATE TABLE DDL(H2 兼容) */
private String buildCreateTable(String tableName, ArrayList<ColumnMap> columns) {
StringBuilder sb = new StringBuilder();
sb.append("CREATE TABLE IF NOT EXISTS \"").append(tableName.toUpperCase()).append("\" (\n");
sb.append(" \"ID\" INT NOT NULL AUTO_INCREMENT(10001) PRIMARY KEY");
for (ColumnMap col : columns) {
sb.append(",\n \"").append(col.columnName.toUpperCase()).append("\" ").append(col.dataType);
}
sb.append("\n);\n");
return sb.toString();
}
/** 生成 ALTER TABLE ADD COLUMN DDL(H2 兼容) */
private String buildAddColumn(String tableName, ColumnMap col) {
return "ALTER TABLE \"" + tableName.toUpperCase() +
"\" ADD COLUMN \"" + col.columnName.toUpperCase() +
"\" " + col.dataType +
";\n";
}
/** 查询指定表的已有列 */
private List<ColumnMap> queryTableColumns(Connection conn, String tableName) {
String sql = "SELECT COLUMN_NAME "
+ "FROM information_schema.COLUMNS "
+ "WHERE TABLE_NAME = ? "
+ "ORDER BY ORDINAL_POSITION";
List<ColumnMap> list = new ArrayList<>();
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, tableName);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
ColumnMap col = new ColumnMap();
col.tableName = tableName;
col.columnName = rs.getString("COLUMN_NAME");
list.add(col);
}
}
} catch (SQLException e) {
log.error("查询表 {} 列信息失败", tableName, e);
}
return list;
}
/** 执行 DDL,逐条分割执行 */
private void executeDdl(Connection conn, String sql) {
String[] statements = sql.split(";\n");
try (Statement stmt = conn.createStatement()) {
for (String statement : statements) {
String trimmed = statement.trim();
if (!trimmed.isEmpty()) {
log.debug("执行DDL:{}", trimmed);
stmt.execute(trimmed);
}
}
} catch (SQLException e) {
log.error("DDL执行失败", e);
throw new RuntimeException("DDL执行失败:" + e.getMessage());
}
}
/** 获取数据库连接 */
private Connection getConnection() throws SQLException {
return DriverManager.getConnection(
Objects.requireNonNull(environment.getProperty("spring.datasource.url")),
environment.getProperty("spring.datasource.username"),
environment.getProperty("spring.datasource.password"));
}
// ==================== 类扫描工具方法 ====================
private Set<Class<?>> getAnnotationClasses(String packageName)
throws Exception {
Set<Class<?>> result = new HashSet<>();
Set<Class<?>> clsList = getClasses(packageName);
for (Class<?> cls : clsList) {
if (cls.getAnnotation(AutoTable.class) != null) {
result.add(cls);
}
}
return result;
}
/** 从包 packageName 中获取全部的 Class */
private Set<Class<?>> getClasses(String packageName) throws Exception {
Set<Class<?>> classes = new HashSet<>();
String packageDirName = packageName.replace('.', '/');
Enumeration<URL> 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)) {
JarFile jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.charAt(0) == '/') {
name = name.substring(1);
}
if (name.startsWith(packageDirName) && name.endsWith(".class")
&& !entry.isDirectory()) {
String className = name.substring(
packageName.length() + 1, name.length() - 6);
try {
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
log.error("加载类失败:{}", className, e);
}
}
}
}
}
return classes;
}
private void addClass(Set<Class<?>> classes, String filePath, String packageName) {
File[] files = new File(filePath).listFiles(file
-> (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory());
if (files == null) return;
for (File file : files) {
String fileName = file.getName();
if (file.isFile()) {
String classSimpleName = fileName.substring(0, fileName.lastIndexOf("."));
String fullClassName = packageName.isEmpty()
? classSimpleName
: packageName + "." + classSimpleName;
try {
classes.add(Class.forName(fullClassName));
} catch (ClassNotFoundException e) {
log.error("加载类失败:{}", fullClassName, e);
}
}
}
}
/**
* 驼峰 → 下划线小写。如 HelloWorld → hello_world。
* 本身已是下划线命名的直接返回。
*/
private static String underscoreName(String name) {
if (name == null || name.isEmpty()) {
return name;
}
if (name.contains("_")) {
return name;
}
StringBuilder result = new StringBuilder();
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();
}
/** 获取所有字段(含继承),在 Object.class 停止 */
private Field[] getAllDeclaredFields(Class<?> cls) {
List<Field> fields = new ArrayList<>();
while (cls != null && cls != Object.class) {
fields.addAll(Arrays.asList(cls.getDeclaredFields()));
cls = cls.getSuperclass();
}
return fields.toArray(new Field[0]);
}
// ==================== 内部类 ====================
/** 列映射 */
private static class ColumnMap {
String tableName;
String columnName;
String dataType;
}
}
使用方式
配置
# 自动表维护
autoTable:
# 实体类包
tablePack: com.fan.entity
使用示例
继承方式演示
import com.fan.autoTable.AutoTableColumn;
import lombok.Data;
import java.util.Date;
/**
* 基础实体类(纯基类,不建表)
* <p>
* 子类加 @AutoTable 即可将本类字段一并映射到表
*/
@Data
public class BaseEntity {
/**
* ID 主键(框架自动生成 INT AUTO_INCREMENT(10001),此处仅作 Java 类型声明)
*/
private Long id;
/**
* 创建时间
*/
@AutoTableColumn
private Date createTime;
/**
* 修改时间
*/
@AutoTableColumn
private Date updateTime;
}
具体实体类
import com.fan.autoTable.AutoTable;
import com.fan.autoTable.AutoTableColumn;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* Demo 实体(模板参考)
*
* <p>最终生成表 DEMO:</p>
* <pre>
* ID INT AUTO_INCREMENT(10001) PRIMARY KEY -- 框架自动
* CREATE_TIME DATETIME -- 继承 BaseEntity
* UPDATE_TIME DATETIME -- 继承 BaseEntity
* NAME VARCHAR(100) -- 显式指定类型
* AGE INT -- Java 类型推断
* </pre>
*/
@EqualsAndHashCode(callSuper = true)
@Data
@AutoTable
public class Demo extends BaseEntity {
@AutoTableColumn("varchar(100)")
private String name;
@AutoTableColumn
private Integer age;
}