SpringMVC参数校验(针对@RequestBody返回400) 前言 习惯别人帮忙做事的结果是自己不会做事了。一直以来,spring帮我解决了程序运行中的各种问题,我只要关心我的业务逻辑,设计好我的业务代码,返回正确的结果即可。直到遇到了400。
spring返回400的时候通常没有任何错误提示,当然也通常是参数不匹配。这在参数少的情况下还可以一眼看穿,但当参数很大是,排除参数也很麻烦,更何况,既然错误了,为什么指出来原因呢。好吧,springmvc把这个权力交给了用户自己。
springmvc异常处理最开始的时候也想过自己拦截会出异常的method来进行异常处理,但显然不需要这么做。spring提供了内嵌的以及全局的异常处理方法,基本可以满足我的需求了。
1. 内嵌异常处理 如果只是这个controller的异常做单独处理,那么就适合绑定这个controller本身的异常。
具体做法是使用注解@ExceptionHandler .
在这个controller中添加一个方法,并添加上述注解,并指明要拦截的异常。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @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异常。具体如下:
1 2 3 4 5 6 {   "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来做批量拦截。
第一次看到注释这么少的源码,忍不住多读几遍。
1 2 Indicates the annotated class assists a "Controller". 
表示这个注解是服务于Controller的。
1 Serves as a specialization of {@link Component @Component}, allowing for implementation classes to be autodetected through classpath scanning. 
用来当做特殊的Component注解,允许使用者扫描发现所有的classpath。
1 2 3 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.
1 2 3 4 5 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. 
这几个参数指定了扫描范围。
1 2 3 the default behavior (i.e. if used without any selector),  * the {@code @ControllerAdvice} annotated class will  * assist all known Controllers. 
默认扫描所有的已知的的Controllers。
1 2 Note that those checks are done at runtime, so adding many attributes and using  * multiple strategies may have negative impacts (complexity, performance). 
注意这个检查是在运行时做的,所以注意性能问题,不要放太多的参数。
说的如此清楚,以至于用法如此简单。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 @ResponseBody @ControllerAdvice("com.api") public  class  ApiExceptionHandler  extends  BaseClientController   {    private  static  final  Logger LOGGER = LoggerFactory.getLogger(ApiExceptionHandler.class);          @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 1 2 3 4 5 <dependency>      <groupId>org.hibernate</groupId>      <artifactId>hibernate-validator</artifactId>      <version>5.0.2.Final</version>   </dependency> 
1 2 3 4 5 6 <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 使用 在实体类字段上标注要求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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
1 2 3 4 5 6 @RequestMapping(value = "sign", method = RequestMethod.POST)     public  String sign (@Valid  @RequestBody  AlipayRequest params      )  {        ....     } 
3.错误处理 前面已经提到,如果不做处理的结果就是400,415. 这个对应Exception是MethodArgumentNotValidException,也是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @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; } 
返回结果:
1 2 3 4 5 6 7 8 {   "code" : 1 ,   "msg" : "校验失败" ,   "data" : {     "out_trade_no" : "不能为空" ,     "business_type" : "订单类型不能为空"    } } 
大概有这么几个限制注解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 /**  * 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=)  被注释的元素必须在合适的范围内   */