首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

和 if else说再见,SpringBoot 这样做参数校验才足够优雅!

# 概述

当我们想提供可靠的 API 接口,对参数的校验,以保证最终数据入库的正确性,是 必不可少 的活。比如下图就是 我们一个项目里 新增一个菜单校验 参数的函数,写了一大堆的 if else 进行校验,非常的不优雅,比起枯燥的CRUD来说,参数校验更是枯燥。这只是一个创建菜单的校验,只需要判断菜单,菜单url 以及菜单的父类id是否为空,上级菜单是否挂载正确,这样已经消耗掉了30,40行代码了,更不要说,管理后台创建商品这种参数贼多的接口。

估计要写几百行校验代码了。

可能小伙伴会说不加参数校验行不行?或者把参数校验放到前端不就行了?那你可是想的太简单了,世界比我们想象中的不安全,可能有“黑客”会绕过浏览器,直接使用 HTTP 工具,模拟请求向后端 API 接口传入违法的参数,以达到它们 “不可告人” 的目的。比如 sql 注入攻击,我相信,很多时候并不是我们不想添加,而是没有统一方便的方式,让我们快速的添加实现参数校验的功能。

世界上大多数碰到的困难,大多已经有了解决方案,特别是开发生态非常完整的java来说,早在 Java 2009 年就提出了 Bean Validation 规范,并且已经历经 JSR303、JSR349、JSR380 三次标准的置顶,发展到了 2.0 。

有细心的小伙伴可能发现 Jakarta Bean Validation 3.0 里,这里3.0变化并不大,只是更改了一下包名 和命名空间而已。实际上还是对 Bean Validation 2.0 的实现。

Bean Validation 和我们很久以前学习过的 JPA 一样,只提供规范,不提供具体的实现,目前实现

Bean Validation 规范的数据校验框架,主要有:

Hibernate Validator

Apache BVal

可能有小伙伴就要说 Hibernate 不就是个老掉牙的ORM框架吗?现在不都没人用了吗?其实不然 Hibernate 可是打着 Everything data 口号的,它还提供了 Hibernate Search、Hibernate OGM 等等解决方案的。Hibernate 只是在国内的用的少了,国内主要是还是用 mybatis 这种 半orm框架的多。我们可以通过 google 的 trends 来看一下:

在中国的 mybatis, jpa, Hibernate 搜索热度:

在全球的 mybatis, jpa, Hibernate 搜索热度:

由于国内的开发环境可以说 99.99% 的开发者肯定都在用 spring,且正好 Spring Validation 提供了对 Bean Validation 的内置封装支持,可以使用 @Validated 注解,实现声明式校验,而无需直接调用 Bean Validation 提供的 API 方法。而在实现原理上,也是基于 Spring AOP 拦截,最终还是调用不同的 Bean Validation 的实现框架。例如说,Hibernate Validator 。实现校验相关的操作这一点,类似 Spring Transaction 事务,通过 @Transactional 注解,实现声明式事务。下面,让我们开始学习如何在 Spring Boot 中,实现参数校验。

# 注解

大致可以分为以下几类:

2.1 空和非空检查

@NotBlank :只能用于字符串不为 null ,并且字符串 #trim() 以后 length 要大于 0 。

@NotEmpty :集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null 。

@NotNull :不能为 null 。

@Null :必须为 null 。

2.2 数值检查

@DecimalMax(value) :被注释的元素必须是一个数字,其值必须小于等于指定的最大值。

@DecimalMin(value) :被注释的元素必须是一个数字,其值必须大于等于指定的最小值。

@Digits(integer, fraction) :被注释的元素必须是一个数字,其值必须在可接受的范围内。

@Positive :判断正数。

@PositiveOrZero :判断正数或 0 。

@Max(value) :该字段的值只能小于或等于该值。

@Min(value) :该字段的值只能大于或等于该值。

@Negative :判断负数。

@NegativeOrZero :判断负数或 0 。

2.3 Boolean 值检查

@AssertFalse :被注释的元素必须为 true 。

@AssertTrue :被注释的元素必须为 false 。

2.4 长度检查

@Size(max, min) :检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。

2.5 日期检查

@Future :被注释的元素必须是一个将来的日期。

@FutureOrPresent :判断日期是否是将来或现在日期。

@Past :检查该字段的日期是在过去。

@PastOrPresent :判断日期是否是过去或现在日期。

2.6 其它检查

@Email :被注释的元素必须是电子邮箱地址。

@Pattern(value) :被注释的元素必须符合指定的正则表达式。

2.7 Hibernate Validator 附加的约束注解

@Range(min=, max=) :被注释的元素必须在合适的范围内。

@Length(min=, max=) :被注释的字符串的大小必须在指定的范围内。

@URL(protocol=,host=,port=,regexp=,flags=) :被注释的字符串必须是一个有效的 URL 。

@SafeHtml :判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。

2.8 @Valid 和 @Validated

@Valid 注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。

@Validated 注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated 有 value 属性,支持分组校验。属性如下:

@Valid 有嵌套对象校验功能 例如说:如果不在 User.profile 属性上,添加 @Valid 注解,就会导致 UserProfile.nickname 属性,不会进行校验。

总的来说,绝大多数场景下,我们使用 @Validated 注解即可。而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上。

# 快速入门

3.1 引入依赖

在 Spring Boot 体系中,也提供了 spring-boot-starter-validation 依赖。在这里,我们并没有引入。为什么呢?因为 spring-boot-starter-web 已经引入了 spring-boot-starter-validation ,而 spring-boot-starter-validation 中也引入了 hibernate-validator 依赖,所以无需重复引入。三者的依赖引入关系可见下图

3.2 创建基本的类

UserAddDTO 实体类:

UserController 用来写接口,在类上,添加 @Validated 注解,表示 UserController 是所有接口都需要进行参数校验。

3.3 启动程序,进行测试

启动程序,然后再浏览器里我们就可以进行输入: swagger访问地址:http://localhost:8080/doc.html#/home 打开swagger文档 就可以进行测试了:

首先我们访问 http://localhost:8080/users/get?id=-1 进行测试,查看返回结果,果然对我们的 id 进行校验。

接下来我们访问 http://localhost:8080/users/add 进行新增用户的校验:请求体我们写成:

然后返回结果的如下:

返回结果的json串中的 errors 字段,参数错误明细数组。每一个数组元素,对应一个参数错误明细。这里,username 违背了 账号长度为 5-16 位规定。password违反了 密码长度为 4-16 位的规定。

返回结果示意图:

3.3 一些疑问

在这里 细心的小伙伴可能会有几个疑问:

3.3.1 疑问一

#get(id) 方法上,我们并没有给 id 添加 @Valid 注解,而 #add(addDTO) 方法上,我们给 addDTO 添加 @Valid 注解。这个差异,是为什么呢?

因为 UserController 使用了 @Validated 注解,那么 Spring Validation 就会使用 AOP 进行切面,进行参数校验。而该切面的拦截器,使用的是 MethodValidationInterceptor 。

对于 #get(id) 方法,需要校验的参数 id ,是平铺开的,所以无需添加 @Valid 注解。

对于 #add(addDTO) 方法,需要校验的参数 addDTO ,实际相当于嵌套校验,要校验的参数的都在 addDTO 里面,所以需要添加 @Valid (其实实测加@Validated也行,暂时不知道为啥 为了好区分就先用 @Valid 吧 )注解。

3.3.2 疑问二

#get(id) 方法的返回的结果是 status = 500 ,而 #add(addDTO) 方法的返回的结果是 status = 400 。

对于 #get(id) 方法,在 MethodValidationInterceptor 拦截器中,校验到参数不正确,会抛出 ConstraintViolationException 异常。

对于 #add(addDTO) 方法,因为 addDTO 是个 POJO 对象,所以会走 SpringMVC 的 DataBinder 机制,它会调用 DataBinder#validate(Object... validationHints) 方法,进行校验。在校验不通过时,会抛出 BindException 。

在 SpringMVC 中,默认使用 DefaultHandlerExceptionResolver 处理异常。

对于 BindException 异常,处理成 400 的状态码。

对于 ConstraintViolationException 异常,没有特殊处理,所以处理成 500 的状态码。

这里,我们在抛个问题,如果 #add(addDTO 方法,如果参数正确,在走完 DataBinder 中的参数校验后,会不会在走一遍 MethodValidationInterceptor 的拦截器呢?思考 100 毫秒…

答案是会。这样,就会导致浪费。所以 Controller 类里,如果 只有 类似的 #add(addDTO) 方法的 嵌套校验,那么我可以不在 Controller 类上添加 @Validated 注解。从而实现,仅使用 DataBinder 中来做参数校验。

3.3.3 返回提示很不友好,太长了

第三点,无论是 #get(id) 方法,还是 #add(addDTO) 方法,它们的返回提示都非常不友好,那么该怎么办呢?我们将在 第四章节通过 处理校验异常 进行处理。

# 处理校验异常

4.1 校验不通过的枚举类

4.2 统一返回结果实体类

4.3 增加全局异常处理类 GlobalExceptionHandler

4.4 测试

访问:http://localhost:8080/users/add 可以看到异常结果已经被拼接成一个字符串,相比之前清新 易懂了不少。

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20230521A06ATP00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券