实际开发中,参数校验必不可少,因为用户的心思你永远无法洞察,他们会提交你根本无法想象的内容或者格式,如果前端后端都没做数据校验,那么恭喜你,你应该会收到很多垃圾数据,有些人甚至会提交一些恶意脚本,这样的话服务器就存在被攻击的风险。最好的方法就是把这些坏心思扼杀在萌芽之中,除了前端校验,后端校验也是重中之重。因为前端还是有风险的,比如浏览器端的js校验,我们就可以通过设置跳过这些js校验,相当于前端校验作废了,如果你服务器端没加校验的话,脏数据、垃圾数据还是会进来,所以,虽有前端校验还是不行,后端校验必须有。
1、依赖pom
从springboot-2.3
开始,校验包被独立成了一个starter
组件,所以需要引入validation和web,而springboot-2.3
之前的版本只需要引入 web 依赖就可以了。
org.springframework.boot
spring-boot-starter-validation
2.2.8.RELEASE
org.springframework.boot
spring-boot-starter-web
或者把spring-boot-starter-validation 替换为hibernate-validator
org.hibernate.validator
hibernate-validator
6.0.20.Final
2、单个类参数校验
定义要校验参数的实体类
@Data
public class UserInfo {
@ApiModelProperty(value = "id")
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(max = 3,message = "用户名不能超过3")
@ApiModelProperty(value = "用户名")
private String userName;
@NotBlank(message = "昵称不能为空")
@ApiModelProperty(value = "昵称")
private String nickName;//
@Email(message = "邮箱格式不正确")
@ApiModelProperty(value = "邮箱")
private String email;
}
内置校验注解:
注解 | 校验功能 |
---|---|
@AssertFalse | 必须是false |
@AssertTrue | 必须是true |
@DecimalMax | 小于等于给定的值 |
@DecimalMin | 大于等于给定的值 |
@Digits | 可设定最大整数位数和最大小数位数 |
校验是否符合Email格式 | |
@Future | 必须是将来的时间 |
@FutureOrPresent | 当前或将来时间 |
@Max | 最大值 |
@Min | 最小值 |
@Negative | 负数(不包括0) |
@NegativeOrZero | 负数或0 |
@NotBlank | 不为null并且包含至少一个非空白字符 |
@NotEmpty | 不为null并且不为空 |
@NotNull | 不为null |
@Null | 为null |
@Past | 必须是过去的时间 |
@PastOrPresent | 必须是过去的时间,包含现在 |
@PositiveOrZero | 正数或0 |
@Size | 校验容器的元素个数 |
定义UserInfoController进行测试
@ApiOperation(value = "添加用户")
@PostMapping("/addUserInfo")
public ResultInfo addUserInfo(@Validated UserInfo userInfo, BindingResult result) {
List fieldErrors = result.getFieldErrors();
if(!fieldErrors.isEmpty()){
//取出所有校验不通过的信息
List collect = fieldErrors.stream().map(s->s.getDefaultMessage()).collect(Collectors.toList());
return ResultInfo.success(HttpStatus.BAD_REQUEST.value(),"字段校验不通过",collect);
}
return ResultInfo.success(200,"成功");
}
测试效果如下
3、全局异常处理
每个Controller
方法中如果都写一遍BindingResult
信息的处理还是很繁的。当我们写了@validated
注解,不写BindingResult
的时候,Spring 就会抛出异常。因此,我们可以通过全局异常处理的方式统一处理校验异常,从而免去重复编写异常信息的代码。全局异常处理类只需要在类上标注@RestControllerAdvice
,并在处理相应异常的方法上使用@ExceptionHandler
注解,写明处理哪个异常即可。
全局异常处理类 GlobalExceptionHandler
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final String BAD_REQUEST_MSG = "参数检验不通过";
//处理 form data方式调用接口校验失败抛出的异常
@ExceptionHandler(BindException.class)
public ResultInfo bindExceptionHandler(BindException e) {
List fieldErrors = e.getBindingResult().getFieldErrors();
List collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
}
// 处理 json 请求体调用接口校验失败抛出的异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResultInfo methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
List fieldErrors = e.getBindingResult().getFieldErrors();
List collect = fieldErrors.stream().map(o -> o.getDefaultMessage()).collect(Collectors.toList());
return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
}
// 处理单个参数校验失败抛出的异常
@ExceptionHandler(ConstraintViolationException.class)
public ResultInfo constraintViolationExceptionHandler(ConstraintViolationException e) {
Set> constraintViolations = e.getConstraintViolations();
List collect = constraintViolations.stream().map(o -> o.getMessage()).collect(Collectors.toList());
return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, collect);
}
// 处理以上处理不了的其他异常
@ExceptionHandler(Exception.class)
public ResultInfo exceptionHandler(Exception e) {
return new ResultInfo().success(HttpStatus.BAD_REQUEST.value(), BAD_REQUEST_MSG, e.getMessage());
}
}
4、测试
测试一:使用form data方式调用接口,校验异常抛出 BindException
@ApiOperation(value = "添加用户2")
@PostMapping("/addUserInfo2")
public ResultInfo addUserInfo2(@Validated UserInfo userInfo) {
return ResultInfo.success(HttpStatus.OK.value(),"成功",userInfo);
}
测试二:使用 json 请求体调用接口,校验异常抛出 MethodArgumentNotValidException
@ApiOperation(value = "添加用户3")
@PostMapping("/addUserInfo3")
public ResultInfo addUserInfo3(@RequestBody @Validated UserInfo userInfo) {
return ResultInfo.success(HttpStatus.OK.value(),"成功",userInfo);
}
测试三:单个参数校验异常抛出ConstraintViolationException
注意:单个参数校验需要在当前所在类的类名上加注解:@Validated
@ApiOperation(value = "打招呼-Hello")
@GetMapping("/hello")
public ResponseEntity hello(@RequestParam(value = "name",required = false) @NotBlank(message = "name不能为空") String name){
return ResponseEntity.ok("Hello:"+name);
}
5、自定义注解
虽然Spring Validation 提供的注解基本上够用,但是面对复杂的定义,我们还是需要自己定义相关注解来实现自动校验。正好,Spring 这个万能的框架就提供了这种扩展。
自定义注解类 Phone11 校验11位手机号是否正确
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {Phone11Validator.class})// 标明由哪个类执行校验逻辑
@NotBlank(message = "电话不能为空")//有现成的轮子拿过来用啊
public @interface Phone11 {
boolean required() default true;
String message() default "11位手机格式不正确";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
逻辑校验类:Phone11Validator
public class Phone11Validator implements ConstraintValidator {
//校验手机号正则
public static final String REGEX_MOBILE = "^((13[0-9])|(15[^4,\\D])|(17[0-9])|(18[0-9]))\\d{8}$";
@Override
public boolean isValid(String mobile, ConstraintValidatorContext constraintValidatorContext) {
if (isMobile(mobile)){
return true;//校验通过
}else {
return false;//校验未通过
}
}
/**
* 校验手机号
* @param mobile
* @return 校验通过返回true,否则返回false
*/
public static boolean isMobile(String mobile) {
return Pattern.matches(REGEX_MOBILE, mobile);
}
}
接着再实体里添加字段phone
//校验11位手机号格式是否正确
@Phone11
private String phone;
继续调用addUserInfo3进行测试,测试结果如下:
测试结果中发现电话有两个提示,一个不能为空,一个格式不对,不能为空那个就是因为在自定义注解的时候加了@NotNull注解,而另一个就是我们自己定义的提示信息。
6、递归校验
有时候我们的实体不是单纯的自己一个,而是TA里边有可能包含了另一个实体类,比如常见的一对一或者一对多关系,遇到这种情况,我们不但要校验本类自己的属性,而且包含的另一个实体类也需要校验,就会用到递归校验。很简单,我们只需要再包含的另一个类的上边加上注解 @Valid 即可实现,假设我们还有个部门实体Department,用户实体UserInfo包含部门实体,如下:
@Data
public class UserInfo {
@ApiModelProperty(value = "id")
private Long id;
...省略其他代码...
@Valid
private Depatement depatement;
}
department 如下
@Data
@ApiModel(value = "department",description = "用户部门表")
public class Department {
@ApiModelProperty(value = "id")
private Long id;
@NotBlank(message = "部门名不能为空")
@ApiModelProperty(value = "部门名")
private String deptName;
}
调用addUserInfo3进行测试,测试结果如下
7、快速失败返回
现在有个问题:就是有很多个字段需要校验,目前的情况是所有没通过校验的都会提示出来,对我们来说,只要有一个校验不通过,那么这次请求就是失败的,为啥还要花那时间全部检测出来呢。因此我们可以改善一下,快速失败,只要有一个字段不符合,就返回给用户提示,其他的也就不用再花时间去校验了。
新建配置类:ValidatorConfiguration,别忘了加注解@Configuration
@Configuration
public class ValidatorConfiguration {
/**
* JSR和Hibernate validator的校验只能对Object的属性进行校验
* 不能对单个的参数进行校验
* spring 在此基础上进行了扩展
* 添加了MethodValidationPostProcessor拦截器
* 可以实现对方法参数的校验
*
* @return
*/
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(validator());
return processor;
}
@Bean
public static Validator validator() {
return Validation
.byProvider(HibernateValidator.class)
.configure()
//快速返回模式,有一个验证失败立即返回错误信息
.failFast(true)
.buildValidatorFactory()
.getValidator();
}
}
然后我们再次测试,继续调用addUserInfo3,会发现每次都只返回一个错误信息。
代码地址:https://github.com/ComeFromChina/SpringBootDemo/tree/master/springboot-swagger-knife4j
有问题欢迎加群探讨:700637673