SpringMVC参数校验(针对`@RequestBody`返回`400`)

SpringMVC参数校验(针对@RequestBody返回400

From https://ryan-miao.github.io/2017/05/20/spring400/

前言

习惯别人帮忙做事的结果是自己不会做事了。一直以来,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("请求参数不匹配。", 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 使用
  1. 在实体类字段上标注要求 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; //.... }
  2. controller里添加@Valid
@RequestMapping(value = "sign", method = RequestMethod.POST)
    public String sign(@Valid @RequestBody AlipayRequest params
    ){
        ....
    }

3.错误处理 前面已经提到,如果不做处理的结果就是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=)  被注释的元素必须在合适的范围内 
 */

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏精讲JAVA

接口方法上的注解无法被 @Aspect 声明的切面拦截的原因分析

在Spring中使用MyBatis的Mapper接口自动生成时,用一个自定义的注解标记在Mapper接口的方法中,再利用@Aspect定义一个切面,拦截这个注解...

3332
来自专栏菩提树下的杨过

JAVA CDI 学习(4) - @Alternative/@Default/@Any & Extension

前面几节学习到的CDI内容,基本上都是hard-code,以硬编码的方式在代码里指定注入类型,这并非依赖注入的本意,依赖注入的优势之一在于“解耦”,这一节我们将...

25210
来自专栏编程坑太多

java并发之辅助类semaphore

1403
来自专栏Jed的技术阶梯

Kafka 中使用 Avro 序列化框架(二):使用 Twitter 的 Bijection 类库实现 avro 的序列化与反序列化

使用传统的 avro API 自定义序列化类和反序列化类比较麻烦,需要根据 schema 生成实体类,需要调用 avro 的 API 实现 对象到 byte[]...

3054
来自专栏Java Web

JavaWeb中使用JSON

JSON 使用 JavaScript 语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。

2864
来自专栏JAVA后端开发

activiti构造属于自己的流程定义

说起actviti,很多人都会说它支持bpmn标准,它的流转都是基于bpmn文件来运行! 但我们在设计流程时,流程定义真的只能是bpmn定义吗?   其实不...

3082
来自专栏张善友的专栏

Disruptor-NET和内存栅栏

Disruptor-NET算法(是一种无锁算法)需要我们自己实现某一种特定的内存操作的语义以保证算法的正确性。这时我们就需要显式的使用一些指令来控制内存操作指令...

2346
来自专栏美团技术团队

Mson,让JSON序列化更快

本文由秦喆 芝任 天洲 赵鹏四位作者共同完成。 问题 我们经常需要在主线程中读取一些配置文件或者缓存数据,最常用的结构化存储数据的方式就是将对象序列化为JSON...

48711
来自专栏JackieZheng

照虎画猫写自己的Spring——自定义注解

Fairy已经实现的功能 读取XML格式配置文件,解析得到Bean 读取JSON格式配置文件,解析得到Bean 基于XML配置的依赖注入 所以,理所当然,今天该...

3149
来自专栏运维技术迷

连仕彤博客[Python笔记] IPython使用技巧

帮助 ?:IPython的概述和简介   In [1]: ?   IPython -- An enhanced Interactive Python ====...

2955

扫码关注云+社区

领取腾讯云代金券