我是怎么进行SpringMVC参数校验的?

前语
不要为了读文章而读文章,一定要带着问题来读文章,勤思考。

image

在 Web 开发中, 我们经常需要校验各种参数,这是一件繁琐又重要的事情,对于很多人来说,在做参数校验的时候,会有以下几种类型的处理方式。

甩锅型

校验太麻烦了,让客户端去负责校验就行了,调用方传错了是调用方的问题,不是服务的问题,甩个 500 错误让他们好好反省:

image

劳模型

有多少参数,我就写多少个 if 语句做判断,校验不通过的都写一句友好的提示,如:

image

工具型

自己写个参数校验的通用工具,然后每个请求接收到的参数都调用工具方法来校验,校验不通过就把校验结果返回给调用方:。

image

半自动型

对 SpringMVC 了解比较全面的朋友都知道,它支持 Bean Validation,因此可以通过使用 javax.validation.constraints 包下的注解,如 @NotNull@Max@Min 等,来实现由框架处理数据校验。

首先,添加 hibernate-validator 依赖(SpringBoot 已经为我们自动添加了)。

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232);"><dependency>

<groupId>
org.hibernate.validator
</groupId>

<artifactId>
hibernate-validator
</artifactId>

<version>
6.0.10.Final
</version>
</dependency>
</pre>

然后,在参数对象的字段上打注解:

image

最后,在 Controller 中给参数对象添加 @Valid 注解,并处理校验结果:

Tip:如果你的参数不是对象,一定要在 Controller 上打 @Validated 注解!

[图片上传中...(image-657a49-1562073850576-1)]

这样做,每个 Controller 方法都要处理结果,也是很麻烦。

方案分析

以上这些处理方式都有不足之处:

首先,参数校验是一件非常重要的事,客户端要把住第一道防线,而服务方要采取不信任的态度,做好参数校验。否则非法请求参数小则影响用户体验或者产生垃圾数据,大则会拖跨整个系统!

其次,手工对所有的参数进行校验相当繁琐,容易出错,而且 So boring~

最后,通过工具来完成是比较好的方式,但是必须让工具变得优雅一些。

那么,有没有更好的解决方案呢?答案是:有的!

最佳实践

其实,上面的半自动型的解决方式,只要再进一步,就可以实现全自动了!

想想,如果上面的半自动型例子中,我们不在 Controller 方法中处理校验结果,会怎么样呢?答案是,会抛出异常:

image

那么,如果我们做了全局统一异常处理,不就可以实现自动校验并返回我们想要的结果了吗?所以我们可以这样做:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232);">@ControllerAdvice
public class 
GlobalExceptionHandler
 {

/** 统一处理参数校验异常 */

@ExceptionHandler

@ResponseBody
 public 
ResultBean
<?> handleValidationException(
BindException
 e) {

// 获取

String
 msg = e.getBindingResult().getAllErrors().stream()
 .map(
DefaultMessageSourceResolvable
::getDefaultMessage)
 .collect(
Collectors
.joining(
","
));
 log.warn(
"参数校验不通过, msg: {}"
, msg);
 return 
ResultBean
.fail(msg);
 }
}
</pre>

然而,如果你只处理 BindException 这个异常的话,你会发现这个方案有时候好用,有时候却会“失灵”。为什么呢?因为对于不同的参数解析方式,Spring做参数校验时会抛出不同的异常,而且这些异常没有继承关系,通过异常获取校验结果的方式也各不相同(好坑爹~)。

总结起来有以下几种异常需要处理:

对象参数接收请求体,即 RequestBody:

MethodArgumentNotValidException

请求参数绑定到对象参数上:

BindException

普通参数:

ConstraintViolationException

必填参数缺失:

ServletRequestBindingException

所以完整的处理方法应该是这样:

<pre style="-webkit-tap-highlight-color: transparent; box-sizing: border-box; font-family: Consolas, Menlo, Courier, monospace; font-size: 16px; white-space: pre-wrap; position: relative; line-height: 1.5; color: rgb(153, 153, 153); margin: 1em 0px; padding: 12px 10px; background: rgb(244, 245, 246); border: 1px solid rgb(232, 232, 232);">@ExceptionHandler
({
ConstraintViolationException
.class,

MethodArgumentNotValidException
.class,

ServletRequestBindingException
.class,

BindException
.class})
@ResponseBody
public 
ResultBean
<?> handleValidationException(
Exception
 e) {

String
 msg = 
""
;
 if (e instanceof 
MethodArgumentNotValidException
) {

MethodArgumentNotValidException
 t = (
MethodArgumentNotValidException
) e;
 msg = t.getBindingResult().getAllErrors().stream()
 .map(
DefaultMessageSourceResolvable
::getDefaultMessage)
 .collect(
Collectors
.joining(
","
));
 } else if (e instanceof 
BindException
) {

BindException
 t = (
BindException
) e;
 msg = t.getBindingResult().getAllErrors().stream()
 .map(
DefaultMessageSourceResolvable
::getDefaultMessage)
 .collect(
Collectors
.joining(
","
));
 } else if (e instanceof 
ConstraintViolationException
) {

ConstraintViolationException
 t = (
ConstraintViolationException
) e;
 msg = t.getConstraintViolations().stream()
 .map(
ConstraintViolation
::getMessage)
 .collect(
Collectors
.joining(
","
));
 } else if (e instanceof 
MissingServletRequestParameterException
) {

MissingServletRequestParameterException
 t = (
MissingServletRequestParameterException
) e;
 msg = t.getParameterName() + 
" 不能为空"
;
 } else if (e instanceof 
MissingPathVariableException
) {

MissingPathVariableException
 t = (
MissingPathVariableException
) e;
 msg = t.getVariableName() + 
" 不能为空"
;
 } else {
 msg = 
"必填参数缺失"
;
 }
 log.warn(
"参数校验不通过,msg: {}"
, msg);
 return 
ResultBean
.fail(msg);
}
</pre>

添加了这个全局异常处理器之后,就可以自动参数校验了!体验飞升的感觉~~

但是,如果用户是在浏览器上访问某个页面,然后参数校验不通过时这个统一异常处理器会返回一个 json 格式的文本。普通用户看到这样的文本,估计要懵圈了。

那么问题来了,怎么才能让打开页面的请求返回错误页面,而 ajax 请求返回 json 呢?我的实例代码已经展示了,有兴趣可以了解一下。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,096评论 1 32
  • 1、Spring MVC请求流程 (1)初始化:(对DispatcherServlet和ContextLoderL...
    拾壹北阅读 1,948评论 0 12
  • hibernate validator 校验操作小结 在controller 的方法定义中加上 @Validate...
    一秒一心跳阅读 2,920评论 0 1
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,951评论 6 13
  • 社区里大大小小的超市不下四、五家,有时还是会走顺脚去最小、最不起眼的那家。 店里的大爷,个子不高、瘦瘦的,穿着总是...
    立雪寒梅1阅读 386评论 2 2