前言
上篇文章 介绍了Spring环境下实现优雅的方法级别的数据校验,并且埋下一个伏笔:它在Spring MVC(Controller层)里怎么应用呢?本文为此继续展开讲解Spring MVC中的数据校验~
可能小伙伴能立马想到:这不一样吗?我们使用Controller就是方法级别的,所以它就是直接应用了方法级别的校验而已嘛~对于此疑问我先不解答,而是顺势再抛出两个问题你自己应该就能想明白了:
- 上文有说过,基于方法级别的校验Spring默认是并未开启的,但是为什么你在Spring MVC却可以直接使用@Valid完成校验呢?
- 可能有的小伙伴说他用的是SpringBoot可能默认给开启了,其实不然。哪怕你用的传统Spring MVC你会发现也是直接可用的,不信你就试试
- 类比一下:Spring MVC的HandlerInterceptor是AOP思想的实现,但你有没有发现即使你没有启动@EnableAspectJAutoProxy的支持,它依旧好使~
若你能想明白我提出的这两个问题,下文就非常不难理解了。当然即使你知道了这两个问题的答案,还是建议你读下去。毕竟:永远相信本文能给你带来意想不到的收获~
使用示例
关于数据校验这一块在Spring MVC中的使用案例,我相信但凡有点经验的Java程序员应该没有不会使用的,并且还不乏熟练的选手。在此之前我简单“采访”过,绝大多数程序员甚至一度认为Spring中的数据校验就是指的在Controller中使用@Validated校验入参JavaBean这一块~
因此下面这个例子,你应该一点都不陌生:
@Getter @Setter @ToString public class Person { @NotNull private String name; @NotNull @Positive private Integer age; @Valid // 让InnerChild的属性也参与校验 @NotNull private InnerChild child; @Getter @Setter @ToString public static class InnerChild { @NotNull private String name; @NotNull @Positive private Integer age; } } @RestController @RequestMapping public class HelloController { @PostMapping("/hello") public Object helloPost(@Valid @RequestBody Person person, BindingResult result) { System.out.println(result.getErrorCount()); System.out.println(result.getAllErrors()); return person; } }
发送post请求:/hello Content-Type=application/json,传入的json串如下:
{ "name" : "fsx", "age" : "-1", "child" : { "age" : 1 } }
控制台有如下打印:
2 [Field error in object 'person' on field 'child.name': rejected value [null]; codes
从打印上看:校验生效(拿着错误消息就可以返回前端展示,或者定位到错误页面都行)。
此例两个小细节务必注意:
@RequestBody注解不能省略,否则传入的json无法完成数据绑定(即使不绑定,校验也是生效的哦)~
若方法入参不写BindingResult result这个参数,请求得到的直接是400错误,因为若有校验失败的服务端会抛出异常org.springframework.web.bind.MethodArgumentNotValidException。若写了,那就调用者自己处理喽~
据我不完全和不成熟的统计,就这个案例就覆盖了小伙伴们实际使用中的90%以上的真实使用场景,使用起来确实非常的简单、优雅、高效~
但是作为一个有丰富经验的程序员的你,虽然你使用了@Valid优雅的完成了数据校验,但回头是你是否还会发现你的代码里还是存在了大量的if else的基础的校验?什么原因?其实根本原因只有一个:很多case使用@Valid并不能覆盖,因为它只能校验JavaBean
我相信你是有这样那样的使用痛点的,本文先从原理层面分析,进而给出你所遇到的痛点问题的参考解决参考方案~
原理分析
Controller提供的使用@Valid便捷校验JavaBean的原理,和Spring方法级别的校验支持的原理是有很大差异的(可类比Spring MVC拦截器和Spring AOP的差异区别~),那么现在就看看这块吧
请不要忽视优雅代码的力量,它会成倍提升你的编码效率、成倍降低后期维护成本,甚至成倍提升你的扩展性和成倍降低你写bug的可能性~
回忆DataBinder/WebDataBinder
若对Spring数据绑定模块不是很熟悉的(有阅读过我之前文章的可忽略),建议先补:
- 【小家Spring】聊聊Spring中的数据绑定 — DataBinder本尊(源码分析)
- 【小家Spring】聊聊Spring中的数据绑定 — WebDataBinder、ServletRequestDataBinder、WebBindingInitializer…
DataBinder类名叫数据绑定,但它在org.springframework.validation这个包,可见Spring它把数据绑定和数据校验牢牢的放在了一起,并且内部弱化了数据校验的概念以及逻辑(Spring想让调用者无需关心数据校验的细节,全由它来自动完成,减少使用的成本)。
我们知道DataBinder它主要对外提供了bind(PropertyValues pvs)和validate()方法,当然还有处理绑定/校验失败的相关(配置)组件:
public class DataBinder implements PropertyEditorRegistry, TypeConverter { ... @Nullable private AbstractPropertyBindingResult bindingResult; // 它是个BindingResult @Nullable private MessageCodesResolver messageCodesResolver; private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); // 最重要是它:它是org.springframework.validation.Validator // 一个DataBinder 可以持有对个验证器。也就是说对于一个Bean,是可以交给多个验证器去验证的(当然一般都只有一个即可而已~~~) private final List<Validator> validators = new ArrayList<>(); public void bind(PropertyValues pvs) { MutablePropertyValues mpvs = (pvs instanceof MutablePropertyValues ? (MutablePropertyValues) pvs : new MutablePropertyValues(pvs)); doBind(mpvs); } ... public void validate() { Object target = getTarget(); Assert.state(target != null, "No target to validate"); BindingResult bindingResult = getBindingResult(); // 拿到所有的验证器 一个个的对此target进行验证~~~ // Call each validator with the same binding result for (Validator validator : getValidators()) { validator.validate(target, bindingResult); } } }
DataBinder
提供了这两个非常独立的原子方法:绑定 + 校验。他俩结合完成了我们的数据绑定+数据校验,完全的和业务无关~
网上有很多文章说
DataBinder
完成数据绑定后继续校验,这种说法是不准确的呀,因为它并不处理这部分的组合逻辑,它只提供原始能力~
Spring MVC处理入参的时机
Spring MVC处理入参的逻辑是非常复杂的,前面花大篇幅讲了Spring MVC对返回值的处理器:HandlerMethodReturnValueHandler,详见:
【小家Spring】Spring MVC容器的web九大组件之—HandlerAdapter源码详解—一篇文章带你读懂返回值处理器HandlerMethodReturnValueHandler
同样的,本文只关注它对@RequestBody这种类型的入参进行讲解~
处理入参的处理器:HandlerMethodArgumentResolver,处理@RequestBody最终使用的实现类是:RequestResponseBodyMethodProcessor,Spring借助此处理器完成一系列的消息转换器、数据绑定、数据校验等工作~