升级Spring Boot 3.1.1后所有API调用出现TimeZoneNames资源缺失异常
Spring Boot 3.1.1 + Zing Base Java 17 升级后响应追加错误的排查与解决
问题背景
将Spring Boot从2.x版本升级至3.1.1,同时将Java从8升级为Zing Base Java 17后,部分节点上所有API调用出现异常:API本身可正常执行,但实际响应后会追加额外的错误响应,导致客户端报错。该问题甚至在actuator/health接口中也会出现。
响应示例
{"status":"UP"}{"errorCode":"MYAPP-1000","errorMessage":"Can't find bundle for base name sun.util.resources.cldr.TimeZoneNames, locale en_US"}
自定义异常处理器代码
@ExceptionHandler(value = Exception.class ) public ResponseEntity handleGenericException(Exception exception) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(cacheRefreshService.buildErrorResponse("MYAPP-1000", exception.getMessage())); }
核心异常信息
- 根异常:
java.util.MissingResourceException(提示找不到sun.util.resources.cldr.TimeZoneNames资源包) - 伴随异常:
org.springframework.http.converter.HttpMessageNotWritableException
问题原因
- Zing JDK资源包差异:Zing Base Java 17与标准OpenJDK的资源包结构存在区别,
sun.util.resources.cldr.TimeZoneNames是OpenJDK中CLDR时区名称资源的路径,但Zing可能未包含该资源包,或资源包的命名/存储路径不同。 - 响应流二次写入:API正常执行并写入响应后,Spring在请求收尾阶段(如上下文清理、时区相关处理)触发了
MissingResourceException,全局异常处理器捕获该异常后,向已写入内容的响应流追加了错误JSON,导致响应内容拼接混乱。 - 全局异常处理器范围过广:
@ExceptionHandler(value = Exception.class)捕获了所有异常,包括Spring内部在请求处理后期抛出的底层资源异常,这类异常本不应被暴露给客户端。
解决方法
1. 缩小全局异常处理器的捕获范围
避免捕获所有Exception,仅处理业务相关的自定义异常或预期异常,排除Spring内部框架级异常:
// 仅捕获自定义业务异常 @ExceptionHandler(value = BusinessException.class ) public ResponseEntity handleBusinessException(BusinessException exception) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(cacheRefreshService.buildErrorResponse("MYAPP-1000", exception.getMessage())); } // 针对性处理常见运行时异常 @ExceptionHandler(value = {IllegalArgumentException.class, IllegalStateException.class}) public ResponseEntity handleCommonExceptions(RuntimeException exception) { return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(cacheRefreshService.buildErrorResponse("MYAPP-2000", exception.getMessage())); }
2. 补充Zing JDK缺失的时区资源
若必须依赖sun.util.resources.cldr.TimeZoneNames相关功能,可手动补充资源:
- 从OpenJDK 17发行包中提取
sun.util.resources.cldr相关的资源文件(.properties或.class) - 将这些资源打包成JAR,放入应用类路径
- 检查Zing JDK配置,确认是否有启用CLDR资源的开关,开启对应配置
3. 替换Sun内部API依赖
检查应用中是否直接/间接依赖sun.util.resources.cldr.TimeZoneNames的代码,替换为标准JDK API,避免依赖非公开的Sun内部类:
// 使用标准API获取时区名称 ZoneId zoneId = ZoneId.of("America/New_York"); String displayName = zoneId.getDisplayName(TextStyle.FULL, Locale.US);
4. 避免向已提交的响应流写入内容
在全局异常处理器中判断响应是否已提交,若已提交则不再写入错误内容:
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(value = Exception.class ) public ResponseEntity handleGenericException(Exception exception) { ServletResponse response = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getResponse(); // 响应已提交时,直接抛出异常由容器处理,不追加内容 if (response != null && response.isCommitted()) { throw new RuntimeException(exception); } return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(cacheRefreshService.buildErrorResponse("MYAPP-1000", exception.getMessage())); } }
内容的提问来源于stack exchange,提问作者Nishant Modi
相关产品推荐
相关产品推荐

