解决此问题的主要障碍之一是杰克逊数据绑定器的默认急切失败性质;必须以某种方式说服它继续解析,而不是在第一个错误时绊倒。人们还必须收集这些解析错误,以便最终将它们转换为BindingResult 条目。基本上,必须catch,suppress 和 collect 解析异常,convert 它们到BindingResult 条目然后将这些条目添加到右侧 @Controller 方法 BindingResult 参数。
catch & suppress 部分可以通过以下方式完成:
-
自定义 jackson 反序列化器,它会简单地委托给默认相关的反序列化器,但也会捕获、抑制和收集它们的解析异常
- 使用
AOP(aspectj 版本)可以简单地拦截解析异常的默认反序列化程序,抑制并收集它们
- 使用其他方式,例如适当的
BeanDeserializerModifier,也可以捕获、抑制和收集解析异常;这可能是最简单的方法,但需要对杰克逊特定的自定义支持有一些了解
收集部分可以使用ThreadLocal变量来存储所有必要的异常相关细节。 conversion 到BindingResult 条目和右侧BindingResult 参数的添加 可以通过AOP 拦截器在@Controller 方法(任何类型AOP,包括 Spring 变体)。
有什么收获
通过这种方法,可以将数据 binding 错误(除了 validation 错误)放入 BindingResult 参数中,与获取它们时所期望的方式相同使用例如@ModelAttribute。它也适用于多个级别的嵌入式对象 - 问题中提出的解决方案不会很好地解决这个问题。
解决方案详情(自定义杰克逊反序列化器方法)
我创建了一个small project proving the solution(运行测试类),而这里我只强调主要部分:
/**
* The logic for copying the gathered binding errors
* into the @Controller method BindingResult argument.
*
* This is the most "complicated" part of the project.
*/
@Aspect
@Component
public class BindingErrorsHandler {
@Before("@within(org.springframework.web.bind.annotation.RestController)")
public void logBefore(JoinPoint joinPoint) {
// copy the binding errors gathered by the custom
// jackson deserializers or by other means
Arrays.stream(joinPoint.getArgs())
.filter(o -> o instanceof BindingResult)
.map(o -> (BindingResult) o)
.forEach(errors -> {
JsonParsingFeedBack.ERRORS.get().forEach((k, v) -> {
errors.addError(new FieldError(errors.getObjectName(), k, v, true, null, null, null));
});
});
// errors copied, clean the ThreadLocal
JsonParsingFeedBack.ERRORS.remove();
}
}
/**
* The deserialization logic is in fact the one provided by jackson,
* I only added the logic for gathering the binding errors.
*/
public class CustomIntegerDeserializer extends StdDeserializer<Integer> {
/**
* Jackson based deserialization logic.
*/
@Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
try {
return wrapperInstance.deserialize(p, ctxt);
} catch (InvalidFormatException ex) {
gatherBindingErrors(p, ctxt);
}
return null;
}
// ... gatherBindingErrors(p, ctxt), mandatory constructors ...
}
/**
* A simple classic @Controller used for testing the solution.
*/
@RestController
@RequestMapping("/errormixtest")
@Slf4j
public class MixBindingAndValidationErrorsController {
@PostMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Level1 post(@Valid @RequestBody Level1 level1, BindingResult errors) {
// at the end I show some BindingResult logging for a @RequestBody e.g.:
// {"nr11":"x","nr12":1,"level2":{"nr21":"xx","nr22":1,"level3":{"nr31":"xxx","nr32":1}}}
// ... your whatever logic here ...
有了这些你会得到BindingResult这样的东西:
Field error in object 'level1' on field 'nr12': rejected value [1]; codes [Min.level1.nr12,Min.nr12,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.nr12,nr12]; arguments []; default message [nr12],5]; default message [must be greater than or equal to 5]
Field error in object 'level1' on field 'nr11': rejected value [x]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.level3.nr31': rejected value [xxx]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.nr22': rejected value [1]; codes [Min.level1.level2.nr22,Min.level2.nr22,Min.nr22,Min.java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.level2.nr22,level2.nr22]; arguments []; default message [level2.nr22],5]; default message [must be greater than or equal to 5]
第 1 行由 validation 错误决定(将 1 设置为 @Min(5) private Integer nr12; 的值),而第 2 行由 binding 决定(将"x" 设置为@JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11; 的值)。第 3 行测试 绑定错误 与嵌入对象:level1 包含一个 level2,其中包含一个 level3 对象属性。
注意其他方法如何简单地替换自定义杰克逊反序列化器的使用,同时保留解决方案的其余部分(AOP、JsonParsingFeedBack)。