• 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=)  被注释的元素必须在合适的范围内 
     */
    
  • 相关阅读:
    解决ASP.NET MVC AllowAnonymous属性无效导致无法匿名访问控制器的问题
    ASP.NET MVC 4 RC的JS/CSS打包压缩功能 (转载)
    oracle报错ORA-01507
    oracle 大表删除数据后,回收空间的问题。
    解决MySQL服务启动时报1067错误
    尚未在 Web 服务器上注册 ASP.NET 4.0” 的解决办法
    thymeleaf中相对路径的两种方式
    史上最详 Thymeleaf 使用教程
    isNotBlank()和isNotEmpty()总结
    IDEA去除掉虚线,波浪线,和下划线实线的方法
  • 原文地址:https://www.cnblogs.com/woshimrf/p/spring-web-400.html
Copyright © 2020-2023  润新知