Spring MVC中Java Record抛出CustomException未被正确捕获的问题
问题根源
Spring在将请求体绑定到Java Record实例时,会通过反射调用构造器。如果构造器抛出异常,Spring会把它包装成BeanInstantiationException,导致你的@RestControllerAdvice无法直接捕获到自定义的CustomException,最终返回500状态码。
解决方案与最佳实践
1. 优先用Spring Validation注解(最推荐)
这是符合Spring生态的最优方案,无需手动写校验逻辑,直接借助JSR-380注解完成参数校验:
- 给Record字段添加
@NotNull等校验注解 - 控制器方法参数上标记
@Valid触发校验 - 全局异常处理器捕获
MethodArgumentNotValidException,返回440状态码
示例代码:
// 请求Record public record RequestRecord(@NotNull(message = "s1不能为空") String s1, @NotNull(message = "s2不能为空") String s2) {} // 控制器 @RestController public class TestController { @PostMapping("/test") public ResponseEntity<String> test(@Valid @RequestBody RequestRecord request) { return ResponseEntity.ok("success"); } } // 全局异常处理器 @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<String> handleValidationException(MethodArgumentNotValidException e) { String errorMsg = e.getBindingResult().getFieldErrors().stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(",")); return ResponseEntity.status(440).body(errorMsg); } }
2. 自定义参数解析器捕获构造器异常
如果必须在Record构造器里手动抛CustomException,可以自定义参数解析器,绕开Spring的默认实例化逻辑,直接捕获原始异常:
示例代码:
// 自定义参数解析器 @Component public class RecordArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.getParameterType().isRecord(); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); ObjectMapper objectMapper = new ObjectMapper(); try { String requestBody = request.getReader().lines().collect(Collectors.joining()); return objectMapper.readValue(requestBody, parameter.getParameterType()); } catch (JsonProcessingException e) { throw new CustomException("请求格式错误"); } catch (InvocationTargetException e) { // 提取构造器抛出的自定义异常 if (e.getTargetException() instanceof CustomException customException) { throw customException; } throw e; } } } // 注册参数解析器 @Configuration public class WebConfig implements WebMvcConfigurer { @Autowired private RecordArgumentResolver recordArgumentResolver; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { resolvers.add(recordArgumentResolver); } } // 带手动校验的Record public record RequestRecord(String s1, String s2) { public RequestRecord { if (s1 == null) throw new CustomException("s1不能为空"); if (s2 == null) throw new CustomException("s2不能为空"); } } // 全局异常处理器捕获CustomException @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(CustomException.class) public ResponseEntity<String> handleCustomException(CustomException e) { return ResponseEntity.status(440).body(e.getMessage()); } }
3. 从BeanInstantiationException中提取原始异常
如果不想修改参数解析逻辑,也可以在全局异常处理器中直接处理Spring包装后的异常,提取内部的CustomException:
示例代码:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BeanInstantiationException.class) public ResponseEntity<String> handleBeanInstantiationException(BeanInstantiationException e) { Throwable cause = e.getCause(); if (cause instanceof CustomException customException) { return ResponseEntity.status(440).body(customException.getMessage()); } // 其他情况返回500 return ResponseEntity.status(500).body("服务器内部错误"); } }
最佳实践总结
- 优先使用Spring Validation注解:代码简洁、符合规范,彻底避免异常包装问题
- 尽量不在Record构造器中直接抛业务异常:Spring的参数绑定机制会自动包装异常,增加处理复杂度
- 若必须手动校验,优先选择自定义参数解析器的方式,直接捕获原始异常,保证异常处理的准确性
- 全局异常处理器要考虑异常的嵌套包装情况,确保能正确识别业务异常并返回对应状态码
内容的提问来源于stack exchange,提问作者uicheon
相关产品推荐
相关产品推荐

