下下下周,争取做只水煮鱼~~~
算了吧,买现成的调料吧~~~
fish.png
1 场景
JavaWeb后台应用程序,具体的执行方法,收到请求,需要对请求的数据
进行基础校验
,如字符串长度限制、正则校验、数字区间校验等。
推荐在springMVC中对前台的请求参数进行统一校验,校验方式建议采用JSR30标准
进行校验。
1.1 普通校验方式
最简单的校验方式是,对请求的参数手动一个个进行校验
,如下代码:
@GetMapping("saveWithOld")
public JSONObject saveWithOld(User user) {
JSONObject result = new JSONObject();
if (user.getUserCode() == null || user.getUserCode() == "") {
result.put("success", true);
result.put("message", "用户代码不可为空");
return result;
}
if (user.getUserName() == null || user.getUserName() == "") {
result.put("success", true);
result.put("message", "用户名称不可为空");
return result;
}
// do something ......
result.put("success", true);
return result;
}
这种方式,代码量非常大
,代码非常不友好
。
1.2 springMVC校验方式
springMVC,在执行后台方法之前
,可以对请求的数据通过注解
进行校验。此校验方式基于JSR303规范
。
如下代码所示:
@Data
public class User {
@NotNull(message = "用户代码不可为空")
private String userCode;
}
@GetMapping("saveWithNormal")
public JSONObject saveWithNormal(@Valid User user) {
JSONObject result = new JSONObject();
result.put("success", true);
result.put("message", user.toString());
return result;
}
@PostMapping("saveWithRequestParam")
public JSONObject saveWithRequestParam(@NotNull(message = "用户代码不可为空") String userCode) {
JSONObject result = new JSONObject();
result.put("success", true);
result.put("message", userCode);
return result;
}
此种方式,可以使用注解,已更简单的方式对请求参数进行校验。
3 版本说明
本文中代码涉及到的相关版本如下:
3.1 JDK
JDK1.8
3.2 maven依赖
spring-boot-starter-web中已包含了我们需要的依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.9.RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.7 </version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.18</version>
<scope>provided</scope>
</dependency>
2 名词关系说明
这里讲下springMVC中使用JSR303进行参数校验,相关的名词含义
及名词之间的关系说明
。
2.1 基本说明
springMVC基于JSR303
规范进行校验。
官网说明:https://jcp.org/en/jsr/detail?id=303
规范的相关说明如下:
JSR是Java Specification Requests的缩写,意思是Java 规范提案 。
JSR-303 是JAVA EE 6 中的一项子规范,叫做Bean Validation
。
Hibernate Validator是 Bean Validation的参考实现
。
Hibernate Validator提供了JSR 303 规范中所有内置 constrain(约束)的实现,除此之外还有一些附加的constraint(约束)
。
2.2 详细说明
关于springMVC请求参数校验,涉及几个对应的名词,如下是:
名词 | 说明 |
---|---|
constraint(约束) | 对参数的校验约束 注解,如@NotNull 表示参数不可以为Null |
校验注解 | 为元素加上约束后,有时候 需要在参数前加上校验注解 来开启验证。相关注解有 @Valid 和@Validated ,如没有用到注解独有的特性 (分组、嵌套)等,用哪个注解 都一样。需注意不是所有的校验都需要开启校验,如下不需要加上校验注解: saveWithRequestParam(@NotNull(message = "用户代码不可为空") String userCode) 但是需要在 Controller类上 加上注解@Valid或@Validated |
JSR303规范 | 行业规范标准 ,包括校验的constraint(约束,如@NotNull) 和开启校验注解@Valid 等体现:代码中体现为 注解、接口 ,无具体实现代码 jar包:jakarta.validation-api-2.0.2.jar 约束注解:javax.validation.constraints包下注解+hibernate增强注解org.hibernate.validator.constraints 校验注解: javax.validation.Valid
|
Hibernate Validator | Hibernate对JSR303规范中的约束constraint 的具体代码实现 jar包:hibernate-validator-6.0.20.Final.jar 增强:在原有JSR303的 constraint(约束) 中增加了约束(如@Range)
|
spring JSR303 | spring对JSR303 的包装,对原有的校验进行了增强 增强: 分组校验 、顺序校验 缺点: 不支持嵌套校验 约束注解:javax.validation.constraints包下注解+hibernate增强注解org.hibernate.validator.constraints 校验注解: org.springframework.validation.annotation.Validated 增强说明:所谓的包装和增强,只是将 @Valid 注解扩展为@Validated 注解。约束注解和JSR303一样。 |
2.3 关系图
一图胜千言。参数校验的相关说明,关系图如下:
spring参数校验.jpg3 校验流程
3.1 对象参数
3.1.1 说明
在对象参数
中进行约束校验。需满足以下条件:
(1)在mapping方法
中通过注解@Valid或@Validated指定要校验的参数对象
如下:
@GetMapping("saveWithNormal")
public JSONObject saveWithNormal(@Valid User user) {......}
(2)在对象参数
对应的类中,对需要校验的参数加上约束注解
如下:
@Data
public class User {
/**
* 用户代码
*/
@NotNull(message = "用户代码不可为空")
private String userCode;
}
3.1.2 校验流程
校验失败后,需要对失败的异常信息进行处理,处理方式有两种:
1、在mapping方法上加上参数BindingResult bindingResult
此种方式,校验失败后,会将异常信息封装到参数对象bindingResult
中,可以自行
对其中的异常信息进行处理,封装错位信息,返回请求结果
。
这种情况,需要每个请求,都对参数BindingResult
进行处理,较为繁琐,不建议此种方式。
如下:
@GetMapping("saveWithBind")
public JSONObject saveWithBind(@Valid User user, BindingResult bindingResult) {
// --------------------[手动检测验证是否通过]--------------------
if (bindingResult.hasErrors()) {
for (FieldError fieldError : bindingResult.getFieldErrors()) {
JSONObject result = new JSONObject();
result.put("success", false);
result.put("message", fieldError.getDefaultMessage());
return result;
}
}
// --------------------[验证检测通过后执行其他操作]--------------------
// ......
JSONObject result = new JSONObject();
result.put("success", true);
result.put("message", user.toString());
return result;
}
2、定义spring全局异常处理,捕捉对应的异常信息,进行统一处理
mapping方法上不加参数BindingResult bindingResult
,校验失败后,会抛出异常
,异常信息,通过spring全局异常管理
,统一对抛出的异常信息进行处理,处理后统一封装错位信息。这种方式,代码量较少,且处理错误信息集中,推荐此种方式。
如下代码:
@Data
public class User {
/**
* 用户代码
*/
@NotNull(message = "用户代码不可为空")
private String userCode;
}
// 参数校验失败,抛出异常:org.springframework.validation.BindException
@GetMapping("saveWithNormal")
public JSONObject saveWithNormal(@Valid User user) {
JSONObject result = new JSONObject();
result.put("success", true);
result.put("message", user.toString());
return result;
}
/**
* 捕捉全局异常:org.springframework.validation.BindException
* <div>普通请求的参数,校验失败,抛出此异常</div>
* <div>如:(@Valid User user)</div>
*
* @param exception
* @return
*/
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {
log.info("全局异常[BindException]:" + exception.getMessage());
JSONObject result = new JSONObject();
result.put("success", false);
if (exception != null) {
String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
result.put("message", message);
}
return result;
}
需注意:参数上@Valid和@Validated使用方式的不同
,校验失败后,会抛出不同的异常
:
如下:
/**
* 捕捉全局异常:org.springframework.validation.BindException
* <div>普通请求的参数,校验失败,抛出此异常</div>
* <div>如:(@Valid User user)</div>
* @param exception
* @return
*/
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {......}
/**
* 捕捉全局异常:org.springframework.web.bind.MethodArgumentNotValidException
* <div>@RequestBody修饰的参数,校验失败,抛出此异常</div>
* <div>如:(@RequestBody @Valid User user)</div>
* @param exception
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public JSONObject handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {......}
总结校验流程图如下:
对象参数校验流程.jpg3.2 普通类型参数
3.2.1 说明
在普通类型参数
中进行约束校验。需满足以下条件:
(1)在Controller类
上通过注解@Validated
开启校验
注意:需为@Validated注解,而不是@Valid注解
如下:
@Validated
@RestController
@RequestMapping("user")
public class UserController {......}
(2)在mapping方法
的普通类型
参数前面加上约束注解
如下:
// 参数校验失败,抛出异常:javax.validation.ConstraintViolationException
@PostMapping("saveWithRequestParam")
public JSONObject saveWithRequestParam(@NotNull(message="用户代码不可为空") String userCode){......}
// 参数校验失败,抛出异常:org.springframework.web.bind.ConstraintViolationException
@GetMapping("saveWithRestful/{userCode}")
public JSONObject saveWithRest(@PathVariable("userCode") @Length(max = 10,message="用户代码不可超过10位") String userCode) {......}
3.2.1 校验流程
校验结果的处理流程同《3.1对象参数》
需注意:如使用全局异常捕捉,校验失败后抛出的异常
如下:
/**
* 捕捉全局异常:javax.validation.ConstraintViolationException
* <div>直接在参数上加的校验,校验失败,抛出此异常</div>
* <div>如:(@NotNull(message = "用户代码不可为空") String userCode)</div>
*
* @param exception
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
public JSONObject handlerConstraintViolationException(ConstraintViolationException exception) {......}
总结校验流程图如下:
普通参数校验流程.jpg4 嵌套校验
嵌套校验,准确来说,是对象内约束的嵌套校验
。指的是校验A对象,A对象内有个属性B是对象
,B对象内部属性仍然有约束。需要对A对象的约束
+A对象内B对象的约束
进行校验,这种就是嵌套约束。
4.1 代码示例
这里既校验参数user中的userCode约束
又需要校验user中的属性对象department
中的departmentCode的约束
。
- 实体定义
@Data
public class Department {
@NotNull(message = "部门代码不可为空")
private String departmentCode;
}
@Data
public class User {
@NotNull(message = "用户代码不可为空")
private String userCode;
@Valid
@NotNull(message = "部门不可为空")
private Department department;
private String userName;
}
- mapping方法
@GetMapping("saveWithLevel")
public JSONObject saveWithLevel(@Valid User user) {
JSONObject result = new JSONObject();
result.put("success", true);
result.put("message", user.toString());
return result;
}
- 异常处理
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {
log.info("全局异常[BindException]:" + exception.getMessage());
JSONObject result = new JSONObject();
result.put("success", false);
if (exception != null) {
String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
result.put("message", message);
}
return result;
}
4.2 代码测试
- 请求信息
http://localhost:8080/user/saveWithLevel?department.departmentCode=001
- 返回结果
{"success":false,"message":"部门代码长度需在5~10之间,用户代码不可为空"}
可见嵌套校验起作用了,对象user的内部普通属性userCode和内部对象department的自己的约束都起作用了。
4.3 总结
-
实现嵌套校验,在被校验对象的
内部属性对象上
,必须加上@Valid注解
-
mapping方法参数前,用
@Valid注解和@Validated没有区别
5 分组校验
同一个javaBean,我们加上约束注解后,这个javaBean作为请求参数的对象类型,其中的约束注解,会对参数对象的内容进行校验。
有时候,不同的请求我们会使用相同的javaBean作为对象的参数类型
,如新增用户
和更新用户
我们都会使用用户
这个JavaBean作为请求参数的封装对象。
5.1 代码示例
比如,我们新增用户,需要设置密码;更新用户,不需要设置密码。
代码如下:
- 分组类型
分组接口不需要有实现,仅仅作为一个分组类型
public interface Add {
}
public interface Edit {
}
- 实体定义
通过约束中的group参数
,来指定对应的分组类型,可以指定多个
@Data
public class User {
@NotNull(message = "用户代码不可为空", groups = {Add.class, Edit.class})
private String userCode;
@NotNull(message = "密码不可为空", groups = {Add.class})
private String password;
}
- mapping方法
校验方式,只能指定@Validated
,其中的value为这个参数的分组类型
,和类中约束注解的groups属性相对性
,可以指定多个
。
@GetMapping("groupAdd")
public JSONObject groupAdd(@Validated(Add.class) User user) {
JSONObject result = new JSONObject();
result.put("success", true);
result.put("message", user.toString());
return result;
}
@GetMapping("groupEdit")
public JSONObject groupEdit(@Validated(Edit.class) User user) {
JSONObject result = new JSONObject();
result.put("success", true);
result.put("message", user.toString());
return result;
}
- 异常处理
同4.1
5.2 代码测试
-
新增
-
请求
-
结果
{"success":false,"message":"用户代码不可为空,密码不可为空"}
-
-
编辑
-
请求
-
结果
{"success":false,"message":"用户代码不可为空"}
-
5.3 总结
(1)分组校验中,定义的分组类型
接口,不需要有实现内容,仅仅是作为分组的一个类型
存在,不同的业务,可以共用相同的类型。
(2)约束中的分组类型,可以定义多个。
(3)@Validated中的分组类型,也可以指定多个。
(4)校验的时候,根据@Validated中指定分组类型,
,去找校验对象中的对应有此分组类型的约束
,进行校验。
(5)指定分组后,不满足分组的约束
(不加分组的约束为默认分组,也是一种分组),不会进行校验
6 顺序校验
如不进行顺序校验配置,校验对象内的属性,校验顺序是随机
的。
有时候想先校验比较简单的约束,再校验复杂的,因此需要指定约束的校验顺序。可以结合《7 验证将检测到第一个约束违例时停止
》一起使用。
6.1 代码示例
- 分组类型
// 分组类型:第一个执行
public interface FirstCheck {
}
// 分组类型:第二个执行
public interface SecondCheck {
}
// 待顺序的分组类型组
@GroupSequence({FirstCheck.class, SecondCheck.class})
public interface UserGroupCheck {
}
- 实体定义
@Data
public class User {
@NotNull(message = "用户代码不可为空", groups = {FirstCheck.class})
private String userCode;
@NotNull(message = "密码不可为空", groups = {SecondCheck.class})
private String password;
@NotNull(message = "用户名不可为空")
private String userName;
}
- mapping方法
@GetMapping("orderCheck")
public JSONObject orderCheck(@Validated(UserGroupCheck.class) User user) {
JSONObject result = new JSONObject();
result.put("success", true);
result.put("message", user.toString());
return result;
}
- 异常处理
同4.1
6.2 代码测试
-
请求1
- 请求
http://localhost:8080/user/orderCheck
- 结果
{"success":false,"message":"用户代码不可为空"}
-
请求2
- 请求
http://localhost:8080/user/orderCheck?userCode=001
- 结果
{"success":false,"message":"密码不可为空"}
请求3
-
- 请求
http://localhost:8080/user/orderCheck?userCode=001&password=123456
- 结果
{"success":true,"message":"User(userCode=001, password=123456, userName=null)"}
-
特殊请求
-
变更
将实体类进行变更,userName上的约束也加上分组为FirstCheck。此时userCode和userName的约束分组均为FirstCheck。
如下:
@Data public class User { @NotNull(message = "用户代码不可为空", groups = {FirstCheck.class}) private String userCode; @NotNull(message = "密码不可为空", groups = {SecondCheck.class}) private String password; @NotNull(message = "用户名不可为空", groups = {FirstCheck.class}) private String userName; }
-
请求
-
结果
-
{"success":false,"message":"用户代码不可为空,用户名不可为空"}
或
```json
{"success":false,"message":"用户名不可为空,用户代码不可为空"}
可以看出,当一个分组内有多个约束,约束的校验顺序仍然是随机的
6.3 总结
(1)根据参数中的分组对应的接口中@GroupSequence
指定的分组类型
的顺序进行加校验
(2)只有当一个分组内的所有约束都校验通过后
,才会进入下一个分组
进行校验。
(3)顺序校验,指的是@GroupSequence
内配置的分组的顺序,当一个分组
内有多个约束
,这个分组内约束
的校验顺序仍然随机
7 验证将检测到第一个约束违例时停止
默认,有多个约束的情况下
,将会对所有参数进行校验
,如果存在校验失败的约束,返回的校验结果(BindingResult或对应Exception)中会有所有的参数校验错误信息
。即如果多个不满足约束,则返回结果中会有多个失败信息
。
有时候,我们只需要返回第一个
一个校验失败的约束信息就好,校验到一个约束失败后,没有必要再花费代价
进行其他约束校验。
springBoot中,参数校验的实现,基于MethodValidationPostProcessor
:
@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator);
return processor;
}
这个postProcessor的校验配置基于spring中的beanValidator
,我们创建自己的Validator的bean,配置failFast
,即可实现验证将检测到第一个约束违例时停止
这个要求。
实现代码如下:
@Bean
public Validator validator() {
HibernateValidatorConfiguration configuration = Validation.byProvider(HibernateValidator.class).configure();
//验证将检测到第一个约束违例时停止
configuration.failFast(true);
ValidatorFactory validatorFactory = configuration.buildValidatorFactory();
return validatorFactory.getValidator();
}
或使用更简洁的写法:
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
//验证将检测到第一个约束违例时停止
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
failFast
是HibernateValidatorConfiguration
中的一个属性配置,配置中还有其他配置属性
,可以定制我们的校验器
。
8 自定义校验器
自定义校验器,注意点比较多,不是本文的重点,暂时不进行记录,后续有时间会有专门的文章进行分析。
9 生产环境配置
前面说的都是原理和使用细节,这里记录下生产环境,需要进行哪些全局配置。
9.1 全局异常处理
建议使用全局异常处理,对请求的异常信息进行统一处理。
代码如下:
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
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;
/**
* 统一异常处理
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 捕捉全局异常:org.springframework.web.bind.MethodArgumentNotValidException
* <div>@RequestBody修饰的参数,校验失败,抛出此异常</div>
* <div>如:xxxAction(@RequestBody @Valid User user)</div>
* @param exception
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public JSONObject handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
log.info("全局异常[MethodArgumentNotValidException]:" + exception.getMessage());
JSONObject result = new JSONObject();
result.put("success", false);
if (exception != null) {
String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
result.put("message", message);
}
return result;
}
/**
* 捕捉全局异常:org.springframework.validation.BindException
* <div>普通请求的参数,校验失败,抛出此异常</div>
* <div>如:xxxAction(@Valid User user)</div>
* @param exception
* @return
*/
@ExceptionHandler(BindException.class)
public JSONObject handlerBindException(BindException exception) {
log.info("全局异常[BindException]:" + exception.getMessage());
JSONObject result = new JSONObject();
result.put("success", false);
if (exception != null) {
String message = exception.getBindingResult().getFieldErrors().stream().filter(e -> e != null).map(FieldError::getDefaultMessage).collect(Collectors.joining(","));
result.put("message", message);
}
return result;
}
/**
* 捕捉全局异常:javax.validation.ConstraintViolationException
* <div>直接在参数上加的校验,校验失败,抛出此异常</div>
* <div>如:xxxAction(@NotNull(message = "用户代码不可为空") String userCode)</div>
* <div>如:xxxAction(@PathVariable("userCode") @Length(max = 10,message="用户代码不可超过10位") String userCode)</div>
* @param exception
* @return
*/
@ExceptionHandler(ConstraintViolationException.class)
public JSONObject handlerConstraintViolationException(ConstraintViolationException exception) {
log.info("全局异常[ConstraintViolationException]:" + exception.getMessage());
JSONObject result = new JSONObject();
result.put("success", false);
if (exception != null) {
result.put("message", exception.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(";")));
}
return result;
}
}
参数校验失败,返回的错误json信息如下,可以根据项目的实际情况进行定制:
{"success":false,"message":"用户代码不可超过10位"}
9.2 验证将检测到第一个约束违例时停止
import org.hibernate.validator.HibernateValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
@Configuration
public class ValidConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
//验证将检测到第一个约束违例时停止
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
10 补充
10.1 校验限制
@Valid支持嵌套验证
、@Validated支持分组验证
、排序验证
(准确来说,排序验证也是分组验证的一种)。
故无法实现
:“嵌套验证+分组验证
”和“嵌套验证+排序验证
”这种组合形式的验证。
10.2 建议
虽然JSR303支持自定义校验器,笔者不建议将太复杂的校验交给JSR303的标准进行校验
。
如果是参数基本的属性校验(是否为空、长度、大小、枚举、正则格式),可以以这种形式进行校验。
但是如果是太复杂的校验
,如需要连接数据库进行业务判断的校验,笔者仍然建议在具体的业务代码
中进行校验。
10.2 校验顺序的随机性
如不使用@Validated指定约束的校验顺序,所有约束的校验顺序是随机的,即相同的情况,返回的校验结果的顺序可能不一样。
10.3 一个字段多个约束
同一个字段可以加多个约束注解,并不是只能有一个约束注解。如下:
@NotNull(message = "用户代码不可为空")
@Length(min = 5, max = 10, message = "用户代码长度需在5~10之间")
private String userCode;
如userCode为空,则抛出异常:用户代码不可为空
如userCode不为空,则校验约束:用户代码长度需在5~10之间
网友评论