Spring Boot应用未返回预期错误响应问题求助
Spring Boot饰品管理API参数校验异常排查问题
运行Spring Boot饰品管理应用时,使用Insomnia测试REST API发现:向数字类型字段传入字符串(如测试JSON中weight字段传"twenty"),未返回预期的参数校验错误,反而返回500内部服务器错误,请求帮忙排查。
相关代码
OrnamentController
package com.example.demo.controller; import java.util.Random; import java.util.stream.Collectors; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import com.example.demo.model.Ornament; import com.example.demo.services.OrnamentService; import jakarta.validation.Valid; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @RestController public class OrnamentController { private final OrnamentService ornamentService; public OrnamentController(OrnamentService ornamentService) { this.ornamentService = ornamentService; } @GetMapping("/listAllOrnaments") public Flux<Ornament> listAllOrnaments() { Flux<Ornament> randomOrnaments = Flux.fromStream( new Random() .ints(10) .mapToObj(i -> generateRandomOrnament()) ); return randomOrnaments; } private Ornament generateRandomOrnament() { Random random = new Random(); String name = "Ornament " + random.nextInt(1000); double priceMin = ornamentService.getRandomPrice(); double priceMax = priceMin + random.nextDouble() * 100; double priceMedian = (priceMin + priceMax) / 2; return new Ornament(name, 0, 0, priceMin, priceMax, priceMedian, ""); } @PostMapping("/addOrnament") public Mono<ResponseEntity<String>> addOrnament(@Valid @RequestBody Ornament ornament, BindingResult bindingResult) { if (bindingResult.hasErrors()) { String errorMessage = bindingResult.getFieldErrors().stream() .map(error -> { String fieldName = error.getField(); String errorMsg = error.getDefaultMessage(); if (fieldName.equals("price") && errorMsg.contains("NumberFormatException")) { return "Price should be a number."; } return "Validation error: Field '" + fieldName + "' " + errorMsg; }) .collect(Collectors.joining(", ")); return Mono.just(ResponseEntity.badRequest().body("Validation error: " + errorMessage)); } return ornamentService.addOrnament(ornament) .thenReturn(ResponseEntity.ok("Ornament added successfully")) .onErrorResume(ex -> Mono.just(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage()))); } @GetMapping("/error") public ResponseEntity<String> handleErrors() { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred."); } }
GlobalExceptionHandler
package com.example.demo.exception; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(S3ServiceException.class) public ResponseEntity<String> handleS3ServiceException(S3ServiceException ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage()); } @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<String> handleTypeMismatch(MethodArgumentTypeMismatchException ex) { String errorMessage; switch (ex.getName()) { case "weight": case "diamondCarat": case "priceMin": case "priceMax": case "priceMedian": errorMessage = "Validation error: Field '" + ex.getName() + "' should be a number."; break; default: errorMessage = "Validation error: Field '" + ex.getName() + "' has invalid type."; } return ResponseEntity.badRequest().body(errorMessage); } @ExceptionHandler(Exception.class) public ResponseEntity<String> handleException(Exception ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Internal server error occurred."); } }
Ornament
package com.example.demo.model; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import lombok.Data; import reactor.core.publisher.Mono; @Data public class Ornament { @NotNull private String name; @Min(0) private double weight; @Min(0) private double diamondCarat; @Min(0) private double priceMin; @Max(10) private double priceMax; @Min(0) private double priceMedian; private String currency; public Ornament(String name, double weight, double diamondCarat, double priceMin, double priceMax, double priceMedian, String currency) { this.name = name; this.weight = weight; this.diamondCarat = diamondCarat; this.priceMin = priceMin; this.priceMax = priceMax; this.priceMedian = priceMedian; this.currency = currency; } public Mono<Ornament> toMono() { return Mono.just(this); } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getWeight() { return weight; } public void setWeight(double weight) { this.weight = weight; } public double getDiamondCarat() { return diamondCarat; } public void setDiamondCarat(double diamondCarat) { this.diamondCarat = diamondCarat; } public double getPriceMin() { return priceMin; } public void setPriceMin(double priceMin) { this.priceMin = priceMin; } public double getPriceMax() { return priceMax; } public void setPriceMax(double priceMax) { this.priceMax = priceMax; } public double getPriceMedian() { return priceMedian; } public void setPriceMedian(double priceMedian) { this.priceMedian = priceMedian; } public String getCurrency() { return currency; } public void setCurrency(String currency) { this.currency = currency; } }
OrnamentService
package com.example.demo.services; import java.util.Random; import org.springframework.stereotype.Service; import com.example.demo.model.Ornament; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @Service public class OrnamentService { private final Random random = new Random(); public Flux<Ornament> listAllOrnaments() { return Flux.just(new Ornament("Ornament 1", 0, 0, 0, 0, 0, ""), new Ornament("Ornament 2", 0, 0, 0, 0, 0, ""), new Ornament("Ornament 3", 0, 0, 0, 0, 0, "")); } public Mono<Ornament> addOrnament(Ornament ornament) { return Mono.just(ornament); } public double getRandomPrice() { return 1 + (100 - 1) * random.nextDouble(); } }
测试请求JSON
{ "name": "Ornament 100", "weight": "twenty", "diamondCarat": 10, "priceMin": 20, "priceMax": 10, "priceMedian": 20, "currency": null }
问题原因及解决方案
核心原因
当请求JSON中数字字段传入字符串时,Jackson反序列化基本类型double会直接抛出JsonMappingException,这个异常发生在Spring MVC参数绑定阶段,早于@Valid校验和BindingResult处理流程,因此控制器内的错误逻辑无法触发。同时,全局异常处理器中未处理该异常,最终被兜底的Exception处理器捕获,返回500错误。
另外,MethodArgumentTypeMismatchException仅处理URL路径参数/请求参数的类型不匹配,不适用@RequestBody的JSON反序列化场景,因此该处理器无效。
解决方案
方案1:新增JSON反序列化异常处理
在GlobalExceptionHandler中添加JsonMappingException的处理器,解析错误字段并返回400错误:
import com.fasterxml.jackson.databind.JsonMappingException; // ... 其他代码 @ExceptionHandler(JsonMappingException.class) public ResponseEntity<String> handleJsonMappingException(JsonMappingException ex) { String fieldName = ex.getPath().get(0).getFieldName(); String errorMessage = String.format("Validation error: Field '%s' should be a valid number.", fieldName); return ResponseEntity.badRequest().body(errorMessage); }
方案2:将基本类型改为包装类型
把Ornament类中的double字段替换为Double(包装类型),这样反序列化失败时字段会被设为null,触发@Min(0)的校验规则,BindingResult就能捕获错误,控制器内的逻辑即可返回预期的400错误:
@Min(0) private Double weight; @Min(0) private Double diamondCarat; @Min(0) private Double priceMin; @Max(10) private Double priceMax; @Min(0) private Double priceMedian;
额外优化
- 控制器中同时使用了构造函数注入和
@Autowired,保留构造函数注入即可(更符合Spring最佳实践),删除@Autowired注解。 - 测试JSON中
priceMax设为10,符合@Max(10)规则,若要测试该校验,可将值改为11。
内容的提问来源于stack exchange,提问作者Aswin Shaji
相关产品推荐
相关产品推荐

