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.
默认扫描所有的已知的的Controller
s。
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 使用
- 在实体类字段上标注要求 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
){
....
}
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=) 被注释的元素必须在合适的范围内
*/
- POJ 3278 Catch That Cow(BFS,板子题)
- 【请您听我说】PHP语法特点的一些看法
- 喵哈哈村的魔法考试 Round #1 (Div.2) 题解&源码(A.水+暴力,B.dp+栈)
- 查找第k小的元素(O(n)递归解法)
- C/C++对bool operator < (const p &a)const的认识,运算符重载详解(杂谈)
- C/C++中substr函数的应用(简单讲解)
- C++ STL学习之容器set和multiset (补充材料)
- Codeforces Round #410 (Div. 2)(A,字符串,水坑,B,暴力枚举,C,思维题,D,区间贪心)
- 关于int *a[常量]与int (*a)[常量]的分析与区分(详解)
- python笔记3-发送邮件(smtplib)
- 关关的刷题日记01—Leetcode 169. Majority Element
- 关小刷刷题02——Leetcode 169. Majority Element 方法2和3
- 树链剖分简单分析及模板(杂谈)
- 第十五届北京师范大学程序设计竞赛现场决赛题解&源码(A.思维,C,模拟,水,坑,E,几何,思维,K,字符串处理)
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- laravel model模型定义实现开启自动管理时间created_at,updated_at
- laravel ORM关联关系中的 with和whereHas用法
- thinkPHP事务操作简单案例分析
- php 多个变量指向同一个引用($b = &$a)用法分析
- 浅谈laravel5.5 belongsToMany自身的正确用法
- 在 PHP 和 Laravel 中使用 Traits的方法
- Laravel 修改默认日志文件名称和位置的例子
- php 命名空间(namespace)原理与用法实例小结
- Laravel Eloquent ORM 实现查询表中指定的字段
- Yii框架 session 数据库存储操作方法示例
- 使用laravel指定日志文件记录任意日志
- php实现微信小程序授权登录功能(实现流程)
- PHP封装请求类实例分析【基于Yii框架】
- php 多继承的几种常见实现方法示例
- PHP中通过getopt解析GNU C风格命令行选项