• 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(MethodArgumentNotValidException .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;
    
        //....
    }
    
    1. 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=)  被注释的元素必须在合适的范围内 
     */
    
  • 相关阅读:
    Spring Boot2 系列教程(二十)Spring Boot 整合JdbcTemplate 多数据源
    Spring Boot 如何给微信公众号返回消息
    Spring Boot2 系列教程(十九)Spring Boot 整合 JdbcTemplate
    Spring Boot2 系列教程(十八)Spring Boot 中自定义 SpringMVC 配置
    Spring Boot 开发微信公众号后台
    Spring Boot2 系列教程(十七)SpringBoot 整合 Swagger2
    Spring Boot2 系列教程(十六)定时任务的两种实现方式
    Spring Boot2 系列教程(十五)定义系统启动任务的两种方式
    Spring Boot2 系列教程(十四)CORS 解决跨域问题
    JavaScript二维数组
  • 原文地址:https://www.cnblogs.com/woshimrf/p/spring-web-400.html
Copyright © 2020-2023  润新知