Spring MVC参数校验详解(关于`@RequestBody`返回`400`)
前言
工作中发现一个定律,如果总是习惯别人帮忙做事的结果是自己不会做事了。一直以来,spring帮我解决了程序运行中的各种问题,我只要关心我的业务逻辑,设计好我的业务代码,返回正确的结果即可。直到遇到了400。
spring返回400的时候通常没有任何错误提示,当然也通常是参数不匹配。这在参数少的情况下还可以一眼看穿,但当参数很大是,排除参数也很麻烦,更何况,既然错误了,为什么指出来原因呢。好吧,springmvc把这个权力交给了用户自己。话不多说了,来一起看看详细的介绍吧。
springmvc异常处理
最开始的时候也想过自己拦截会出异常的method来进行异常处理,但显然不需要这么做。spring提供了内嵌的以及全局的异常处理方法,基本可以满足我的需求了。
1. 内嵌异常处理
如果只是这个controller的异常做单独处理,那么就适合绑定这个controller本身的异常。
具体做法是使用注解@exceptionhandler.
在这个controller中添加一个方法,并添加上述注解,并指明要拦截的异常。
@requestmapping(value = "saveorupdate", method = requestmethod.post) public string saveorupdate(httpservletresponse response, @requestbody order order){ codemsg result = null; try { result = orderservice.saveorupdate(order); } catch (exception e) { logger.error("save failed.", e); return this.renderstring(response, codemsg.error(e.getmessage())); } return this.renderstring(response, result); } @responsebody @responsestatus(httpstatus.bad_request) @exceptionhandler(httpmessagenotreadableexception.class) public codemsg messagenotreadable(httpmessagenotreadableexception exception, httpservletresponse response){ logger.error("请求参数不匹配。", exception); return codemsg.error(exception.getmessage()); }
这里saveorupdate是我们想要拦截一样的请求,而messagenotreadable则是处理异常的代码。
@exceptionhandler(httpmessagenotreadableexception.class)表示我要拦截何种异常。在这里,由于springmvc默认采用jackson作为json序列化工具,当反序列化失败的时候就会抛出httpmessagenotreadableexception异常。具体如下:
{ "code": 1, "msg": "could not read json: failed to parse date value '2017-03-' (format: \"yyyy-mm-dd hh:mm:ss\"): unparseable date: \"2017-03-\" (through reference chain: com.test.modules.order.entity.order[\"servetime\"]); nested exception is com.fasterxml.jackson.databind.jsonmappingexception: failed to parse date value '2017-03-' (format: \"yyyy-mm-dd hh:mm:ss\"): unparseable date: \"2017-03-\" (through reference chain: com.test.modules.order.entity.order[\"servetime\"])", "data": "" }
这是个典型的jackson反序列化失败异常,也是造成我遇见过的400原因最多的。通常是日期格式不对。
另外, @responsestatus(httpstatus.bad_request)
这个注解是为了标识这个方法返回值的httpstatus code。我设置为400,当然也可以自定义成其他的。
2. 批量异常处理
看到大多数资料写的是全局异常处理,我觉得对我来说批量更合适些,因为我只是希望部分controller被拦截而不是全部。
springmvc提供了@controlleradvice来做批量拦截。
第一次看到注释这么少的源码,忍不住多读几遍。
indicates the annotated class assists a "controller".
表示这个注解是服务于controller的。
serves as a specialization of {@link component @component}, allowing for implementation classes to be autodetected through classpath scanning.
用来当做特殊的component注解,允许使用者扫描发现所有的classpath。
it is typically used to define {@link exceptionhandler @exceptionhandler}, * {@link initbinder @initbinder}, and {@link modelattribute @modelattribute} * methods that apply to all {@link requestmapping @requestmapping} methods.
典型的应用是用来定义xxxx.
one of {@link #annotations()}, {@link #basepackageclasses()}, * {@link #basepackages()} or its alias {@link #value()} * may be specified to define specific subsets of controllers * to assist. when multiple selectors are applied, or logic is applied - * meaning selected controllers should match at least one selector.
这几个参数指定了扫描范围。
the default behavior (i.e. if used without any selector), * the {@code @controlleradvice} annotated class will * assist all known controllers.
默认扫描所有的已知的的controllers。
note that those checks are done at runtime, so adding many attributes and using * multiple strategies may have negative impacts (complexity, performance).
注意这个检查是在运行时做的,所以注意性能问题,不要放太多的参数。
说的如此清楚,以至于用法如此简单。
@responsebody @controlleradvice("com.api") public class apiexceptionhandler extends baseclientcontroller { private static final logger logger = loggerfactory.getlogger(apiexceptionhandler.class); /** * * @param exception unexpectedtypeexception * @param response * @return */ @responsestatus(httpstatus.bad_request) @exceptionhandler(unexpectedtypeexception.class) public codemsg unexpectedtype(unexpectedtypeexception exception, httpservletresponse response){ logger.error("校验方法太多,不确定合适的校验方法。", exception); return codemsg.error(exception.getmessage()); } @responsestatus(httpstatus.bad_request) @exceptionhandler(httpmessagenotreadableexception.class) public codemsg messagenotreadable(httpmessagenotreadableexception exception, httpservletresponse response){ logger.error("请求参数不匹配,request的json格式不正确", exception); return codemsg.error(exception.getmessage()); } @responsestatus(httpstatus.bad_request) @exceptionhandler(exception.class) public codemsg ex(methodargumentnotvalidexception exception, httpservletresponse response){ logger.error("请求参数不合法。", exception); bindingresult bindingresult = exception.getbindingresult(); string msg = "校验失败"; return new codemsg(codemsgconstant.error, msg, geterrors(bindingresult)); } private map<string, string> geterrors(bindingresult result) { map<string, string> map = new hashmap<>(); list<fielderror> list = result.getfielderrors(); for (fielderror error : list) { map.put(error.getfield(), error.getdefaultmessage()); } return map; } }
3. hibernate-validate
使用参数校验如果不catch异常就会返回400. 所以这个也要规范一下。
3.1 引入hibernate-validate
<dependency> <groupid>org.hibernate</groupid> <artifactid>hibernate-validator</artifactid> <version>5.0.2.final</version> </dependency>
<mvc:annotation-driven validator="validator" /> <bean id="validator" class="org.springframework.validation.beanvalidation.localvalidatorfactorybean"> <property name="providerclass" value="org.hibernate.validator.hibernatevalidator"/> <property name="validationmessagesource" ref="messagesource"/> </bean>
3.2 使用
在实体类字段上标注要求
public class alipayrequest { @notempty private string out_trade_no; private string subject; @decimalmin(value = "0.01", message = "费用最少不能小于0.01") @decimalmax(value = "100000000.00", message = "费用最大不能超过100000000") private string total_fee; /** * 订单类型 */ @notempty(message = "订单类型不能为空") private string business_type; //.... }
controller里添加@valid
@requestmapping(value = "sign", method = requestmethod.post) public string sign(@valid @requestbody alipayrequest params ){ .... }
4.错误处理
前面已经提到,如果不做处理的结果就是400,415. 这个对应exception是methodargumentnotvalidexception,也是这样:
@responsestatus(httpstatus.bad_request) @exceptionhandler(exception.class) public codemsg ex(methodargumentnotvalidexception exception, httpservletresponse response){ logger.error("请求参数不合法。", exception); bindingresult bindingresult = exception.getbindingresult(); string msg = "校验失败"; return new codemsg(codemsgconstant.error, msg, geterrors(bindingresult)); } private map<string, string> geterrors(bindingresult result) { map<string, string> map = new hashmap<>(); list<fielderror> list = result.getfielderrors(); for (fielderror error : list) { map.put(error.getfield(), error.getdefaultmessage()); } return map; }
返回结果:
{ "code": 1, "msg": "校验失败", "data": { "out_trade_no": "不能为空", "business_type": "订单类型不能为空" } }
大概有这么几个限制注解:
/** * bean validation 中内置的 constraint * @null 被注释的元素必须为 null * @notnull 被注释的元素必须不为 null * @asserttrue 被注释的元素必须为 true * @assertfalse 被注释的元素必须为 false + * @min(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 * @max(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 * @decimalmin(value) 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 * @decimalmax(value) 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 * @size(max=, min=) 被注释的元素的大小必须在指定的范围内 * @digits (integer, fraction) 被注释的元素必须是一个数字,其值必须在可接受的范围内 * @past 被注释的元素必须是一个过去的日期 * @future 被注释的元素必须是一个将来的日期 * @pattern(regex=,flag=) 被注释的元素必须符合指定的正则表达式 * hibernate validator 附加的 constraint * @notblank(message =) 验证字符串非null,且长度必须大于0 * @email 被注释的元素必须是电子邮箱地址 * @length(min=,max=) 被注释的字符串的大小必须在指定的范围内 * @notempty 被注释的字符串的必须非空 * @range(min=,max=,message=) 被注释的元素必须在合适的范围内 */
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流,谢谢大家对的支持。