起因,使用 springboot 创建了一个web项目,借助 mybatis 写了一个创建新用户的接口, 此时对客户端传递过来参数,该如何进行校验,哪个字段必传,如果某字段为空,如何返回一个指定的字符串消息?这样,我找到了
JSR303
规范
JSR-303 是一套用于对 JavaBean 进行验证得 Java API 规范 ,又叫做
bean validation 1.0
。当然,截止到目前为止,bean validation 规范已经在这二十几年里更新到了 1.1(JSR-349), 2.0(JSR380),3.0 等版本,还好不必担心本文内容已过时,因为升级后的版本,仅仅是添加了若干可使用的注解而已,核心内容都未变动。(关于历史版本文末有连接)
JSR-303 是一个规范,那么我应该如何去实现它?在 官方文档 中
第一个例子 2.1.2
所描述:1.先创建一个符合规范的注解,2.然后将该注解类标记在实体类上即可
首先,创建一个注解 OrderNumber
- 符合 JSR-303 标准的注解类,必须有 3 个字段
字段 message
定义:返回的错误消息。用于当 JavaBean 验证失败后,返回该内容字段 groups
定义:该元素指定与约束声明关联的处理组。比如设我们的昵称,我们可能会设定多个限制,只允许英文,长度在 10 个以内。以 yyds_开头…登等, 像这样有多重限制的规则可以定义在一个组内 NamesGroups,当然默认位空,该规则就是单独的。方法 payload()
定义:有效负载元素,该元素指定与约束声明关联的有效负载。这一方法通常是指定该注解只能够添加到哪个字段上去。默认是为空的,可以添加在任何字段上
package com.acme.constraint;
/**
* Mark a String as representing a well formed order number
*/
@Documented
@Constraint(validatedBy = OrderNumberValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
public @interface OrderNumber {
String message() default "{com.acme.constraint.OrderNumber.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
创建好之后,在字段上使用,就是这么简单
public class UserEntity implements Serializable {
@OrderNumber
private String oneOrder;
}
目前看起来, 似乎这个规范非常简单,仅仅是定义了几个字段和方法而已,似乎没什么特别的,但是对于一个规范而言,其实要考虑的东西非常多,摘取部分官方描述(感兴趣的可以查阅官方文档):
验证数据是在整个应用程序(从表示层到持久性层)中发生的常见任务。通常,在每一层中都实现了相同的验证逻辑,事实证明这很耗时且容易出错。为了避免在每一层中重复这些验证,开发人员通常将验证逻辑直接捆绑到域模型中,从而将域类与验证代码(实际上是有关类本身的元数据)混杂在一起。
此 JSR 定义了用于 JavaBean 验证的元数据模型和 API。默认的元数据源是注释,能够通过使用 XML 验证描述符来覆盖和扩展元数据。
此 JSR 开发的验证 API 不适用于任何一个层或编程模型。它特别不依赖于 Web 层或持久性层,并且可用于服务器端应用程序编程以及富客户端 Swing 应用程序开发人员。此 API 被视为 JavaBeans 对象模型的一般扩展,因此有望用作其他规范中的核心组件。易用性和灵活性影响了该规范的设计
JSR-303
规范的框架。前面已经介绍了 JSR-303
规范是什么以及如何实现它。
在实际开发工作中,如果靠我们自己去实现很麻烦,而且后续升级交接也很头疼,如果有现成的,实现了这个规范的框架就好了。
因此,我找到了若干通用框架,只需要引入坐标即可。
接下来列举最常见通用的两种情景:
spring boot (version<2.3)
框架内部已经默认实现了 JSR-303 规范,可以直接使用其现成的注解
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
spring boot (version>=2.3)
由于 web 框架内部已经剥离了该实现,因此需要引入这个依赖包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependencies>
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>${api-version}</version>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>${hibernate-validator-version}</version>
</dependency>
</dependencies>
compile group: 'org.springframework.boot', name: 'spring-boot-starter-validation', version: '2.2.6.RELEASE'
Bean Validation 的内嵌的注解
有以下这些,也意味着 引入的符合 JSR303 规范的框架 ,其内部也同样实现了这些注解。
注解 | 详细信息 |
---|---|
@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(value) | 被注释的元素必须符合指定的正则表达式 |
使用示例:
// 在字段上使用
public class UserEntity implements Serializable {
@Null
private Integer id;
@NotNull
private String userName;
@Pattern(regexp = "^(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,120}$", message = "密码最少为8位字母和数字组合")
private String pwd;
@NotBlank(message = "性别不能为空")
@Min(value = 0,message = "性别不能既非男又非女")
private String sex;
@Max(value = 100,message = "不准超过65")
private Integer age;
@DecimalMin(value="200000", message = "工资不能低于每月2W")
private Integer salary;
@Past
private Date pastDay;
}
// 在方法上使用
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
@Validated
public interface SystemUserService {
// 注销用户
void logOff(@Min(1) long userId,@NotNull @Size(min = 10, max = 200) String reasonForlogOff);
}
通常情况下,我们都在 controller
这验证请求参数,只需要添加 @Valid
,非常简单:
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
...
@RestController
public class UserController {
@PostMapping("/users")
ResponseEntity<String> addUser(@Valid @RequestBody User user, BindingResult bindingResult) {
// BindingResult中存储校验结果
// 如果有错误提示信息
if (bindingResult.hasErrors()) {
Map<String , String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach( (item) -> {
String message = item.getDefaultMessage();
String field = item.getField();
map.put( field , message );
} );
// 返回提示信息
return ResponseEntity.error(map.toString());
}
return ResponseEntity.ok("创建成功");
}
}
假设这样一种情况:当我们新建用户时,不需要传用户 ID,让数据库自增 ID, 但是修改用户时需要传递一个用户 ID,并且新增和修改用的都是同一个 实体 接收参数,此时的约束条件该如何写?
public class Groups {
public interface Add{}
public interface Update{}
}
groups
. 新增时,约束条件@Null
生效。更新时,约束条件@NotNull
生效.// 在字段上使用
public class UserEntity implements Serializable {
@Null(message = "新增不需要指定id" , groups = Groups.Add.class)
@NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
private Integer id;
@NotNull
private String userName;
}
@Valid
改为 @Validated(Groups.Add.class)
. SR303 本身的 @Valid 并不支持分组校验,但是 Spring 提供了一个注解@Validated 支持分组校验import javax.validation.Valid;
import org.springframework.validation.BindingResult;
...
@RestController
public class UserController {
@PostMapping("/users")
ResponseEntity<String> addUser(@Validated(Groups.Add.class) @RequestBody User user, BindingResult bindingResult) {
//如果有错误提示信息
if (bindingResult.hasErrors()) {
Map<String , String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach( (item) -> {
String message = item.getDefaultMessage();
String field = item.getField();
map.put( field , message );
} );
//返回提示信息
return ResponseEntity.error(map.toString());
}
return ResponseEntity.ok("创建成功");
}
}
此时,当发起请求时,传递了参数 id, 那么就会返回错误信息:新增不需要指定id
### 新增用户
POST http://localhost:9000/user/register
Content-Type: application/json
{
"id": 1234,
"phone": 13366668888,
"pwd": "12345678abc"
}
什么是嵌套校验,说白了就是一个实体上,其中一个字段是另一个实体,那么字这个字段上添加上注解@Valid
即可
public class UserEntity implements Serializable {
@Null(message = "新增不需要指定id" , groups = Groups.Add.class)
@NotNull(message = "修改需要指定id" , groups = Groups.Update.class)
private Integer id;
/**
* @Valid 重要
*/
@Valid
@NotNull
private RoleEntity role;
}
class RoleEntity implements Serializable {
@NotBlank(message = "角色名称不能为空")
private String roleName;
}
需要注意的是,嵌套校验对于分组有严格校验,也就是说,在 controller 中使用@Validated
时不应该再指定分组了
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
...
@RestController
public class UserController {
// @Validated 不能指定分组为 groups = Groups.Add.class,否则嵌套校验role会失效
@PostMapping("/users")
ResponseEntity<String> addUser(@Validated @RequestBody User user, BindingResult bindingResult) {
//如果有错误提示信息
if (bindingResult.hasErrors()) {
Map<String , String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach( (item) -> {
String message = item.getDefaultMessage();
String field = item.getField();
map.put( field , message );
} );
//返回提示信息
return ResponseEntity.error(map.toString());
}
return ResponseEntity.ok("创建成功");
}
}
虽说在日常的开发中内置的约束注解已经够用了,但是仍然有些时候不能满足需求,需要自定义一些校验约束。
比如:有这样一个例子,性别字段只允许传入 1 和 2,否则校验失败。
三步走:
1.创建注解
DefineRangeValidationValues
,其中在类上额外指定的规则@NotNull
也会生效.另外为了通用,创建一个 values 字段用来接收稍后传入的数据
@Documented
@Constraint(validatedBy = DefineRangeValidator.class)
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@NotNull(message = "不能为空")
public @interface DefineRangeValidationValues {
String message() default "传入的值不在范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* @return 传入的值
*/
int[] values() default {};
}
DefineRangeValidator
package com.mock.water.core.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.HashSet;
import java.util.Set;
/**
* $$ 第一个泛型是校验注解,第二个是参数类型
*
* @author [email protected]
* @version 1.0.0
* @date 2022/10/14 17:18
**/
public class DefineRangeValidator implements ConstraintValidator<DefineRangeValidationValues, Integer> {
/**
* 存储枚举值
*/
private Set<Integer> enumValues = new HashSet<>();
/**
* 初始化
*
* @param constraintAnnotation 约束注释
*/
@Override
public void initialize(DefineRangeValidationValues constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
for (int value : constraintAnnotation.values()) {
enumValues.add(value);
}
}
/**
* 是否有效
*
* @param value 整数
* @param constraintValidatorContext 约束验证器上下文
* @return boolean
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext constraintValidatorContext) {
//判断是否包含这个值
return enumValues.contains(value);
}
}
- 测试一下
// 在字段上使用
public class UserEntity implements Serializable {
@DefineRangeValidationValues(values = {1, 2}, message = "性别只能传入1或者2")
private Integer sex;
}
在上面示例中, 已经看到我们可以通过 validation 框架提供的类 BindingResult
来接受参数。
这种方式弊端很明显,每一个接口都需要在传递参数的位置写上声明,以及对校验结果进行处理。因此正确的做法是进行全局异常捕捉
参数在校验失败的时候会抛出的MethodArgumentNotValidException
或者BindException
两种异常,可以在全局的异常处理器中捕捉到这两种异常,将提示信息或者自定义信息返回给客户端。
作者这里就不再详细的贴出其他的异常捕获了,仅仅贴一下参数校验的异常捕获
package com.mock.water.core.group.exception;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* $$
*
* @author [email protected]
* @version 1.0.0
* @date 2022/10/14 16:45
**/
@RestControllerAdvice
public class GlobalExceptionHandler {
@Autowired
private ObjectMapper objectMapper;
/**
* 参数校验异常步骤
*/
@ExceptionHandler(value= {MethodArgumentNotValidException.class , BindException.class})
public String onException(Exception e) throws JsonProcessingException {
BindingResult bindingResult = null;
if (e instanceof MethodArgumentNotValidException) {
bindingResult = ((MethodArgumentNotValidException)e).getBindingResult();
} else if (e instanceof BindException) {
bindingResult = ((BindException)e).getBindingResult();
}
Map errorMap = new HashMap<>(16);
bindingResult.getFieldErrors().forEach((fieldError)->
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage())
);
return objectMapper.writeValueAsString(errorMap);
}
}
------ 如果文章对你有用,感谢右上角 >>>点赞 | 收藏 <<<