数据库文档生成工具(PostgreSQL 适配版 - Java 8 兼容)

该工具类是面向 Java 8 运行环境开发的 PostgreSQL 数据库结构文档自动生成组件,核心解决第三方文档生成框架(screw)对 PostgreSQL 的兼容问题,同时提供双生成策略(框架优先 + 自定义兜底),确保数据库结构文档稳定输出为 HTML 格式,完整覆盖表名、字段名、数据类型、长度、主键、可空性、备注等核心结构信息。

核心定位

专为 PostgreSQL 数据库设计,适配 Java 8 语法规范和依赖生态,无需升级 JDK 即可实现数据库文档的自动化生成,兼顾易用性和稳定性,适用于中小型项目的数据库设计文档管理场景。

核心功能与设计思路

1. 双生成策略(容错兜底)

  • 优先策略:基于 screw 开源框架生成标准化 HTML 文档,通过配置 PostgreSQL 的 public Schema、连接池参数,最大化复用成熟框架的模板能力(Freemarker);
  • 降级策略:若 screw 框架执行失败(如 PostgreSQL 版本兼容、Schema 读取异常),自动切换到纯 JDBC 原生实现的自定义生成逻辑,无第三方框架依赖,保证文档生成不中断。

2. 数据源适配(PostgreSQL 专属)

基于 HikariCP 3.4.5(Java 8 兼容版)构建高性能数据库连接池,针对性配置 PostgreSQL 连接参数:

  • 显式指定currentSchema=public解决表结构读取范围问题;
  • 配置连接超时、最小 / 最大连接数,适配 Java 8 的 JDBC 资源管理机制;
  • 补充 HikariCP 层面的 Schema 参数,双重保障表结构读取准确性。

3. 完整的结构信息采集

通过 JDBC 的DatabaseMetaData原生 API 读取 PostgreSQL 数据库元数据,采集维度包括:

  • 表级信息:表名、表备注;
  • 列级信息:字段名、数据类型、字段长度、是否可空、是否主键、字段备注;
  • 范围控制:仅读取 public Schema 下的用户表(过滤系统表)。

4. Java 8 适配特性(核心改造点)

针对 Java 8 语法和 API 限制做专项适配,避免版本兼容问题:

  • 替换 Java 9 + 专属 API:如List.of()改为new ArrayList<>(),字符集使用StandardCharsets.UTF_8(替代硬编码字符串);
  • 依赖版本适配:选用 screw 1.0.5、HikariCP 3.4.5、PostgreSQL JDBC 42.2.23 等 Java 8 兼容版本;
  • 资源管理:补充ResultSet手动关闭逻辑,结合try-with-resources确保连接 / 结果集资源闭环,避免内存泄漏;
  • 中文兼容:写入 HTML 文件时强制指定 UTF-8 编码,新增 HTML 特殊字符转义方法,解决中文乱码和格式错乱问题;
  • 目录兼容:自动检查并创建输出目录(mkdirs()),适配 Java 8 的文件系统操作规范。

5. 人性化的文档输出

  • 生成的 HTML 文档包含基础样式美化(表格边框、表头背景、标题居中),便于阅读;
  • 输出路径可配置,自动打开输出目录,支持自定义文档名称;
  • 异常细化处理:拆分 SQL 异常、IO 异常、通用异常,输出精准的错误提示,便于问题定位。

使用方式

  1. 配置参数:修改代码中数据库连接信息(URL、用户名、密码)和文档输出路径;
  2. 直接运行:执行DocumentUtil类的main方法,工具会自动优先使用 screw 生成文档,失败则触发自定义生成逻辑;
  3. 结果查看:生成完成后自动打开输出目录,HTML 文档包含所有 public Schema 下的表结构信息,可直接在浏览器中打开查看。

核心价值

  • 零侵入:无需修改数据库结构,通过 JDBC 元数据读取信息;
  • 高兼容:完全适配 Java 8 环境,无需升级 JDK 或重构项目;
  • 高可用:双生成策略避免单一框架故障导致文档生成失败;
  • 易维护:代码结构清晰,实体类、采集逻辑、生成逻辑分层设计,便于扩展(如新增 Markdown 输出格式)。

项目代码:

复制代码
package com.njcky.neosourcepm.util;

import cn.smallbun.screw.core.Configuration;
import cn.smallbun.screw.core.engine.EngineConfig;
import cn.smallbun.screw.core.engine.EngineFileType;
import cn.smallbun.screw.core.engine.EngineTemplateType;
import cn.smallbun.screw.core.execute.DocumentationExecute;
import cn.smallbun.screw.core.process.ProcessConfig;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;

import javax.sql.DataSource;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * 数据库文档生成工具(PostgreSQL 适配版 - Java 8 兼容)
 */
public class DocumentUtil {

    /**
     * 获取 PostgreSQL 数据源(适配 Java 8)
     */
    public DataSource dataSource() {
        HikariConfig hikariConfig = new HikariConfig();
        hikariConfig.setDriverClassName("org.postgresql.Driver");
        // PostgreSQL 连接配置(指定 Schema + 兼容参数)
        hikariConfig.setJdbcUrl("jdbc:postgresql://localhost:54323/xxx?currentSchema=public&useInformationSchema=true");
        hikariConfig.setUsername("xxx");
        hikariConfig.setPassword("xxx");
        // Hikari 连接池参数(Java 8 兼容)
        hikariConfig.setMinimumIdle(5);
        hikariConfig.setMaximumPoolSize(10);
        hikariConfig.setConnectionTimeout(30000);
        hikariConfig.addDataSourceProperty("schema", "public");
        return new HikariDataSource(hikariConfig);
    }

    public static void main(String[] args) {
        DocumentUtil documentUtil = new DocumentUtil();

        // 1. 配置文档生成引擎(输出 HTML)
        EngineConfig engineConfig = EngineConfig.builder()
                .fileOutputDir("C:\\Users\\14786\\Desktop\\项目\\数据库设计文档")
                .openOutputDir(true)
                .fileType(EngineFileType.HTML)
                .produceType(EngineTemplateType.freemarker)
                .fileName("XY_DB数据库设计文档")
                .build();

        // 2. 配置处理规则(指定 PostgreSQL Schema)
        ProcessConfig processConfig = ProcessConfig.builder()
                .designatedTableName(new ArrayList<>()) // Java 8 显式 new ArrayList
                .designatedTablePrefix(new ArrayList<>())
                .designatedTableSuffix(new ArrayList<>())
                .build();

        // 3. 构建整体配置
        Configuration config = Configuration.builder()
                .version("1.0.0")
                .description("XY_DB 数据库设计文档(自动生成)")
                .dataSource(documentUtil.dataSource())
                .engineConfig(engineConfig)
                .produceConfig(processConfig)
                .build();

        // 4. 执行生成(失败则降级到自定义逻辑)
        try {
            new DocumentationExecute(config).execute();
            System.out.println("✅ 数据库文档生成完成!");
        } catch (Exception e) {
            System.err.println("⚠️ screw 生成失败(PostgreSQL 兼容问题),自动降级到自定义方案:");
            e.printStackTrace();
            PostgreSqlDocGenerator.main(null);
        }
    }

    /**
     * 兜底方案:自定义 PostgreSQL 文档生成(纯 Java 8 实现,不依赖第三方)
     */
    static class PostgreSqlDocGenerator {
        // 数据库连接配置
        private static final String URL = "jdbc:postgresql://localhost:54323/xxx?currentSchema=public";
        private static final String USER = "xxx";
        private static final String PWD = "xxx";
        // 文档输出路径(Java 8 兼容路径格式)
        private static final String OUTPUT_PATH = "C:\\Users\\14786\\Desktop\\项目\\数据库设计文档\\XY_DB数据库设计文档_自定义.html";

        public static void main(String[] args) {
            // 确保输出目录存在(Java 8 兼容)
            File outputDir = new File(OUTPUT_PATH).getParentFile();
            if (!outputDir.exists()) {
                outputDir.mkdirs(); // Java 8 创建多级目录
            }

            // 尝试生成文档
            try (Connection conn = getConnection()) {
                List<TableInfo> tableInfos = getTableInfos(conn);
                generateHtml(tableInfos);
                System.out.println("✅ 自定义文档生成完成:" + OUTPUT_PATH);
            } catch (SQLException e) {
                System.err.println("❌ 数据库连接/查询失败:" + e.getMessage());
                e.printStackTrace();
            } catch (IOException e) {
                System.err.println("❌ 文件写入失败:" + e.getMessage());
                e.printStackTrace();
            } catch (Exception e) {
                System.err.println("❌ 自定义生成失败:" + e.getMessage());
                e.printStackTrace();
            }
        }

        /**
         * 获取数据库连接(Java 8 try-with-resources 兼容)
         */
        private static Connection getConnection() throws SQLException {
            HikariConfig config = new HikariConfig();
            config.setJdbcUrl(URL);
            config.setUsername(USER);
            config.setPassword(PWD);
            config.setDriverClassName("org.postgresql.Driver");
            // Java 8 关闭自动提交(可选,增强稳定性)
            HikariDataSource dataSource = new HikariDataSource(config);
            Connection conn = dataSource.getConnection();
            conn.setAutoCommit(false);
            return conn;
        }

        /**
         * 读取数据库表和列信息(纯 Java 8 JDBC 实现)
         */
        private static List<TableInfo> getTableInfos(Connection conn) throws SQLException {
            List<TableInfo> tableList = new ArrayList<>();
            DatabaseMetaData metaData = conn.getMetaData();

            // 1. 查询 public Schema 下的所有表
            ResultSet tableRs = metaData.getTables(null, "public", "%", new String[]{"TABLE"});
            while (tableRs.next()) {
                String tableName = tableRs.getString("TABLE_NAME");
                String tableRemark = tableRs.getString("REMARKS") == null ? "" : tableRs.getString("REMARKS");
                TableInfo tableInfo = new TableInfo(tableName, tableRemark);

                // 2. 查询当前表的所有列信息
                ResultSet columnRs = metaData.getColumns(null, "public", tableName, "%");
                List<ColumnInfo> columnList = new ArrayList<>();
                while (columnRs.next()) {
                    String colName = columnRs.getString("COLUMN_NAME");
                    String colType = columnRs.getString("TYPE_NAME");
                    int colSize = columnRs.getInt("COLUMN_SIZE");
                    String isNullable = columnRs.getString("IS_NULLABLE");
                    String colRemark = columnRs.getString("REMARKS") == null ? "" : columnRs.getString("REMARKS");
                    // 判断是否为主键
                    String isPk = isPrimaryKey(metaData, tableName, colName);

                    columnList.add(new ColumnInfo(colName, colType, colSize, isNullable, isPk, colRemark));
                }
                columnRs.close(); // Java 8 手动关闭 ResultSet(避免资源泄漏)
                tableInfo.setColumnList(columnList);
                tableList.add(tableInfo);
            }
            tableRs.close();
            return tableList;
        }

        /**
         * 判断字段是否为主键(抽离方法,Java 8 代码复用)
         */
        private static String isPrimaryKey(DatabaseMetaData metaData, String tableName, String colName) throws SQLException {
            ResultSet pkRs = metaData.getPrimaryKeys(null, "public", tableName);
            try {
                while (pkRs.next()) {
                    if (colName.equals(pkRs.getString("COLUMN_NAME"))) {
                        return "YES";
                    }
                }
                return "NO";
            } finally {
                pkRs.close(); // 确保 ResultSet 关闭
            }
        }

        /**
         * 生成 HTML 文档(Java 8 字符集兼容)
         */
        private static void generateHtml(List<TableInfo> tableList) throws IOException {
            StringBuilder html = new StringBuilder();
            // HTML 头部(UTF-8 编码,适配 Java 8)
            html.append("<!DOCTYPE html>\n")
                    .append("<html lang=\"zh-CN\">\n")
                    .append("<head>\n")
                    .append("    <meta charset=\"UTF-8\">\n")
                    .append("    <title>XY_DB 数据库设计文档</title>\n")
                    .append("    <style>\n")
                    .append("        table { border-collapse: collapse; width: 100%; margin: 20px 0; }\n")
                    .append("        th, td { border: 1px solid #ccc; padding: 8px; text-align: center; }\n")
                    .append("        th { background: #f5f5f5; }\n")
                    .append("        h1 { text-align: center; margin: 20px 0; }\n")
                    .append("        h2 { color: #333; margin-top: 30px; }\n")
                    .append("        hr { border: 0; border-top: 1px solid #eee; margin: 20px 0; }\n")
                    .append("    </style>\n")
                    .append("</head>\n")
                    .append("<body>\n")
                    .append("    <h1>XY_DB 数据库设计文档</h1>\n");

            // 遍历表生成内容
            for (TableInfo table : tableList) {
                html.append("    <h2>表名:").append(escapeHtml(table.getTableName())).append(" (")
                        .append(escapeHtml(table.getTableRemark())).append(")</h2>\n")
                        .append("    <table>\n")
                        .append("        <tr>\n")
                        .append("            <th>字段名</th>\n")
                        .append("            <th>类型</th>\n")
                        .append("            <th>长度</th>\n")
                        .append("            <th>是否可为空</th>\n")
                        .append("            <th>是否主键</th>\n")
                        .append("            <th>备注</th>\n")
                        .append("        </tr>\n");

                // 遍历列
                for (ColumnInfo col : table.getColumnList()) {
                    html.append("        <tr>\n")
                            .append("            <td>").append(escapeHtml(col.getColumnName())).append("</td>\n")
                            .append("            <td>").append(escapeHtml(col.getTypeName())).append("</td>\n")
                            .append("            <td>").append(col.getColumnSize()).append("</td>\n")
                            .append("            <td>").append(escapeHtml(col.getIsNullable())).append("</td>\n")
                            .append("            <td>").append(escapeHtml(col.getIsPrimaryKey())).append("</td>\n")
                            .append("            <td>").append(escapeHtml(col.getColumnRemark())).append("</td>\n")
                            .append("        </tr>\n");
                }

                html.append("    </table>\n")
                        .append("    <hr/>\n");
            }

            // HTML 尾部
            html.append("</body>\n</html>");

            // Java 8 写入文件(指定 UTF-8,避免中文乱码)
            Files.write(Paths.get(OUTPUT_PATH), html.toString().getBytes(StandardCharsets.UTF_8));
        }

        /**
         * HTML 转义(Java 8 实现,防止 XSS 和格式错乱)
         */
        private static String escapeHtml(String str) {
            if (str == null || str.isEmpty()) {
                return "";
            }
            return str.replace("&", "&amp;")
                    .replace("<", "&lt;")
                    .replace(">", "&gt;")
                    .replace("\"", "&quot;")
                    .replace("'", "&#39;");
        }

        // ======================== 实体类(Java 8 标准实现) ========================
        /**
         * 表信息实体
         */
        static class TableInfo {
            private String tableName;
            private String tableRemark;
            private List<ColumnInfo> columnList;

            public TableInfo(String tableName, String tableRemark) {
                this.tableName = tableName;
                this.tableRemark = tableRemark;
            }

            // Getter & Setter(Java 8 标准写法)
            public String getTableName() { return tableName; }
            public String getTableRemark() { return tableRemark; }
            public List<ColumnInfo> getColumnList() { return columnList; }
            public void setColumnList(List<ColumnInfo> columnList) { this.columnList = columnList; }
        }

        /**
         * 列信息实体
         */
        static class ColumnInfo {
            private String columnName;
            private String typeName;
            private int columnSize;
            private String isNullable;
            private String isPrimaryKey;
            private String columnRemark;

            public ColumnInfo(String columnName, String typeName, int columnSize, String isNullable, String isPrimaryKey, String columnRemark) {
                this.columnName = columnName;
                this.typeName = typeName;
                this.columnSize = columnSize;
                this.isNullable = isNullable;
                this.isPrimaryKey = isPrimaryKey;
                this.columnRemark = columnRemark;
            }

            // Getter(Java 8 标准写法)
            public String getColumnName() { return columnName; }
            public String getTypeName() { return typeName; }
            public int getColumnSize() { return columnSize; }
            public String getIsNullable() { return isNullable; }
            public String getIsPrimaryKey() { return isPrimaryKey; }
            public String getColumnRemark() { return columnRemark; }
        }
    }
}

项目依赖

复制代码
        <!-- <数据库文档生成> -->
        <!-- PostgreSQL JDBC 驱动(Java 8 兼容版) -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.2.23</version> <!-- 适配 Java 8 的稳定版 -->
        </dependency>

        <!-- HikariCP 连接池(Java 8 兼容版) -->
        <dependency>
            <groupId>com.zaxxer</groupId>
            <artifactId>HikariCP</artifactId>
            <version>3.4.5</version> <!-- 最后支持 Java 8 的版本 -->
        </dependency>

        <!-- screw 文档生成(Java 8 兼容版) -->
        <dependency>
            <groupId>cn.smallbun.screw</groupId>
            <artifactId>screw-core</artifactId>
            <version>1.0.5</version> <!-- 适配 Java 8 的版本 -->
        </dependency>

        <!-- Freemarker 模板引擎(Java 8 兼容) -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>
相关推荐
a程序小傲2 小时前
美团二面:KAFKA能保证顺序读顺序写吗?
java·分布式·后端·kafka
a努力。2 小时前
网易Java面试被问:fail-safe和fail-fast
java·windows·后端·面试·架构
计算机毕设指导62 小时前
基于微信小程序的宠物走失信息管理系统【源码文末联系】
java·spring boot·mysql·微信小程序·小程序·tomcat·宠物
小雨下雨的雨2 小时前
第7篇:Redis性能优化实战
数据库·redis·性能优化
姜太小白2 小时前
【数据库】SQLite 时间加1天的方法总结
java·数据库·sqlite
BBB努力学习程序设计2 小时前
Java异常处理机制:从基础到高级实践指南
java
曹牧2 小时前
Java:Jackson库序列化对象
java·开发语言·python
先做个垃圾出来………2 小时前
SQL字符串函数
数据库·sql
中国胖子风清扬2 小时前
Spring AI 深度实践:在 Java 项目中统一 Chat、RAG、Tools 与 MCP 能力
java·人工智能·spring boot·后端·spring·spring cloud·ai