好的。经过两天的各种变化,现在已经解决了。如果 Spring 的验证允许您做一件事 - 它会提出一系列令人难以置信的不起作用的事情!但回到我的解决方案。
基本上,我需要的是一种手动创建请求映射参数、验证它们并确保无论成功还是失败,调用者始终会收到自定义 JSON 响应的方法。事实证明,这样做比我想象的要困难得多,因为尽管有很多博客文章和 stackoverflow 答案,但我从未找到完整的解决方案。所以我努力勾勒出实现我想要的每一个难题。
注意:在以下代码示例中,我概括了事物的名称,以帮助阐明什么是自定义的,什么不是。
配置
虽然我阅读的几篇博客文章谈到了各种类,例如MethodValidationPostProcessor,最后我发现除了@EnableWebMvc 注释之外我不需要任何设置。默认解析器等证明是我需要的。
请求映射
我的最终请求映射签名如下所示:
@RequestMapping(...)
public MyMsgObject handleRequest (
@Valid final MyHeaderObj myHeaderObj,
@RequestBody final MyRequestPayload myRequestPayload
) {...}
您会注意到,与我发现的几乎所有博客文章和示例不同,我有两个对象被传递给该方法。第一个是我想从标头动态生成的对象。第二个是来自 JSON 有效负载的反序列化对象。其他对象也可以很容易地包含在内,例如路径参数等。尝试这样的操作而不使用下面的代码,你会得到各种各样的奇怪和奇妙的错误。
给我带来所有痛苦的棘手部分是我想验证myHeaderObj 实例,而不是验证myRequestPayload 实例。解决这个问题很头疼。
还要注意MyMsgObject 结果对象。在这里,我想返回一个将被序列化为 JSON 的对象。包括何时发生异常,因为此类包含除了 HttpStatus 代码之外还需要填充的错误字段。
控制器建议
接下来,我创建了一个 ControllerAdvice 类,其中包含用于验证的绑定和一般错误陷阱。
@ControllerAdvice
public class MyControllerAdvice {
@Autowired
private MyCustomValidator customValidator;
@InitBinder
protected void initBinder(WebDataBinder binder) {
if (binder.getTarget() == null) {
// Plain arguments have a null target.
return;
}
if (MyHeaderObj.class.isAssignableFrom(binder.getTarget().getClass())) {
binder.addValidators(this.customValidator);
}
}
@ExceptionHandler(Exception.class)
@ResponseStatus(value=HttpStatus.INTERNAL_SERVER_ERROR)
@ResponseBody
public MyMsgObject handleException(Exception e) {
MyMsgObject myMsgObject = new MyMsgObject();
myMsgObject.setStatus(MyStatus.Failure);
myMsgObject.setMessage(e.getMessage());
return myMsgObject;
}
}
这里发生了两件事。首先是注册验证器。请注意,我们必须检查参数的类型。这是因为@InitBinder 为@RequestMapping 的每个参数调用,而我们只需要MyHeaderObj 参数上的验证器。如果我们不这样做,当 Spring 尝试将验证器应用于无效的参数时,将引发异常。
第二件事是异常处理程序。我们必须使用@ResponseBody 来确保Spring 将返回的对象视为要序列化的对象。否则我们只会得到标准的 HTML 异常报告。
验证器
这里我们使用了一个非常标准的验证器实现。
@Component
public class MyCustomValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return MyHeaderObj.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
...
errors.rejectValue("fieldName", "ErrorCode", "Invalid ...");
}
}
我仍然没有真正理解的一件事是supports(Class<?> clazz) 方法。我原以为 Spring 使用这种方法来测试参数来决定是否应该应用这个验证器。但事实并非如此。因此,@InitBinder 中的所有代码决定何时应用此验证器。
参数处理程序
这是最大的一段代码。这里我们需要生成MyHeaderObj 对象来传递给@RequestMapping。 Spring 会自动检测这个类。
public class MyHeaderObjArgumentHandler implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return MyHeaderObj.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
// Code to generate the instance of MyHeaderObj!
MyHeaderObj myHeaderObj = ...;
// Call validators if the argument has validation annotations.
WebDataBinder binder = binderFactory.createBinder(webRequest, myHeaderObj, parameter.getParameterName());
this.validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors()) {
throw new MyCustomException(myHeaderObj);
}
return myHeaderObj;
}
protected void validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) {
Annotation[] annotations = methodParam.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] { hints });
binder.validate(validationHints);
break;
}
}
}
}
这个类的主要工作是使用构建参数所需的任何方法 (myHeaderObj)。一旦构建,它就会继续调用 Spring 验证器来检查这个实例。如果存在问题(通过检查返回的错误检测到),则会引发@ExceptionHandler 可以检测和处理的异常。
注意validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) 方法。这是我在许多 Spring 类中找到的代码。它的工作是检测任何参数是否具有@Validated 或@Valid 注释,如果是,则调用相关的验证器。默认情况下,Spring 不会为像这样的自定义参数处理程序执行此操作,因此由我们来添加此功能。认真的春天???没有 AbstractSomething ????
最后一部分,显式异常捕获
最后,我还需要捕获更明确的异常。例如上面抛出的MyCustomException。所以在这里我创建了第二个@ControllerAdvise。
@ControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE) // Make sure we get the highest priority.
public class MyCustomExceptionHandler {
@ExceptionHandler
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
@ResponseBody
public Response handleException(MyCustomException e) {
MyMsgObject myMsgObject = new MyMsgObject();
myMsgObject.setStatus(MyStatus.Failure);
myMsgObject.setMessage(e.getMessage());
return myMsgObject;
}
}
虽然表面上类似于一般的异常处理程序。有一种不同。我们需要指定@Order(Ordered.HIGHEST_PRECEDENCE) 注释。如果没有这个,Spring 将只执行第一个与抛出的异常匹配的异常处理程序。不管是否有更好的匹配处理程序。所以我们使用这个注解来确保这个异常处理程序优先于一般处理程序。
总结
这个解决方案对我很有效。我不确定我是否有最好的解决方案,并且可能有一些我没有找到的 Spring 类可以提供帮助。我希望这对遇到相同或类似问题的人有所帮助。