You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 00:07:03