前言
参数校验在项目中是必不可少的一环,对于一些常规的校验,可以通过注解来开启自动校验,减少重复的校验代码,简化开发。
JSR 303
JSR 303 是Bean Validation验证的规范 ,定义了如下的注解:
Hibernate Validator
Hibernate Validator 是该规范的参考实现,它除了实现规范要求的注解外,还额外添加了一些注解:
spring validation
spring validation对hibernate validation进行了二次封装,在springmvc模块中添加了自动校验,并将校验信息封装进了特定的类中。
以springboot工程为例,添加 spring-boot-starter-web 依赖,其自带了spring validation:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.2.4.RELEASEversion>
dependency>
一个典型springmvc接口如下:
package pers.skindream.controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pers.skindream.entity.SysUser;
@RestController
@RequestMapping("test")
public class TestController {
@PostMapping
public void doTest(@Validated SysUser user) {
}
}
实体类入参
在接口实体入参user前有标有@Validated注解,SysUser类中代码如下:
package pers.skindream.entity;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
/**
*
*使用是lombok进行代码简化,下同
*
*/
@Data
public class SysUser {
private String id;
@NotBlank(message = "请输入用户名")
private String username;
@NotBlank(message = "请输入密码")
private String password;
@Email(message = "邮箱格式不正确")
@NotBlank(message = "请输入邮箱地址")
private String email;
}
不带如何参数访问接口地址localhost:7001/test,会抛出异常。
org.springframework.validation.BindException
在springboot项目中可以定义一个全局异常处理器捕获并处理异常,自定义的全局异常处理器代码如下:
package pers.skindream.system.handler;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import pers.skindream.commons.result.AjaxResult;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = BindException.class)
public AjaxResult handBindException(BindException exception) {
String message = exception.getAllErrors().get(0).getDefaultMessage();
return AjaxResult.error(message);
}
}
其中AjaxResult为自定义的一个响应实体,代码如下:
package pers.skindream.commons.result;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class AjaxResult {
private int code;
private String msg;
private Object data;
public static AjaxResult success() {
return new AjaxResult(200, null, null);
}
public static AjaxResult error() {
return new AjaxResult(0, null, null);
}
public static AjaxResult error(String msg) {
return new AjaxResult(0, msg, null);
}
public static AjaxResult success(String msg, Object object) {
return new AjaxResult(200, msg, object);
}
}
再次访问接口地址localhost:7001/test,得到:
{
"code": 0,
"msg": "请输入密码",
"data": null
}
分组校验
对于一个实体,在不同的接口有不同的校验规则,这是可以对校验注解添加分组,groups属性可以接收一个到多个分组接口的class对象。
为上述SysUser类添加分组,代码如下:
package pers.skindream.entity;
import lombok.Data;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Data
public class SysUser {
private String id;
@NotBlank(message = "请输入用户名", groups = UnameAndPwd.class)
private String username;
@NotBlank(message = "请输入密码", groups = UnameAndPwd.class)
private String password;
@Email(message = "邮箱格式不正确", groups = CheckEmail.class)
@NotBlank(message = "请输入邮箱地址", groups = CheckEmail.class)
private String email;
public interface UnameAndPwd{}
public interface CheckEmail{}
}
其中username和password属于UnameAndPwd分组,email属于CheckEmail分组
修改上述接口,代码如下:
package pers.skindream.controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pers.skindream.entity.SysUser;
@RestController
@RequestMapping("test")
public class TestController {
@PostMapping
public void doTest(@Validated(SysUser.UnameAndPwd.class) SysUser user) {
}
}
这次指定了分组UnameAndPwd,只会去校验username和password了
访问接口地址localhost:7001/test
{
"code": 0,
"msg": "请输入用户名",
"data": null
}
改为指定CheckEmail分组
package pers.skindream.controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pers.skindream.entity.SysUser;
@RestController
@RequestMapping("test")
public class TestController {
@PostMapping
public void doTest(@Validated(SysUser.CheckEmail.class) SysUser user) {
}
}
访问接口地址localhost:7001/test
{
"code": 0,
"msg": "请输入邮箱地址",
"data": null
}
非实体类入参
校验非实体类,改造上述接口代码如下:
package pers.skindream.controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
@Validated
@RestController
@RequestMapping("test")
public class TestController {
@PostMapping
public void doTest(@NotBlank(message = "id不能为空") String id,
@NotBlank(message = "请输入用户名") String username) {
}
}
注意:方法所在类前需要添加注解@Validated才能生效
访问接口地址localhost:7001/test,抛出异常
javax.validation.ConstraintViolationException: doTest.id: id不能为空, doTest.username: 请输入用户名
在上述全局异常处理类中捕获并处理该异常,改造代码如下:
package pers.skindream.system.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import pers.skindream.commons.result.AjaxResult;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.stream.Collectors;
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = BindException.class)
public AjaxResult handBindException(BindException exception) {
String message = exception.getAllErrors().get(0).getDefaultMessage();
return AjaxResult.error(message);
}
@ExceptionHandler(value = ConstraintViolationException.class)
public AjaxResult handConstraintViolationException(ConstraintViolationException exception) {
String message = exception.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(","));
return AjaxResult.error(message);
}
}
访问接口地址localhost:7001/test
{
"code": 0,
"msg": "id不能为空,请输入用户名",
"data": null
}
这里会收集到所有错误消息,如果想让他校验到了错误就立即返回,需要设置为快速失败模式,新建
package pers.skindream.system.config;
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 ValidatorConfig {
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(true)
.buildValidatorFactory();
return validatorFactory.getValidator();
}
}
访问接口地址localhost:7001/test
{
"code": 0,
"msg": "id不能为空",
"data": null
}
自定义校验注解
当已有的校验注解不能满足业务需求时,可以自定义注解实现校验。
现在有需求校验字符串中不能含有空格,自定义注解代码如下:
package pers.skindream.system.validation.constraints;
import pers.skindream.system.validation.validator.NotSpacesValidator;
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义校验注解,校验字符串中是否含有空格
*/
@Constraint(
validatedBy = NotSpacesValidator.class
)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotSpaces {
String message() default "字符串不能含有空格";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
其中指定的校验器为NotSpacesValidator,代码如下:
package pers.skindream.system.validation.validator;
import pers.skindream.system.validation.constraints.NotSpaces;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class NotSpacesValidator implements ConstraintValidator<NotSpaces, String> {
@Override
public void initialize(NotSpaces constraintAnnotation) {
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
if (s.contains(" ")) {
return false;
}
return true;
}
}
改造接口代码如下:
package pers.skindream.controller;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pers.skindream.system.validation.constraints.NotSpaces;
@Validated
@RestController
@RequestMapping("test")
public class TestController {
@PostMapping
public void doTest(@NotSpaces(message = "密码中不能含有非法字符") String password) {
}
}
访问接口地址localhost:7001/test
{
"code": 0,
"msg": "密码中不能含有非法字符",
"data": null
}
手动校验
在上述快速失败中,已经定义了一个validator单例对象,现在改造接口代码如下:
package pers.skindream.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import pers.skindream.commons.result.AjaxResult;
import pers.skindream.entity.SysUser;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
Validator validator;
@PostMapping
public AjaxResult doTest() {
SysUser user = getUser();
Set> validate = validator.validate(user, SysUser.UnameAndPwd.class);
if (!CollectionUtils.isEmpty(validate)){
ConstraintViolation constraintViolation = (ConstraintViolation) validate.toArray()[0];
return AjaxResult.error(constraintViolation.getMessage());
}
return AjaxResult.success();
}
public SysUser getUser() {
SysUser user = new SysUser();
user.setUsername("");
user.setPassword("123456");
user.setEmail("[email protected]");
return user;
}
}
访问接口地址localhost:7001/test
{
"code": 0,
"msg": "请输入用户名",
"data": null
}
结语
以上是基于springboot项目接口入参的简单参数校验,对于复杂的入参校验还是需要基于代码实现。