logo 范 · 拾光录
网址收集 关于作者 Github Gitee
杂文随笔5
Hexo博客:基础使用Hexo博客:Next主题Hexo博客:Next进阶使用Hexo博客:Next高级配置基于Node的WIKI管理
前端知识16
HTML常用知识CSS常用知识CSS美化checkbox复选框JavaScript常用知识JavaScript格式化时间戳JavaScript窗口宽高处理JavaScript黑夜主题切换实现方案JavaScript数字转大写简易图片查看器TypeScript基础知识Threejs基础三要素Threejs网格辅助和轨道控制器Threejs物体绘制Electron基础使用Nodejs基础知识animate.css页面动画
Vue框架19
Vite的使用及扩展Vue3父子组件Vue3使用Marked解析MarkdownMermaid图表生成库初始化页面加载动画Axios表单提交二维码解决方案NProgress加载进度条Vue3动态菜单实现Vue3使用ECharts图表Vue3处理Excel导入导出keep-alive页面缓存及setup问题Element:文件上传Element:结合Pinia实现动态菜单Element:图片上传组件Element:自定义统一弹窗组件Element:表格自定义指令控制按钮显示(鉴权)可视化大屏使用缩放适配分辨率
UniApp15
UniApp的基础使用封装网络请求工具及文件上传uni-app的开发记录微信小程序分享原生文件上传Pinia取消滚动条(兼容小程序)tabbar消息数量显示scroll-view上滑到底部加载数据状态栏高度动态设配数据共享与传递uview-plus导航栏实现背景融合Wot UIWot UI实现顶部背景图融合uni-app x
Java基础知识10
基础知识面向对象Lambda表达式常用API常用知识积累try-with-resource注解反射多线程经纬度距离计算
SpringBoot31
application配置Maven创建聚合项目全局异常处理锁机制项目启动初始化数据方式邮件功能集成原生定时任务异步集成阿里云OSS阿里OSS预签名上传基于hutool读excelJSR303WebSocketWebSocket版AI接口流式调用Smart-Doc接口文档生成器application配置信息加密雪花算法工具AOP实现请求参数脱敏思路JWT生成Token及工具类SpringBoot默认JSON与对象转换若依框架:安装使用若依框架:优化和调整文件上传若依框架:管理后台页面优化若依框架:后端接口代码优化SpringAISpringBoot实现AI接口流式调用服务启动时创建MySQL连接自建项目工程树形结构处理工具微信支付代码微信手机号登录
SpringMVC14
跨域处理拦截器RESTful风格伪前后端分离Jackson转换器调整Thymeleaf基于拦截器做权限校验AOP打印接口请求响应日志AOP打印接口请求响应耗时文件上传和回显POST请求加解密实现(AES)POST请求加解密实现(RSA+AES)参数动态校验实现方案真实IP和归属地
MyBatis8
MyBatis基本使用与配置Mapper使用相关MaBatis多数据源配置MyBatisPlus数据统计类处理方案MyBatisPlus条件查询正向工程的实现(H2)mybatis-plus-join
SpringCloud15
Netflix:微服务与搭建Netflix:服务的消费与提供Netflix:EurekaNetflix:ActuatorNetflix:RibbonNetflix:FeignNetflix:HystrixNetflix:ZuulAlibaba:简介与搭建Alibaba:Nacos注册中心Alibaba:RibbonAlibaba:OpenFeignAlibaba:Nacos配置中心Alibaba:GetewayAlibaba:Sentinel
MySQL6
MySQL基础知识MySQL多表查询与事务MySQL常用函数及解决方案MySQL视图MySQL索引安装MySQL
Redis7
Redis介绍和安装Redis配置文件Redis持久化Redis集群Redis语法基础Redis相关问题及解决方案SpringBoot集成Redis使用记录
MongoDB10
Linux安装MongoDBMongoDB基础语法MongoTemplate及SpringBoot配置MongoTemplate中Update操作MongoTemplate中聚合查询MongoTemplate日期归档示例项目使用相关知识归纳地理位置存储与距离查询MongoDB副本集与事务获取类名和属性名工具类
其他数据库1
H2数据库
Python编程6
Python基础知识Python语法yolo目标检测OpenCV的使用及树莓派平台condauv
工具集合13
IDEAMavenGradleGitNginx安装Nginx配置VSCodeJMeter压测DockerOllamaRustFSPicGoObs录制
Linux知识11
Linux常用命令Jar启动脚本VirtualBox安装CentOSVirtualBox安装Ubuntu树莓派安装及使用frp内网穿透ArchLinux:基础系统安装ArchLInux:图形化界面安装ArchLinux:常用软件ArchLinux:深度优化ArchLinux:Niri
创意设计2
Blender:入门知识UI设计基础知识
AI相关9
Claude CodeHermes AgentOpenAI基本使用OpenAI工具调用OpenAI记忆管理OpenAI推理执行OpenAI开发框架Langchainllama.cpp

环境

项目作用

数据库的正向工程,自动根据实体类维护表结构,仅支持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;

}
环境
项目作用
主键策略
新增结构
创建autoTable包
AutoTable
AutoTableColumn
AutoTableTaskConfig
使用方式
配置
使用示例