【问题标题】:Spring validation keeps validating the wrong argumentSpring验证不断验证错误的参数
【发布时间】:2015-05-25 07:24:25
【问题描述】:

我有一个带有如下 Web 方法的控制器:

public Response registerDevice(
    @Valid final Device device, 
    @RequestBody final Tokens tokens
) {...}

还有一个看起来像这样的验证器:

public class DeviceValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Device.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        // Do magic
        }
    }
}

我正在尝试让 Spring 验证由拦截器生成的 Device 参数。但每次我尝试时,它都会验证 tokens 参数。

我尝试使用@InitBinder 指定验证器@Validated 而不是@Valid 并注册MethodValidationPostProcessor 类。到目前为止没有运气。

要么根本没有调用验证器,要么在验证设备参数时验证了令牌参数。

我正在使用 Spring 4.1.6 和 Hibernate 验证器 5.1.3。

谁能提供任何线索来说明我做错了什么?我整个下午都在网上搜索,试图解决这个问题。不敢相信spring的validation区还跟5年前一样乱:-(

【问题讨论】:

  • 为什么要同时提交模型属性和子负载?一开始我觉得很奇怪。 @Valid 还应该有 @ModelAttribute 注释,如果你想访问验证错误,你需要在 Device 参数后面直接有一个属性。但是我会说你试图用一种方法做多件事,这就是你做错了。
  • @ModelAttribute 不起作用。它只是创建了一个空设备并绕过了创建对象的我的 HandlerMethodArgumentResolver。我不确定你在谈论两个不同的事情。该设备是根据标头信息动态构建的,令牌来自 http 请求正文。在这一点上,它们不是同一模型的一部分。令我困惑的是,验证内容的文档相当模糊,似乎只有在您有内部注释的模型类时才适用。我只想使用验证器验证单个参数。
  • 我在这里做了假设,你没有解释你有一个自定义的HandlerMethodArgumentResolver。验证通常仅适用于模型属性或请求主体,不适用于其他对象。你必须把它放在你的自定义HandlerMethodArgumentResolver中,你可以看看ModelAttributeMethodProcessor是如何在那里实现验证的。
  • 谢谢。我会调查的。

标签: spring bean-validation


【解决方案1】:

好的。经过两天的各种变化,现在已经解决了。如果 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&lt;?&gt; 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 类可以提供帮助。我希望这对遇到相同或类似问题的人有所帮助。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-01-07
    • 1970-01-01
    • 1970-01-01
    • 2017-09-11
    • 1970-01-01
    • 2018-10-26
    • 2016-06-18
    相关资源
    最近更新 更多