Spring验证不断验证错误的参数 [英] Spring validation keeps validating the wrong argument

查看:72
本文介绍了Spring验证不断验证错误的参数的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个带有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指定验证器,而不是@Valid并注册MethodValidationPostProcessor类.到目前为止没有运气.

要么根本不调用验证器,要么在我验证Device参数时验证了tokens参数.

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

任何人都可以提供有关我做错事情的任何线索吗?我整个下午都在网上搜索,试图解决这个问题.不能相信spring的验证区域仍然像5年前一样混乱:-(

解决方案

好.经过两天的各种变化后,现在已经解决了.如果有一件事情可以让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;
    }
}

这里发生两件事.首先是注册验证器.注意,我们必须检查参数的类型.这是因为对@RequestMapping的每个参数都调用了@InitBinder,而我们只希望在MyHeaderObj参数上使用验证器.如果我们不这样做,那么当Spring尝试将验证器应用于对其无效的参数时,将引发异常.

第二件事是异常处理程序.我们必须使用@ResponseBody来确保Spring将返回的对象视为要序列化的对象.否则,我们将只获得标准的HTML异常报告.

Validator

在这里,我们使用了一个非常标准的验证器实现.

@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中的所有代码都决定了何时应用此验证器.

自变量处理程序

这是最大的代码段.在这里,我们需要生成要传递给@RequestMappingMyHeaderObj对象. 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类可以提供帮助.希望这对遇到相同或相似问题的人有所帮助.

I have a controller with a web method that looks like this:

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

And a validator that looks like this:

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
        }
    }
}

I'm trying to get Spring to validate the Device argument which is being generated by an interceptor. But every time I try, it validates the tokens argument instead.

I've tried using @InitBinder to specify the validator, @Validated instead of @Validand registering MethodValidationPostProcessor classes. So far with no luck.

Either the validator is not called at all, or tokens argument is validated when I was the Device argument validated.

I'm using Spring 4.1.6 and Hibernate validator 5.1.3.

Can anyone offer any clues as to what I'm doing wrong? I've searched the web all afternoon trying to sort this out. Can't believe that the validation area of spring is still as messed up as it was 5 years ago :-(

解决方案

Ok. Have now solved it after two days of messing about with all sorts of variations. If there is one thing Spring's validation lets you do - it's come up with an incredible array of things that don't work! But back to my solution.

Basically what I needed was a way to manually create request mapping arguments, validate them and then ensure that no matter whether it was a success or failure, that the caller always received a custom JSON response. Doing this proved a lot harder than I thought because despite the number of blog posts and stackoverflow answers, I never found a complete solution. So I've endeavoured to outline each piece of the puzzle needed to achieve what I wanted.

Note: in the following code samples, I've generalised the names of things to help clarify whats custom and whats not.

Configuration

Although several blog posts I read talked about various classes such as the MethodValidationPostProcessor, in the end I found I didn't need anything setup beyond the @EnableWebMvc annotation. The default resolvers etc proved to be what I needed.

Request Mapping

My final request mapping signatures looked like this:

@RequestMapping(...)
public MyMsgObject handleRequest (
    @Valid final MyHeaderObj myHeaderObj, 
    @RequestBody final MyRequestPayload myRequestPayload
    ) {...}

You will note here that unlike just about every blog post and sample I found, I have two objects being passed to the method. The first is an object that I want to dynamically generate from the headers. The second is a deserialised object from the JSON payload. Other objects could just as easily be included such as path arguments etc. Try something like this without the code below and you will get a wide variety of weird and wonderful errors.

The tricky part that caused me all the pain was that I wanted to validate the myHeaderObj instance, and NOT validate the myRequestPayload instance. This caused quite a headache to resolve.

Also note the MyMsgObject result object. Here I want to return an object that will be serialised out to JSON. Including when exceptions occur as this class contains error fields that need to be populated in addition to the HttpStatus code.

Controller Advice

Next I created an ControllerAdvice class which contained the binding for validation and a general error trap.

@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;
    }
}

Two things going on here. The first is registering the validator. Note that we have to check the type of the argument. This is because @InitBinder is called for each argument to the @RequestMapping and we only want the validator on the MyHeaderObj argument. If we don't do this, exceptions will be thrown when Spring attempts to apply the validator to arguments it's not valid for.

The second thing is the exception handler. We have to use @ResponseBody to ensure that Spring treats the returned object as something to be serialised out. Otherwise we will just get the standard HTML exception report.

Validator

Here we use a pretty standard validator implementation.

@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 ..."); 
    }
}

One thing that I still don't really get with this is the supports(Class<?> clazz) method. I would have thought that Spring uses this method to test arguments to decide if this validator should apply. But it doesn't. Hence all the code in the @InitBinder to decide when to apply this validator.

The Argument Handler

This is the biggest piece of code. Here we need to generate the MyHeaderObj object to be passed to the @RequestMapping. Spring will auto detect this class.

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;
            }
        }
    }
}

The main job of this class is to use whatever means it requires to build the argument (myHeaderObj). Once built it then proceeds to call the Spring validators to check this instance. If there is a problem (as detected by checking the returned errors), it then throws an exception that the @ExceptionHandler's can detect and process.

Note the validateIfApplicable(WebDataBinder binder, MethodParameter methodParam) method. This is code I found in a number of Spring's classes. It's job is to detect if any argument has a @Validated or @Valid annotation and if so, call the associated validators. By default, Spring does not do this for custom argument handlers like this one, so it's up to us to add this functionality. Seriously Spring ???? No AbstractSomething ????

The last piece, explicit Exception catches

Lastly I also needed to catch more explicit exceptions. For example the MyCustomException thrown above. So here I created a second @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;
    }
}

Although superficially the similar to the general exception handler. There is one different. We need to specify the @Order(Ordered.HIGHEST_PRECEDENCE) annotation. Without this, Spring will just execute the first exception handler that matches the thrown exception. Regardless of whether there is a better matching handler or not. So we use this annotation to ensure that this exception handler is given precedence over the general one.

Summary

This solution works well for me. I'm not sure that I've got the best solution and there may be Spring classes which I've not found which can help. I hope this helps anyone with the same or similar problems.

这篇关于Spring验证不断验证错误的参数的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆