spring boot默认sqlexceptiontranslator无法区分可重试与需告警场景,因其仅返回泛化500响应;必须自定义sqlexceptiontranslator实现类并声明@primary bean,递归解析异常链获取原始oracle异常(如ora-12545),按错误语义映射http状态码(如503/400),同时需协同驱动版本、jdbc参数与连接池配置。
spring boot 默认的 sqlerrorcodesqlexceptiontranslator 对 oracle 错误码有一定支持,但面对 sqlrecoverableexception、连接超时、tns解析失败等真实生产问题,它只返回泛化的 500 响应,无法区分“可重试”和“需告警”的场景。必须用 exceptiontranslator 体系做定制化转换。
为什么不能只靠 @ExceptionHandler(SQLException.class)
直接在 @RestControllerAdvice 里捕获 SQLException 看似简单,但会漏掉关键信息:
-
SQLException的子类(如SQLRecoverableException)可能被 Spring JDBC 层提前包装成DataAccessResourceFailureException或CannotGetJdbcConnectionException,原始异常链被截断 - Oracle 同一错误码(如 ORA-12545)在不同驱动版本中可能对应不同子类,硬匹配类型不稳定
- 业务需要按错误语义返回不同 HTTP 状态码(如 503 表示服务暂时不可用,400 表示连接串写错),而
@ExceptionHandler只能靠人工解析 message 字符串,脆弱且难测试
如何让 ExceptionTranslator 捕获到原始 Oracle 异常
核心是把翻译器插入到 Spring JDBC 的异常翻译链最前端,而不是等异常冒泡到 Web 层。关键点:
- 自定义的
ExceptionTranslator必须实现SQLExceptionTranslator接口,并通过@Primary声明为 Bean,替换掉默认的SQLErrorCodeSQLExceptionTranslator - 不要在
@Configuration类里直接 new 实例,要交给 Spring 管理生命周期,否则afterPropertiesSet()不会被调用 - Oracle 驱动抛出的原始异常通常藏在
getCause()链里,你的translate()方法必须递归遍历Throwable.getCause()才能拿到oracle.net.ns.NetException或oracle.jdbc.driver.T4CConnection相关异常 - 示例片段:
public ApiResult translate(Throwable throwable) { SQLException sqlEx = findRootSQLException(throwable); if (sqlEx != null && sqlEx.getErrorCode() == 12545) { return ApiResult.fail(503, "数据库地址不可达,请检查 TNS 配置"); } return null; // 交由 fallback 处理 }
ORA-错误码与业务语义的映射策略
Oracle 官方文档定义了数百个 ORA- 错误码,但实际高频影响连接的不到 20 个。建议按恢复能力分组处理:
- 立即重试类:ORA-12170(TNS:Connect timeout)、ORA-12535(TNS:operation timed out)→ 返回 503 + Retry-After 头
- 需人工介入类:ORA-12545(TNS:name lookup failure)、ORA-12154(TNS:could not resolve service name)→ 返回 400,附带配置校验提示
- 资源耗尽类:ORA-01017(invalid username/password)、ORA-12516(TNS:listener could not find available handler)→ 返回 401 或 503,避免暴露敏感信息
- 注意:Oracle 12c+ 的
SQLRecoverableException并不都对应可重试操作,比如 ORA-00604(error occurred at recursive SQL level)就可能是死锁,不能盲目重试
连接池失效导致的异常容易被忽略
HikariCP 或 Druid 在连接空闲超时后未及时检测并剔除失效连接,会导致首次请求拿到一个已关闭的物理连接,抛出 java.sql.SQLNonTransientConnectionException: Closed Connection。这种异常不会触发 Oracle 错误码机制,SQLErrorCodeSQLExceptionTranslator 完全无感。必须在 ExceptionTranslator 中单独判断:
- 检查异常 message 是否包含
"Closed Connection"或"Connection is closed" - 检查异常是否为
SQLNonTransientConnectionException且 cause 是SocketException或EOFException - 这类情况应统一降级为 503,并触发连接池的
evict()操作(需注入HikariDataSource)
真正棘手的不是怎么写翻译逻辑,而是 Oracle 驱动版本、JDBC URL 参数(如 connectTimeout、socketTimeout)、连接池的 validationQuery 和 testOnBorrow 设置三者必须协同——任一环节松动,异常就会绕过你精心设计的 ExceptionTranslator。











