前言
參數校驗在項目中是必不可少的一環,對於一些常規的校驗,可以通過注解來開啟自動校驗,減少重復的校驗代碼,簡化開發。
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.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.2.4.RELEASE</version>
</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<ConstraintViolation<SysUser>> 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("123456@163.com");
return user;
}
}
訪問接口地址localhost:7001/test
{
"code": 0,
"msg": "請輸入用戶名",
"data": null
}
結語
以上是基於springboot項目接口入參的簡單參數校驗,對於復雜的入參校驗還是需要基於代碼實現。
