Java连接PostgreSQL完整指南:JDBC驱动、连接池与避坑清单 简介Java连接PostgreSQL是后端开发中常见需求这份配套PDF面向需要快速掌握JDBC连接方式的Java开发者。内容围绕官方驱动下载与导入、连接URL配置、程序实现与运行结果展开系统拆解Connection、Statement、ResultSet三个核心对象的使用流程并附带数据库查询示例的具体代码。资源为单个PDF文件压缩包大小仅98KB轻量便携可直接在电脑或手机上阅读。目前已有2132人学习下载。文档从驱动获取讲到代码运行包含完整可参考的HelloWorld示例同时总结了JDBC、PostgreSQL、JDBC驱动程序等基础概念以及连接字符串、用户名密码设置、SQL语句执行和结果集遍历等关键步骤既适合初学者按步骤复现也可作为日常开发中连接PostgreSQL时的速查笔记。1. Java连PostgreSQLJDBC驱动的第一个坑不在代码里很多人以为Java连接postgresql数据库的示例代码就是把Class.forName(org.postgresql.Driver)一写DriverManager.getConnection一调就完事。真到自己动手依赖没进classpath、驱动版本和数据库对不上、URL少写一个schema参数、连接用完不关随便一个坑就能让你从“示例跑通”卡到“本地都起不来”。这篇文章按实际落地路径走一遍从引入驱动、最小连接、增删改查到连接池封装和避坑清单。适合刚在Java项目里接PostgreSQL的开发者也适合维护老代码时被JDBC坑过的人——照着改至少不会再犯我已经踩过的那些低级错误。2. 搭环境与引入驱动让第一个JDBC连接跑起来的完整步骤2.1 先搞懂三件事驱动、URL、JDBC版本Java连接PostgreSQL走的是JDBCJava Database Connectivity这一套标准接口。数据库厂商负责提供“驱动”把JDBC调用翻译成PostgreSQL的线上协议。所以你写的连接代码其实是同一套DriverManager/Connection/Statement换数据库时只需要换驱动jar和URL。这也是很多人把MySQL的代码改成PostgreSQL时经常只改URL、结果被Druid或HikariCP的配置坑住的原因——连接串后面那些参数很多是数据库特有的。PostgreSQL官方驱动的主类叫org.postgresql.DriverMaven坐标是org.postgresql:postgresql。选版本有个原则尽量选和数据库主版本匹配的稳定版。比如数据库是旧版13驱动太新一般也能连但反过来数据库很新、驱动却停留在两三年前就可能握手失败或者报unsupported startup message。我一般会先看驱动发布页的“兼容性”那段描述再决定版本而不是永远用最新。另外JDBC版本方面Java 8用的JDBC 4.2和Java 11/17用的JDBC 4.3驱动都兼容但代码写法上LocalDateTime、setObject这类接口在JDBC 4.2之后用起来更顺手老代码里的java.sql.Timestamp也能继续跑只是要多做转换。URL格式是这个样子的jdbc:postgresql://host:port/database?param1valueparam2value默认端口是5432本地直连可以写127.0.0.1:5432。如果数据库在Docker映射了端口端口号就跟着映射走。URL后面跟参数的方式是?开始、连接这跟MySQL的useUnicodetruecharacterEncodingutf8那套很像但参数名和含义完全不同。PostgreSQL最常见的是currentSchema、connectTimeout、ssl这一组后面章节会逐个说。2.2 Maven与Gradle依赖别把驱动装到JDK里现在Java项目基本都走构建工具最省事的做法是把驱动作为项目依赖。Maven的pom.xml里加入dependency groupIdorg.postgresql/groupId artifactIdpostgresql/artifactId version使用当前稳定版/version /dependencyGradle则是在build.gradle里写implementation org.postgresql:postgresql:使用当前稳定版这里我没有写死版本号因为驱动版本更新频繁直接用你查询到的当前稳定版即可。如果你项目的编译目标Java版本比较旧比如还在Java 8那要额外看一眼驱动要求的Java最低版本高版本的驱动也可能要求Java 11。这个在依赖下载页的meta信息里能看到别等运行时报UnsupportedClassVersionError才回头。如果项目没上构建工具只能手动放jar比如老派web项目那就把postgresql-*.jar放进WEB-INF/lib或者放到应用服务器的lib目录。这里有个常见误操作把驱动jar丢进${JAVA_HOME}/jre/lib/ext想着“全局加载”。我见过有人这么干确实能跑但换了服务器环境就忘而且如果容器里有两份驱动类加载器会随机选一个版本错乱时特别难排查。所以能走构建工具就走构建工具实在手放的请确保整个classpath里只有一份驱动jar。依赖加好之后可以先执行编译并看依赖树确认驱动真的进来了。Maven项目跑mvn dependency:tree -Dincludesorg.postgresql:postgresql如果列出了版本说明依赖没问题。这步能帮你排除“我明明写了依赖但代码找不到类”的玄学问题。2.3 最小连接代码从DriverManager开始依赖就绪后可以写一个最简连接示例用来验证环境import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class QuickConnect { public static void main(String[] args) { String url jdbc:postgresql://127.0.0.1:5432/postgres; String user postgres; String password postgres; try (Connection conn DriverManager.getConnection(url, user, password)) { System.out.println(连接成功当前数据库: conn.getCatalog()); } catch (SQLException e) { e.printStackTrace(); } } }这段代码里有个细节JDBC 4.0之后驱动只要在classpath里DriverManager会自动加载它所以不用写Class.forName(org.postgresql.Driver)。很多老教程还在写那行写上也不算错但对现代驱动来说已经多余。我自己的习惯是不写Class.forName用try-with-resources管理Connection这样连接必然被关闭不会因为异常路径漏关。try-with-resources是Java 7以后的语法括号里创建的Connection实现了AutoCloseable代码块结束后自动调用close()。对于连接对象这是最不容易出错的写法。后面正文里的所有示例都用这个写法。conn.getCatalog()返回的是当前连接的数据库名对PostgreSQL来说就是URL里/后面的那个名字。用它确认连接串没配错。如果运行后报No suitable driver found先别怀疑代码去查classpath里驱动到底在不在。如果报连接超时则去查网络和数据库的pg_hba.conf这些在避坑章节里展开。2.4 连接参数schema、超时、ssl这些别用默认值一个能连接的URL只是起点实际项目中至少要关心下面几个参数参数示例作用currentSchemacurrentSchemapublic指定默认schema避免每次操作都写schema前缀connectTimeoutconnectTimeout10建连超时秒数超时快速失败socketTimeoutsocketTimeout300读取超时秒数避免连接假死ApplicationNameApplicationNameerp-service设置应用名方便在数据库侧看来源sslsslmoderequire是否要求SSL内网常不开启currentSchema是PostgreSQL特有的概念。一个数据库下面可以有多个schema默认是public。如果代码里写SELECT * FROM users实际查的是当前schema下的users如果业务把表建在别的schema下不加这个参数就得写SELECT * FROM s1.users不仅啰嗦还容易在联表时混错schema。在连接串上指定当前schema能让SQL干净很多jdbc:postgresql://127.0.0.1:5432/erp?currentSchemasalesconnectTimeout10socketTimeout300注意schema名如果有大小写或特殊字符URL里需要URL编码普通小写没问题。connectTimeout和socketTimeout的单位都是秒。connectTimeout默认可能是0即无限等待生产环境建议至少设10秒这样数据库不可达时应用能快速报错而不是所有线程都挂在建立连接上。SSL这一项内外网差异大。公司内网数据库一般不开SSLURL不需要带sslmode。云数据库或跨公网访问时至少要sslmoderequire更严格可以用verify-full做证书校验。这里提醒如果你在内网强制sslmoderequire而服务器没开SSL连接会直接失败别到时候怪代码。除了URL参数还可以在PostgreSQL的服务端配置里信任内部网络避免每次连接都要求密码。但项目里原则上仍要传用户名密码代码示例中只是作为变量实际别把密码硬编码进源码。3. 增删改查示例PreparedStatement与ResultSet的实用写法3.1 查询PreparedStatement如何防注入又保性能连接是基础真正干活的还是SQL执行。Java里发SQL有两种方式Statement和PreparedStatement。前者直接把字符串拼进SQL有注入风险还因为每次执行都要重新解析性能差后者是预编译用占位符?留参数PostgreSQL收到后先解析再绑定参数。我建议所有业务SQL都走PreparedStatement不管是查询还是更新。一个典型的分页查询长这样String sql SELECT id, username, email, created_at FROM users WHERE status ? ORDER BY id DESC LIMIT ? OFFSET ?; try (Connection conn Db.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, 1); ps.setInt(2, 20); ps.setInt(3, 0); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Long id rs.getLong(id); String username rs.getString(username); String email rs.getString(email); Timestamp createdAt rs.getTimestamp(created_at); System.out.println(id username email createdAt); } } } catch (SQLException e) { e.printStackTrace(); }这段代码有几个关键点。第一通过prepareStatement让SQL先在数据库端编译setInt、setString按位置绑定参数参数值不会跟SQL字符串拼接天然防注入。第二limit ? offset ?这两个占位符也能用setInt绑定PostgreSQL的limit表达式允许参数化这没问题。第三ResultSet也放进try-with-resources里否则它在异常时不一定马上关闭即使Connection关了某些驱动也会延迟释放结果集资源。读取字段时我建议按列名rs.getLong(id)而不是下标rs.getLong(1)。列名可读性好而且如果SELECT的字段顺序变了按列名的代码不用改。但如果SQL里有JOIN两个表都有id就要用别名区分比如SELECT u.id AS user_id不然getLong(id)会拿到第一个匹配的列很容易踩坑。3.2 更新INSERT、UPDATE、DELETE与返回自增主键写操作和查询的区别主要在方法executeUpdate()而不是executeQuery()返回是一个整数表示影响的行数。插入时经常需要拿到数据库生成的自增主键比如BIGSERIAL或IDENTITY列那就要在prepareStatement时显式声明要返回键String insertSql INSERT INTO users(username, email) VALUES (?, ?); try (Connection conn Db.getConnection(); PreparedStatement ps conn.prepareStatement(insertSql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, alice); ps.setString(2, aliceexample.com); int affected ps.executeUpdate(); if (affected 0) { try (ResultSet keys ps.getGeneratedKeys()) { if (keys.next()) { long newId keys.getLong(1); System.out.println(新用户ID: newId); } } } }Statement.RETURN_GENERATED_KEYS这个第二个参数告诉驱动执行完后把自增键找回来。getGeneratedKeys()返回一个ResultSet里面就是数据库返回的主键。注意不同数据库实现有差异在PostgreSQL上配合SERIAL和IDENTITY列通常没问题但如果你用insert ... on conflict do nothing且插入因冲突没成功这个结果集里就没有键所以在if (keys.next())前面要判断affected 0。更新和删除的写法类似String updateSql UPDATE users SET email ? WHERE id ?; try (PreparedStatement ps Db.getConnection().prepareStatement(updateSql)) { ps.setString(1, newexample.com); ps.setLong(2, 1L); int updated ps.executeUpdate(); if (updated 0) { System.out.println(记录不存在或没有变化); } }这里有个容易忽略的行为PostgreSQL默认executeUpdate返回的是“匹配到并更新的行数”不是精确的“修改了值的行数”。如果新值跟旧值一样PostgreSQL的UPDATE默认还是会计数这点跟MySQL的默认行为不同写业务逻辑别把返回值当成“值真的变了”。3.3 时间类型读写LocalDateTime与timestamp的映射JDBC里历史遗留的时间类型让人很头疼。java.sql.Date、java.sql.Timestamp继承自java.util.Date很多人图省事直接用它们但在Java 8项目中我更推荐用java.time包下的类型。PostgreSQL JDBC驱动对java.time支持已经很成熟setObject和getObject可以直接处理String sql INSERT INTO events(event_time) VALUES (?); try (PreparedStatement ps Db.getConnection().prepareStatement(sql)) { ps.setObject(1, LocalDateTime.now()); ps.executeUpdate(); } String query SELECT event_time FROM events WHERE id ?; try (PreparedStatement ps Db.getConnection().prepareStatement(query)) { ps.setLong(1, 1L); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { LocalDateTime eventTime rs.getObject(event_time, LocalDateTime.class); } } }分页里用了Timestamp是因为示例代码保留老风格。新代码我建议用LocalDateTimesetObject和getObject都能自动映射。但这里有个容易忽略的点PostgreSQL有两种时间类型。timestamp without time zone存的是“墙上时间”不带时区timestamp with time zonetimestamptz存的是即时时间点内部统一转成UTC存储。如果列是timestamptz驱动返回OffsetDateTime更合适如果你强行取LocalDateTime驱动会按数据库会话时区转换容易产生时区偏差。我的建议是业务内部统一用LocalDateTime存“墙上时间”存timestamptz就配合OffsetDateTime显式带时区。写成ps.setObject(1, OffsetDateTime.now())读取时也按OffsetDateTime.class读避免本地时区和服务端时区不一致导致莫名其妙差8小时。3.4 事务控制手动commit与rollback默认情况下DriverManager.getConnection拿到的连接是自动提交的每条SQL立刻生效。但业务上经常需要“要么都成功要么都失败”这时候要关闭自动提交try (Connection conn Db.getConnection()) { conn.setAutoCommit(false); try { // 例创建订单同时扣库存 String orderSql INSERT INTO orders(user_id, total) VALUES (?, ?); try (PreparedStatement ps conn.prepareStatement(orderSql)) { ps.setLong(1, 1001L); ps.setBigDecimal(2, new BigDecimal(99.00)); ps.executeUpdate(); } String stockSql UPDATE products SET stock stock - ? WHERE id ? AND stock ?; try (PreparedStatement ps conn.prepareStatement(stockSql)) { ps.setInt(1, 1); ps.setLong(2, 77L); ps.setInt(3, 1); int affected ps.executeUpdate(); if (affected 0) { throw new SQLException(库存不足); } } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }这里有几个坑。第一setAutoCommit(false)之后所有executeUpdate都进入同一个事务直到commit或rollback。第二一旦出现异常必须rollback否则事务会一直挂着占着连接和数据库锁。第三finally块里恢复自动提交这步很关键——如果连接返回连接池或者被复用自动提交状态不恢复会让下一个人用这个连接时所有SQL都神奇地不生效。第四持有连接期间不要做耗时的外部调用事务里握着锁时间越长越容易造成数据库锁等待。4. 避坑清单Java连PostgreSQL常见的5个翻车现场4.1 现象ClassNotFoundException与No suitable driver found这是我见过新手遇到最多的报错。ClassNotFoundException: org.postgresql.Driver说明classpath里根本没有驱动包。SQLException: No suitable driver found for jdbc:postgresql://...说明驱动类虽然存在但DriverManager不认识这个URL——常见原因是URL的协议名称写错了比如写成jdbc:postgres://或者把MySQL的jdbc:mysql://改了一半。原因大多数是依赖没真正加入或打war包时没把驱动jar打进去再或者是依赖的scope选了provided运行时容器里没有。比如Maven依赖写了scopeprovided/scope在可执行jar里不会带上驱动。另一种情况是多个驱动共存时PostgreSQL驱动不是默认首个被加载的但DriverManager仍然能识别所以归根结底还是classpath问题。解决先跑mvn dependency:tree -Dincludesorg.postgresql:postgresql确认依赖再检查打包插件的mainClass和shade配置。如果是Spring Boot项目看看BOOT-INF/lib里有没有驱动jar。我建议所有连接串统一用jdbc:postgresql://这个协议名是驱动的注册标识写错一个字母就会变成“No suitable driver”。4.2 现象连接超时但psql能连上本地用psql -h 127.0.0.1 -U postgres能连Java一跑就connect timed out或者Connection refused。这两个错误还不一样Connection refused说明端口没开或IP不对Operation timed out说明包发出去但没人应答通常是防火墙或网络策略拦截。原因最常见的是pg_hba.conf里只允许了127.0.0.1/32而Java进程跑在容器或另一台机器上来源IP不在白名单。其次PostgreSQL默认监听localhost如果数据库跑在Docker里只映射了端口外部IP进不来。还有一种是java进程自己所在环境的出站防火墙拦了5432端口。解决先分清是refused还是timeout。refused时检查ss -lnt | grep 5432看数据库监听地址timeout时检查防火墙或安全组。临时验证可以关掉防火墙后测试但关键还是调整pg_hba.confhost all all 10.0.0.0/8 md5相应的要在URL里设置connectTimeout5让失败快速暴露否则默认可能等很久。注意改了pg_hba.conf要重载SELECT pg_reload_conf();不用重启进程。4.3 现象中文变问号或数据库里已经是乱码Java读出来是???或者往里写再读就变成?。这个坑我在老项目里踩过。PostgreSQL建库时如果不指定编码很多模板默认是UTF8但老库可能是SQL_ASCII或LATIN1。SQL_ASCII不检查编码合法性字符一旦写入再按UTF-8读就全乱。原因数据库本身的编码不是UTF8连接时又没有正确声明客户端编码。PostgreSQL JDBC驱动默认会按数据库的client_encoding来但SQL_ASCII下不做转换Java侧按UTF-8解码就出问题。还有可能是应用服务器的默认文件编码被改了但这种情况少。解决先把新建数据库的编码固定为UTF8CREATE DATABASE demo ENCODING UTF8 LC_COLLATE zh_CN.UTF-8 LC_CTYPE zh_CN.UTF-8 TEMPLATE template0;连接串上可以显式加参数但PostgreSQL的编码参数名不是characterEncoding而是characterEncodingUTF8也可以驱动兼容这个名更规范的是直接让数据库默认UTF8连接不指定也是UTF8。检查一下当前数据库编码SHOW server_encoding;如果是SQL_ASCII就算你在URL加了characterEncodingUTF8存进去的字节也未必是对的正确做法是重建库。数据迁移的话先pg_dump再用UTF8库pg_restore别想在原库上改。4.4 现象时间比本地时间差8小时写入数据库的时间读出来发现比本地时间少8小时或者反过来。这几乎是PostgreSQL新手必经的坑。原因在于数据库时区、JDBC会话时区、Java时区三者没有对齐。比如数据库服务器时区是UTCJava跑在东八区列类型是timestamp without time zone驱动写入时按LocalDateTime字节直接存但读出来时驱动会结合会话时区转成TimestampJava再按本地时区格式化就会出现偏移。解决最直接的办法是连接串上指定会话时区jdbc:postgresql://127.0.0.1:5432/app?TimeZoneAsia/ShanghaiTimeZone这个参数驱动支持它会把timestamp类型的读取和写入都在该时区下解释。如果列用的是timestamptz带时区那么无论会话时区是什么内部都存UTC时间戳读取时转成指定时区反而更可靠。我现在的习惯是业务字段能用timestamptz就用它Java侧统一用OffsetDateTime或Instant这样时区问题从源头消失。如果你还在用旧的java.sql.Timestamp那就确保连接串TimeZone和JVM默认时区一致否则永远差着那几小时。还有一个隐蔽点PostgreSQL驱动读取timestamp without time zone时会把数据库端“无时区”的时间强行当成“当前时区”解释。如果你数据库的timezone配置是UTC而连接串没指定本地读出来就会少8小时。用SELECT now()和Java打印的new Date()对比如果差整小时基本就是这个原因。4.5 现象应用卡死数据库连接数被耗尽线上应用跑着跑着突然全部请求超时数据库侧看到too many connections。查应用日志经常是“从连接池获取不到连接”。这个坑绝大多是资源泄漏导致的。原因代码里开了Connection、Statement、ResultSet但只在finally里关了Connection漏关了Statement和ResultSet或者干脆连Connection都没关。连接池的作用是管理连接但如果你自己不关Connection池里的连接被借走不还池很快就空。另外如果配了minimumIdle和maximumPoolSize不合理或者maxLifetime过长数据库端对几十年不动的连接也会超时断开池里还存着无效连接取出来就报错。解决把所有JDBC对象都放进try-with-resources或者用工具类统一封装确保ResultSet→Statement→Connection按顺序关闭。同时给连接池设置合理的校验和回收config.setMaximumPoolSize(10); config.setMinimumIdle(5); config.setMaxLifetime(1800000); config.setValidationTimeout(5000); config.setConnectionTestQuery(SELECT 1);maxLifetime设为30分钟低于数据库tcp_keepalives_idle的默认值让池定期丢弃旧连接避免被数据库单方面切断。排查时在数据库侧执行SELECT pid, usename, application_name, state, now() - backend_start AS duration FROM pg_stat_activity WHERE datname app;看到一堆陈旧连接优先排查是不是哪条代码路径漏了close()。5. 连接池与工具类封装从一次性连接到生产可用5.1 为什么生产环境不用DriverManager前面所有示例都用DriverManager.getConnection()这是理解JDBC的起点。但生产环境里每个请求都新建连接、用完关闭开销很大。PostgreSQL建连要TCP握手、认证、准备会话大概几毫秒到几十毫秒虽然不比网络调用那么贵但在高并发下会成为瓶颈。更重要的是连接不可重用数据库要维护大量的短暂连接连接数一旦被拉高新来的请求直接排队。连接池的想法很简单启动时预先创建一小批连接放进池里业务要连时从池里借用完还回去池自己负责创建、回收、保活。Java生态里最常用的连接池是HikariCP它性能好、配置少PostgreSQL官方文档也把它列为推荐方案。所以在项目里封装一个基于HikariCP的DataSource比到处写DriverManager要靠谱得多。5.2 用HikariCP配置一个实用的连接池引入HikariCPMaven依赖很简单dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version使用当前稳定版/version /dependency然后在程序里创建数据源HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:postgresql://127.0.0.1:5432/app?currentSchemapublicTimeZoneAsia/Shanghai); config.setUsername(app); config.setPassword(请在配置中心或环境变量中读取); config.setMaximumPoolSize(10); config.setMinimumIdle(5); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); config.setPoolName(pg-pool); config.setConnectionTestQuery(SELECT 1); HikariDataSource dataSource new HikariDataSource(config);这些参数是连接池的核心我给出自己常用的初始值参数建议值理由maximumPoolSize10根据应用并发和数据库能力调不是越大越好minimumIdle5保持少量空闲连接避免突发性能抖动connectionTimeout30000获取连接等待上限单位毫秒maxLifetime1800000连接最大存活30分钟防止数据库断开无效连接connectionTestQuerySELECT 1取连接时校验连接是否可用注意PostgreSQL JDBC本身就支持setConnectionTestQuery但如果已知连接池会校验也可以在URL上加sslmode等参数。这里TimeZoneAsia/Shanghai是利用URL参数正好解决前面时区差8小时的问题不需要在Java代码里到处传时区。5.3 封装一个线程安全的连接工具类有了连接池代码里不应该再直接new HikariDataSource而是把数据源做成单例提供静态方法获取连接。public final class Db { private static final HikariDataSource DS create(); private Db() { } public static Connection getConnection() throws SQLException { return DS.getConnection(); } private static HikariDataSource create() { HikariConfig config new HikariConfig(); // 与上面一致可读配置文件 config.setJdbcUrl(jdbc:postgresql://127.0.0.1:5432/app); config.setUsername(app); config.setPassword(System.getenv(DB_PASSWORD)); config.setMaximumPoolSize(10); return new HikariDataSource(config); } }使用方只需要try (Connection conn Db.getConnection()) { // 执行业务SQL }这样整个项目只初始化一次连接池线程安全由HikariDataSource保证业务代码不用关心连接创建和销毁。但这只是基础封装更高一层的做法是用DAO模式或ORM框架。如果团队已经用了Spring Boot直接用spring.datasource配置更省事这里手写工具类适合无框架的轻量项目。5.4 验证连接池是否正常工作封装完之后别急着提交代码先做个简单验证。写一个测试方法循环获取并归还连接try (Connection conn Db.getConnection()) { try (Statement st conn.createStatement(); ResultSet rs st.executeQuery(SELECT 1)) { if (rs.next()) { System.out.println(连接池校验通过: rs.getInt(1)); } } }然后监控连接池的活跃连接数。HikariCP自带JMX可用jconsole连上去看Pool:pg-pool的几个指标ActiveConnections、IdleConnections、PendingConnections。正常情况调用后ActiveConnections回落到0IdleConnections恢复到minimumIdle。如果ActiveConnections只增不减说明业务层依然存在连接泄漏回到第4.5节查。一个我自己的习惯在测试环境用wrk或JMeter压20个并发观察PendingConnections是否增加。如果PendingConnections一直涨说明连接池真的不够用或者某个慢SQL持有连接的时间太长。这时候别急着调大maximumPoolSize先看SQL执行计划和事务范围。连接池不是越大越好每个连接对应PostgreSQL后端进程连接数太多会让数据库CPU和内存同时飙升。最后说一句血泪经验我从第一个“示例跑通”到线上稳定中间隔着一个连接池和一个try-with-resources。早期项目里为了省事直接new Connection结果某次发版后并发一高数据库连接数被打满整个服务像被点了穴一样。后来统一改成HikariCP 强制try-with-resources同样的SQL压力翻一倍也没再出过“too many connections”。现在回头看那段老代码根本问题不在JDBC示例本身而是没人把资源关闭当回事。希望这篇笔记帮到你少走我已经走过的弯路。本文还有配套的精品资源点击获取