GlobalExceptionHandler 新增 HttpMessageNotReadableException 处理:Jackson 在进 Controller 前抛的 解析失败(如 birthday 非 yyyy-MM-dd)此前无专属 handler,穿透到 api-std 兜底渲染成 code=6「内部服务错误」。 改为从异常 path 取失败字段名,birthday 点名「字段类型需为yyyy-MM-dd」、其余走通用文案,统一复用 paramError 返回 code=4。空串/null 由 Jackson 归一为 null 不抛,满足「空不管」。附单测 2 例。
This commit is contained in:
@@ -1,7 +1,9 @@
|
|||||||
package com.yunhai.userbase.common;
|
package com.yunhai.userbase.common;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
import org.springframework.core.Ordered;
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.core.annotation.Order;
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
import org.springframework.validation.BindException;
|
import org.springframework.validation.BindException;
|
||||||
import org.springframework.validation.FieldError;
|
import org.springframework.validation.FieldError;
|
||||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
@@ -13,6 +15,7 @@ import org.springframework.web.method.annotation.MethodArgumentTypeMismatchExcep
|
|||||||
import javax.validation.ConstraintViolation;
|
import javax.validation.ConstraintViolation;
|
||||||
import javax.validation.ConstraintViolationException;
|
import javax.validation.ConstraintViolationException;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -66,6 +69,27 @@ public class GlobalExceptionHandler {
|
|||||||
return paramError("参数类型不匹配: " + ex.getName());
|
return paramError("参数类型不匹配: " + ex.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求体反序列化失败(Jackson 在进 Controller 前抛出,@Valid 兜不到)。典型如 birthday 等非空字段格式非法:
|
||||||
|
* 从异常最内层 {@link JsonMappingException} 的 path 取失败字段名,birthday 给点名提示,其余走通用文案。
|
||||||
|
* 空串/null 由 Jackson JSR-310 归一为 null、不抛,故不会误入此分支(满足「空不管」)。
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(HttpMessageNotReadableException.class)
|
||||||
|
public Map<String, Object> handleNotReadable(HttpMessageNotReadableException ex) {
|
||||||
|
String field = null;
|
||||||
|
Throwable cause = ex.getMostSpecificCause();
|
||||||
|
if (cause instanceof JsonMappingException) {
|
||||||
|
List<JsonMappingException.Reference> path = ((JsonMappingException) cause).getPath();
|
||||||
|
if (path != null && !path.isEmpty()) {
|
||||||
|
field = path.get(path.size() - 1).getFieldName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("birthday".equals(field)) {
|
||||||
|
return paramError("birthday 字段类型需为yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
return paramError("请求参数格式错误");
|
||||||
|
}
|
||||||
|
|
||||||
private String fieldErrorsMessage(java.util.List<FieldError> fieldErrors) {
|
private String fieldErrorsMessage(java.util.List<FieldError> fieldErrors) {
|
||||||
if (fieldErrors == null || fieldErrors.isEmpty()) {
|
if (fieldErrors == null || fieldErrors.isEmpty()) {
|
||||||
return "参数校验失败";
|
return "参数校验失败";
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package com.yunhai.userbase.common;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.JsonMappingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.HttpInputMessage;
|
||||||
|
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link GlobalExceptionHandler#handleNotReadable} 单测:请求体反序列化失败的友好提示。
|
||||||
|
* 纯单元(无 DB/Spring 上下文),直接构造异常断言返回信封。
|
||||||
|
*/
|
||||||
|
class GlobalExceptionHandlerTest {
|
||||||
|
|
||||||
|
private final GlobalExceptionHandler handler = new GlobalExceptionHandler();
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void birthdayParseErrorReturnsFriendlyParamError() throws Exception {
|
||||||
|
JsonParser p = new ObjectMapper().createParser("{}");
|
||||||
|
JsonMappingException jme = JsonMappingException.from(p, "cannot deserialize LocalDate");
|
||||||
|
jme.prependPath(new JsonMappingException.Reference(null, "birthday"));
|
||||||
|
HttpMessageNotReadableException ex =
|
||||||
|
new HttpMessageNotReadableException("JSON parse error", jme, (HttpInputMessage) null);
|
||||||
|
|
||||||
|
Map<String, Object> body = handler.handleNotReadable(ex);
|
||||||
|
|
||||||
|
assertEquals(4, body.get("code"));
|
||||||
|
String msg = (String) body.get("msg");
|
||||||
|
assertTrue(msg.contains("birthday"), "msg 应点名 birthday 字段: " + msg);
|
||||||
|
assertTrue(msg.contains("yyyy-MM-dd"), "msg 应含期望格式 yyyy-MM-dd: " + msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void otherParseErrorReturnsGenericMessage() {
|
||||||
|
HttpMessageNotReadableException ex =
|
||||||
|
new HttpMessageNotReadableException("malformed JSON", (HttpInputMessage) null);
|
||||||
|
|
||||||
|
Map<String, Object> body = handler.handleNotReadable(ex);
|
||||||
|
|
||||||
|
assertEquals(4, body.get("code"));
|
||||||
|
assertEquals("请求参数格式错误", body.get("msg"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user