对于写Java的同学来说,参数校验是繁琐且重复性很高的代码。很多时候我们的业务代码编写之前先要进行很多的参数校验,浪费了大量的时间和精力。而java中其实已经内置了参数校验的工具,本篇文章主要介绍如何使用Javax.validation来进行参数校验。
@validated注解
@validated
是一套帮助我们继续对传输的参数进行数据校验的注解,通过配置Validation可以很轻松的完成对数据的约束。看到一下注解的源码,我们可以看到@Validated
注解可以作用在类、方法和参数上。
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
Class>[] value() default {};
}
废话不多说,我们直接举个例子来看看@validated
到底好不好用。实际项目中很常见的应用是分页查询的接口,通常分页查询至少需要当前页和页大小这两个字段。通常我们会把分页请求需要的参数封装成一个PageQuery。一个常见的分页参数类,在很多接口中需要使用。我们就可以给这样的参数加上@Validated
注解。表示此类开启参数校验
public Result>> getPage(@Validated PageQuery pageQuery) {
// 设置页大小,当前页
Page
在类的字段上,我们定义校验的规则和返回的错误提示。@validated中所有的校验注解,可以参考下面的表格。
限制 | 说明 |
---|---|
@Null | 限制只能为null |
@NotNull | 限制必须不为null |
@AssertFalse | 限制必须为false |
@AssertTrue | 限制必须为true |
@DecimalMax(value) | 限制必须为一个不大于指定值的数字 |
@DecimalMin(value) | 限制必须为一个不小于指定值的数字 |
@Digits(integer,fraction) | 限制必须为一个小数,且整数部分的位数不能超过integer,小数部分的位数不能超过fraction |
@Max(value) | 限制必须为一个不大于指定值的数字 |
@Min(value) | 限制必须为一个不小于指定值的数字 |
@Past | 限制必须是一个过去的日期 |
@Future | 限制必须是一个将来的日期 |
@Pattern(value) | 限制必须符合指定的正则表达式 |
@Size(max,min) | 限制字符长度必须在min到max之间 |
@NotEmpty | 验证注解的元素值不为null且不为空(字符串长度不为0、集合大小不为0) |
@NotBlank | 验证注解的元素值不为空(不为null、去除首位空格后长度为0),不同于@NotEmpty,@NotBlank只应用于字符串且在比较时会去除字符串的空格 |
验证注解的元素值是Email,也可以通过正则表达式和flag指定自定义的email格式 |
对于页大小,我们限制为非空且不能小于1;对于当前页我们限制为非空。
public class PageQuery implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 页大小
*/
@NotNull(message = "页大小不能为空")
@Min(message = "页大小不能小于1", value = 1)
Integer pageSize;
/**
* 当前页
*/
@NotNull(message = "当前页不能为空")
Integer currentPage;
我们用一个明显校验不通过的参数来请求下这个接口,看看会返回什么。我在请求参数中不传递pageSize
参数,然后发送请求。
{
"timestamp": "2021-12-16T03:09:36.238+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotNull.pageQuery.currentPage",
"NotNull.currentPage",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"pageQuery.currentPage",
"currentPage"
],
"arguments": null,
"defaultMessage": "currentPage",
"code": "currentPage"
}
],
"defaultMessage": "当前页不能为空",
"objectName": "pageQuery",
"field": "currentPage",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotNull.pageQuery.env",
"NotNull.env",
"NotNull.java.lang.String",
"NotNull"
],
"arguments": [
{
"codes": [
"pageQuery.env",
"env"
],
"arguments": null,
"defaultMessage": "env",
"code": "env"
}
],
"defaultMessage": "设备所属环境信息不能为空",
"objectName": "pageQuery",
"field": "env",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
},
{
"codes": [
"NotNull.pageQuery.pageSize",
"NotNull.pageSize",
"NotNull.java.lang.Integer",
"NotNull"
],
"arguments": [
{
"codes": [
"pageQuery.pageSize",
"pageSize"
],
"arguments": null,
"defaultMessage": "pageSize",
"code": "pageSize"
}
],
"defaultMessage": "页大小不能为空",
"objectName": "pageQuery",
"field": "pageSize",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotNull"
}
],
"message": "Validation failed for object='pageQuery'. Error count: 3",
"path": "/localLoadBalance/approval/getApproval"
}
可以看到,返回的结果中包含了我们之前预设的校验提示和内容。不过到这里我们的任务还没有结束,实际项目中,我们不允许接口返回这样的类型。大多数情况下,我们希望接口的返回结果有通用的模板格式。而上面那样的返回方式需要前端做大量的解析,而且也不符合后端接口的规范。因此我们希望能有一个全局的处理器,来解析@validated
抛出的异常。
使用全局异常处理类来进行统一的异常处理
在Spring boot项目中,我们可以使用@ControllerAdvice
注解来进行全局的异常处理,当然@ControllerAdvice
的用处不止是异常处理,还可以实现统一的参数绑定和数据的预处理。详情可以参考# SpringMVC 中 @ControllerAdvice 注解的三种使用场景!
首先我们新增一个handler,当然你也可以指定一个包来扫描包下的所有controller。如@ControllerAdvice(basePackages="com.test.controller")
,然后我们使用@ExceptionHandler
来进行异常的处理。
此处需要说明的是,我们是针对@validated
进行的异常的处理,因此我们希望异常校验类只拦截@validated
注解抛出的异常。所以在本方法中,我只让@ExceptionHandler
拦截了BindException。其次,针对参数校验出现多个异常的情况,我们把多个错误信息通过逗号分隔开来。
@ControllerAdvice
public class GlobalHandler {
private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
/**
* 全局处理所有使用了@validation校验参数的controller
* @param e 捕获到validation抛出异常
* @return 返回参数中所有的校验错误,以,分隔不用的错误信息
*/
@ResponseBody
@ExceptionHandler(BindException.class)
public Result exceptionHandler(BindException e) {
String errors=e.getBindingResult().getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(","));
logger.error("Request params error,caught by global exception handler,{}",errors);
return Result.toBuilder()
.code(0)
.msg(errors)
.builder();
}
}
再次准备一个含有错误参数的请求,这次我们不传currentPage,pageSize的值为-1。我们看看会返回什么。
{
"code": 0,
"msg": "当前页不能为空,页大小不能小于1",
"data": null
}
可以看到,返回的结果符合我们的预期。
在获取错误信息的地方我们看到有针对BindException的异常信息解析,涉及了多个.
操作。有经验的老鸟可能觉得这里容易出现空指针异常。不过此处你大可放心,BindException中的BindingResult是绝对不会为null
的。我们看下源码,可以看到内部是用断言来保证结果不为空的。
/**
* Create a new BindException instance for a BindingResult.
* @param bindingResult the BindingResult instance to wrap
*/
public BindException(BindingResult bindingResult) {
Assert.notNull(bindingResult, "BindingResult must not be null");
this.bindingResult = bindingResult;
}
参考文章
SpringMVC 中 @ControllerAdvice 注解的三种使用场景! - 江南一点雨 - 博客园 (cnblogs.com)
javax.validation 参数验证 - 不朽丶 - 博客园 (cnblogs.com)
@Validated详解 - yuxinkuan - 博客园 (cnblogs.com)