Jackson在处理反序列化映射期间发生的异常时有一种奇怪的行为:它抛出JsonMappingException
,其.getCause()
返回异常链的最内层。
//in main
ObjectMapper jsonMapper = new ObjectMapper();
String json = "{\"id\": 1}";
try {
Q q = jsonMapper.readValue(json, Q.class);
} catch (JsonMappingException e) {
System.out.println(e.getCause()); //java.lang.RuntimeException: ex 2
}
//class Q
public class Q {
@JsonCreator
public Q(@JsonProperty("id") int id) {
throw new RuntimeException("ex 0",
new RuntimeException("ex 1",
new RuntimeException("ex 2")));
}
}
在上面的代码中,我使用jsonMapper.readValue(..)
将字符串json
映射到类Q
的实例,其构造函数标记为@JsonCreator
,引发{{ {1}}:RuntimeException
。如果映射失败,我希望第"ex 0", "ex 1", "ex 2"
行打印出System.out.println(e.getCause());
,但会打印ex 0
。
为什么杰克逊决定这样做,有没有办法配置它以便它不会丢弃我的ex 2
?我试过了
ex 0
但它没有做任何事情。
答案 0 :(得分:6)
在杰克逊的StdValueInstantiator
内部,当反序列化期间抛出异常时,此方法会被点击:
protected JsonMappingException wrapException(Throwable t)
{
while (t.getCause() != null) {
t = t.getCause();
}
if (t instanceof JsonMappingException) {
return (JsonMappingException) t;
}
return new JsonMappingException("Instantiation of "+getValueTypeDesc()+" value failed: "+t.getMessage(), t);
}
正如您所看到的,这将遍历每个"级别"嵌套运行时异常的设置,并将它命中的最后一个设置为它返回的JsonMappingException
的原因。
以下是我需要的代码:
将新模块注册到ObjectMapper
。
@Test
public void testJackson() {
ObjectMapper jsonMapper = new ObjectMapper();
jsonMapper.registerModule(new MyModule(jsonMapper.getDeserializationConfig()));
String json = "{\"id\": \"1\"}";
try {
Q q = jsonMapper.readValue(json, Q.class);
System.out.println(q.getId());
} catch (JsonMappingException e) {
System.out.println(e.getCause()); //java.lang.RuntimeException: ex 2
} catch (JsonParseException e) {
} catch (IOException e) {
}
}
创建自定义模块类。
public class MyModule extends SimpleModule {
public MyModule(DeserializationConfig deserializationConfig) {
super("MyModule", ModuleVersion.instance.version());
addValueInstantiator(Q.class, new MyValueInstantiator(deserializationConfig, Q.class));
addDeserializer(Q.class, new CustomDeserializer());
}
}
创建自定义ValueInstantiator
类以覆盖wrapException(...)
。将实例化器添加到模块中。
public class MyValueInstantiator extends StdValueInstantiator {
public MyValueInstantiator(DeserializationConfig config, Class<?> valueType) {
super(config, valueType);
}
@Override
protected JsonMappingException wrapException(Throwable t) {
if (t instanceof JsonMappingException) {
return (JsonMappingException) t;
}
return new JsonMappingException("Instantiation of "+getValueTypeDesc()+" value failed: "+t.getMessage(), t);
}
}
创建自定义反序列化器以使模块正常工作。将此类添加到模块初始化中。
public class CustomDeserializer extends StdScalarDeserializer<Q> {
public CustomDeserializer() {
super(Q.class);
}
@Override
public Q deserialize(JsonParser jp, DeserializationContext context) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
return new Q(node.get("id").asText());
}
@Override
public Object deserializeWithType(JsonParser jp, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException {
return deserialize(jp, ctxt);
}
}
答案 1 :(得分:0)
对于任何寻求其他解决方案的人来说,这在Spring Boot 2.2.8.RELEASE上对我有用。注意:这是一个示例,当您有一个带有请求主体的rest控制器,该请求主体具有一个枚举,并且客户端可能发送错误的字段字符串性别,并且您想提供适当的错误消息时:
@PostMapping
public ResponseEntity<ProfileResponse> updateProfile(@RequestBody @Valid ProfileRequest profileRequest) {
ProfileResponse profile = //do something important here that returns profile object response
return ResponseEntity
.status(HttpStatus.OK)
.body(profile);
}
ProfileRequest看起来像
@Data //Lombok getters and setters
public class ProfileRequest{
private GenderEnum gender;
//some more attributes
}
将此属性添加到aplication.properties文件中,以确保我们的自定义异常GlobalRuntimeException(有关定义,请参见下文)不会包装在JsonMappingException异常中。
spring.jackson.deserialization.WRAP_EXCEPTIONS=false
然后创建一个类,该类将由Spring Boot自动为其创建一个bean(这将用于反序列化枚举类型的字段性别)。如果找不到枚举,则知道会抛出错误。
@JsonComponent
public class GenderEnumDeserializer extends JsonDeserializer<GenderEnum> {
@Override
public GenderEnum deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String val = p.getValueAsString();
GenderEnum genderEnum = GenderEnum.fromName(val);
if(genderEnum == null){
throw new GlobalRuntimeException("Invalid gender provided. Valid values are MALE | FEMALE | OTHER");
}
return genderEnum;
}
}
GenderEnum中的“ forName”方法如下所示。
public static GenderEnum fromName(String name) {
GenderEnum foundGenderEnum = null;
for (GenderEnum genderEnum : values()) {
if (genderEnum.name().equalsIgnoreCase(name)) {
foundGenderEnum = genderEnum;
}
}
return foundGenderEnum;
}
然后我们将在ControllerAdvice中设置以捕获GlobalRuntimeException:
@ResponseBody
@ExceptionHandler(GlobalRuntimeException.class)
ResponseEntity<?> handleInvalidGlobalRuntimeException(HttpServletRequest request, GlobalRuntimeException ex) {
LOGGER.error("Error " + ex);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorMessage(ex.getCustomMessage));
}
就是这样。