1.情景展示
在实际开发过程中,针对前端请求参数的校验是一个不小的工作量。
什么时候需要对请求参数进行校验?
情形1:前后端分离
前后端分离,虽然会提高项目的开发进度,但同样也存在前后端开发人员交流不及时等问题。
比方说:性别参数,后端要求只能传1或2,前端非要给你传男或女,当前后端对于数据的要求标准不一致时,就会出现问题。
后台对入参进行校验,一方面,可以提高数据的规范性;另一方面,也可以增加数据的安全性(比如:数据在传输过程中被篡改)。
情形2:对外提供接口
本质上,前后端分离,后台提供请求,也是属于接口,这里特指的是后台对后台。
也就是说,别的项目或者公司需要调用咱们写的接口,这个时候,参数的校验就显得格外重要,不想前后端那种,后台加不加校验都没有太大的影响。
2.准备工作
关于校验标准,可供java使用的一共有两套:
一种是:Java API
规范 (JSR303
) 定义了Bean
校验的标准validation-api
,但没有提供实现。
另一种是:hibernate validation
是对这个规范的实现,并增加了校验注解如@Email
、@Length
等。
Spring Validation
是对hibernate validation
的二次封装,用于支持spring mvc
参数自动校验。
接下来,我们以spring-boot
项目为例,介绍Spring Validation
的使用。
关于jar包的引用
如果spring-boot
版本小于2.3.x
,spring-boot-starter-web
会自动传入hibernate-validator
依赖;
如果spring-boot
版本大于2.3.x
,则需要手动引入依赖:
<!--spring对参数进行校验:hibernate validator-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.18.Final</version>
</dependency>
现在网络上都是旧的内容,用的是:
完全没有必要,使用第一个jar包就可以了。
3.具体实现
用法1:requestParam参数校验
描述:通常用于get请求或者请求参数比较少的情形。
校验生效的前提:
必须在Controller
类上标注@Validated
注解,在方法或者参数前添加无效!
如果校验失败,会抛出ConstraintViolationException
异常。
用法:
将请求入参一一在请求方法()内,进行罗列,并将校验注解添加在对应入参的前面。
用法2:pathVariable参数校验
描述:通过{}来动态配置请求路径,并将请求路径当成方法的入参之一。
校验生效的前提:
必须在Controller
类上标注@Validated
注解,在方法或者参数前添加无效!
如果校验失败,会抛出ConstraintViolationException
异常。
用法:校验注解可以放在@PathVariable前面也可以放在它的后面。
用法3:responseBody参数校验(application/json)
当请求方法入参有@RequestBody注解的时候,spring会将它识别成JSON格式的请求,要求调用方必须发送application/json格式的数据;
第1步:在实体类的字段上加上约束注解;
第2步:在方法参数上声明校验注解。
格式:@RequestBody+@Validated+实体类
或者:@RequestBody+@Valid+实体类
这种情况下,使用@Valid
和@Validated
都可以(只能用@Valid或@Validated的地方,下面会讲)。
用法4:responseBody参数校验(application/x-www-form-urlencoded)
当请求方法入参只有实体类接收的时候,spring会将它识别成FORM表单请求,要求调用方必须发送application/x-www-form-urlencoded格式的数据;
第1步:在实体类的字段上加上约束注解;
第2步:在方法参数上声明校验注解。
同样地,使用@Valid
和@Validated
都可以。
4.统一异常处理
如果校验失败,会抛出异常,我们需要对异常进行管理,以便返回一个更友好的提示。
下面是我总结的异常拦截配置类:
查看代码
import org.jetbrains.annotations.NotNull;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;
/**
* 全局异常处理器
* @description: @ControllerAdvice默认会对所有的请求进行处理;
* 对请求入参的校验异常,根本进不到Controller的方法体内,所以,只能在这拦截异常后返回友好错误提示
* 加basePackages,可以只对具体包名下面的请求进行处理
* @ControllerAdvice是一个增强的 Controller。使用这个 Controller,可以实现三个方面的功能:
* 全局异常处理
* 全局数据绑定
* 全局数据预处理
* 只拦截Controller,不会拦截Interceptor的异常
* @author: Marydon
* @date: 2020年08月10日 0010 16:39
*/
// 异常拦截位置
@RestControllerAdvice(basePackages = {"com.xx"
,"com.yy"})
public class CzWebExceptionHandler {
//处理Get请求中 使用@Valid 验证路径中请求实体校验失败后抛出的异常,详情继续往下看代码
@ExceptionHandler(BindException.class)
public CzResponseDto<JSONObject> BindExceptionHandler(@NotNull BindException e) {
String errorMsg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return CzResult.error("接口调取失败:" + errorMsg, null);
}
//处理请求参数格式错误 @RequestParam上validate失败后抛出的异常是javax.validation.ConstraintViolationException
@ExceptionHandler(ConstraintViolationException.class)
public CzResponseDto<JSONObject> ConstraintViolationExceptionHandler(@NotNull ConstraintViolationException e) {
String errorMsg = e.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining());
return CzResult.error("接口调取失败:" + errorMsg, null);
}
//处理请求参数格式错误 @RequestBody上validate失败后抛出的异常是MethodArgumentNotValidException异常。
@ExceptionHandler(MethodArgumentNotValidException.class)
public CzResponseDto<JSONObject> MethodArgumentNotValidExceptionHandler(@NotNull MethodArgumentNotValidException e) {
String errorMsg = e.getBindingResult().getAllErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage).collect(Collectors.joining());
return CzResult.error("接口调取失败:" + errorMsg, null);
}
// 要求Content-type为application/json,但是内容类型却是text/plain或者text时会被该异常捕获
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public CzResponseDto<JSONObject> HttpMediaTypeNotSupportedExceptionHandler(@NotNull HttpMediaTypeNotSupportedException e) {
String errorMsg = "本接口不接收application/json以外格式的数据,请检查入参是否是标准的json数据!\n" + e.getMessage();
return CzResult.error("接口调取失败:" + errorMsg, null);
}
@ExceptionHandler(Exception.class)
public CzResponseDto<JSONObject> handleException(@NotNull Exception e) {
String errorMsg = "系统异常,请联系开发人员Marydon进行排错!\n" + e.getMessage();
return CzResult.error("接口调取失败:" + errorMsg, null);
}
// 运行异常
@ExceptionHandler(RuntimeException.class)
public CzResponseDto<JSONObject> handleRuntimeException(@NotNull Exception e) {
return CzResult.error("接口调取失败:" + e.getMessage(), null);
}
// 服务异常
@ExceptionHandler(ServiceException.class)
public CzResponseDto<JSONObject> handleServiceException(@NotNull Exception e) {
return CzResult.error("接口调取失败:" + e.getMessage(), null);
}
// 请求方式异常(仅支持post请求)
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public CzResponseDto<JSONObject> handleHttpRequestMethodNotSupportedException(@NotNull Exception e) {
return CzResult.error("接口调取失败:" + e.getMessage(), null);
}
// 不存在的请求方法InterfaceMethod,在CzRequestParams可以查看支持的接口
// 注意:当请求数据格式为非text或text/plain时,也会抛出该异常(application/javascript,application/xml,text/xml,text/html)
// 无请求入参时也会抛出该异常
@ExceptionHandler(HttpMessageNotReadableException.class)
public CzResponseDto<JSONObject> handleHttpMessageNotReadableException(@NotNull Exception e) {
// return CzResult.error("接口调取失败:未知的接口方法,请仔细核对入参InterfaceMethod的值!", null);
return CzResult.error("接口调取失败:" + e.getMessage(), null);
}
}
仅供参考。
5.扩展延伸
延伸1:分组校验
在实际项目中,可能多个方法需要使用同一个实体类来接收参数,而不同方法的校验规则很可能是不一样的;
这个时候,简单地在实体的字段上加约束注解无法解决这个问题。
因此,spring-validation
支持了分组校验的功能,专门用来解决这类问题。
举个栗子:
A方法要求参数1的值必须为1,B方法则要求其值必须为2,如何实现?
第1步:在实体类当中添加接口类;
把接口类用作分组的依据;
注意:在实体类当中添加的接口,没有实际意义,仅仅将其作为分组依据;
由于spring校验的groups只能这样用,没有办法。
第1步:在约束注解里,添加该注解生效的的分组信息groups。
多个组之间使用逗号隔开;
并且,组必须以".class"进行结尾。
针对不同的组,可以添加不同的校验规则。
第2步:@Validated注解上指定校验分组。
注意:分组校验只能使用注解@Validated,不能使用@Valid!
另外,方法上使用了分组校验,实体类需要多组共用的字段规则校验,也必须添加组,即使是:校验规则一致的。
否则的话,该校验规则将会失效。
延伸2:嵌套校验
当入参实体类的某字段也是对象时,这时,需要对该对象里的字段进行校验时,这就牵扯到了:嵌套校验;
此时,入参实体类的对应的字段对象,必须标记@Valid注解。
嵌套的实体类,示例:
延伸3:集合校验(list)
如果请求体直接传递了json数组给后台,并希望对数组中的每一项都进行参数校验。
此时,如果我们直接使用java.util.Collection下的list或者set来接收数据,参数校验并不会生效!
我们可以使用自定义list集合来接收参数:
第1步:包装List类型,并声明@Valid注解;
查看代码
public class ValidationList<E> implements List<E> {
@Delegate
@Valid
public List<E> list = new ArrayList<>();
@Override
public String toString() {
return list.toString();
}
}
@Delegate注解受lombok版本限制,1.18.6以上版本可支持;
如果校验不通过,会抛出NotReadablePropertyException,同样可以使用统一异常进行处理。
第2步:校验调用
格式:@Validated + ValidationList<实体类>。
比如,我们需要一次性保存多个User对象,Controller层的方法可以这么写:
查看代码
@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
return Result.ok();
}
延伸4:自定义校验
延伸5:编程式校验
写在最后
哪位大佬如若发现文章存在纰漏之处或需要补充更多内容,欢迎留言!!!