必须用 try-catch 处理 SQLException,记录 SQLState、errorCode 和 message;用 try-with-resources 或 finally 逆序关闭资源;按 SQLState 分类处理事务回滚;PreparedStatement 需确认预编译生效防注入。

SQLException 捕获必须用 try-catch,不能只靠 throws
Java 中数据库操作(如 Connection.createStatement()、Statement.executeUpdate())抛出的是受检异常 SQLException,编译器强制要求处理。只在方法签名加 throws SQLException 是常见偷懒做法,但实际生产中会导致上层调用链处处透传、日志缺失、事务失控。
- 务必用
try-catch包裹关键 DB 操作,至少记录e.getSQLState()、e.getErrorCode()和e.getMessage() - 不要只 catch
Exception或Throwable—— 会掩盖 SQL 特定错误语义(比如唯一约束失败是23505,外键违例是23503,PostgreSQL) - 避免空 catch 块:
catch (SQLException e) { }是线上事故高发源头
连接泄漏比 SQL 错误更隐蔽,必须显式 close
未关闭 Connection、Statement、ResultSet 会导致连接池耗尽、后续请求卡死,这类问题往往在压测或高峰时才暴露,且错误堆栈里不体现“连接已满”,只报超时或 NullPointerException。
- JDBC 4.0+ 支持 try-with-resources,优先用:
try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement("SELECT * FROM user WHERE id = ?")) { ps.setLong(1, userId); try (ResultSet rs = ps.executeQuery()) { // 处理结果 } } - 若用旧版 JDK,确保
finally块中按ResultSet → Statement → Connection逆序关闭,且每个close()都套if (x != null)和独立 try-catch - Spring 的
JdbcTemplate或 MyBatis 自动管理资源,但自定义DataSourceUtils.getConnection()后仍需手动 release
事务回滚不能只依赖 catch,要检查 SQLState 和 errorCode
不是所有 SQLException 都该回滚。比如查询超时(SQLState = "57014")可能只需重试,而主键冲突("23505")通常应转业务逻辑处理,而非无脑 rollback。
- Spring 声明式事务默认对
RuntimeException回滚,但SQLException是受检异常,需显式配置@Transactional(rollbackFor = SQLException.class) - 手动事务中,别写
if (e instanceof SQLException) tx.rollback();—— 应解析e.getSQLState()判断是否属于数据一致性破坏类错误 - HikariCP 等连接池在连接异常中断时可能抛
PooledConnection#checkException,这类底层异常的SQLState可能为空,需额外判空
PreparedStatement 预编译失效时,SQL 注入风险仍在
很多人以为用了 PreparedStatement 就绝对安全,但若拼接表名、列名、ORDER BY 字段等非参数化部分,依然可被注入。更隐蔽的是:某些 JDBC 驱动(如旧版 MySQL Connector/J)在 useServerPrepStmts=false 时,实际走字符串拼接,setString() 无效。
立即学习“Java免费学习笔记(深入)”;
- 检查驱动行为:打印
ps.toString(),看是否含真实参数值(如PreparedStatement: SELECT * FROM user WHERE name = 'admin'),说明预编译未生效 - 确认连接 URL 含
useServerPrepStmts=true&cachePrepStmts=true(MySQL),或prepareThreshold=1(PostgreSQL) - 动态 SQL 场景(如多条件查询)用 MyBatis 的
或 jOOQ,别手拼StringBuilder.append(" AND ").append(fieldName)
事务边界、连接生命周期、SQL 异常分类这三件事一旦耦合错位,问题就很难复现和定位。尤其在分布式事务或连接池混用场景下,SQLException 的根因往往藏在上一个请求的未关闭资源里。











