在設計接口時我們通常需要對接口中的非法參數做校驗,以降低在程序運行時因為一些非法參數而導致程序發生異常的風險,例如登錄的時候需要校驗用戶名密碼是否為空,創建用戶的時候需要校驗郵件、手機號碼格式是否準確。如果在代碼中對接口參數一個個硬編碼校驗的話就太繁瑣了,代碼可讀性極差。這時不妨試試Validator框架。
原生的依賴是validation-api而hibernate-validator是對validation-api的增強,hibernate-validator框架已經集成在spring-boot-starter-web中。
<dependency><groupId>org.hibernate</groupId><artifactId>hibernate-validator</artifactId><version>5.2.0.Final</version>
</dependency>
但從springboot-2.3開始,校驗包被獨立成了一個starter組件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入web依賴就可以了。?
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId>
</dependency>
部分常見注解如下:
注解 | 功能 |
@AssertFalse | 可以為null,如果不為null的話必須為false |
@AssertTrue | 可以為null,如果不為null的話必須為true |
@DecimalMax | 設置不能超過最大值 |
@DecimalMin | 設置不能超過最小值 |
@Digits | 設置必須是數字且數字整數的位數和小數的位數必須在指定范圍內 |
@Future | 日期必須在當前日期的未來 |
@Past | 日期必須在當前日期的過去 |
@Max | 最大不得超過此最大值 |
@Min | 最大不得小于此最小值 |
@NotNull | 不能為null,可以是空 |
@Null | 必須為null |
@Pattern | 必須滿足指定的正則表達式 |
@Size | 集合、數組、map等的size()值必須在指定范圍內 |
必須是email格式 | |
@Length | 長度必須在指定范圍內 |
@NotBlank | 字符串不能為null,字符串trim()后也不能等于“” |
@NotEmpty | 不能為null,集合、數組、map等size()不能為0;字符串trim()后可以等于“” |
@Range | 值必須在指定范圍內 |
@URL | 必須是一個URL |
一、基礎注解使用?
注解簡單演示類
import lombok.Data;
import org.hibernate.validator.constraints.Length;import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;@Data
public class UserVo {@Length(min = 4, max = 12, message = "賬號長度必須位于4到10之間")private String userCode;@NotBlank(message = "用戶名不能為空")private String userName;@Email(message = "郵箱格式有誤")private String email;@Pattern(regexp = "^1[3456789]\\d{9}$", message = "手機號格式錯誤")private String phone;/*** 0:普通用戶;VIP:會員用戶;2:超級VIP用戶*/@Pattern(regexp = "[012]", message = "用戶類型有誤,請輸入0:普通用戶;VIP:會員用戶;2:超級VIP用戶")private String type;
}
在全局異常處理里面加上對參數校驗異常的攔截
@ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})public ResponseVo validatedExceptionHandler(Exception e) {log.error("參數校驗異常!原因是{}", e);if (e instanceof MethodArgumentNotValidException) {//BeanValidation exceptionMethodArgumentNotValidException ex = (MethodArgumentNotValidException) e;return ResponseVo.error(ResponseEnum.BODY_NOT_MATCH.getResultCode(),ex.getBindingResult().getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining("; ")));} else if (e instanceof ConstraintViolationException) {//BeanValidation GET simple paramConstraintViolationException ex = (ConstraintViolationException) e;return ResponseVo.error(ResponseEnum.BODY_NOT_MATCH.getResultCode(),ex.getConstraintViolations().stream().map(ConstraintViolation::getMessage).collect(Collectors.joining("; ")));} else if (e instanceof BindException) {//BeanValidation GET object paramBindException ex = (BindException) e;return ResponseVo.error(ResponseEnum.BODY_NOT_MATCH.getResultCode(),ex.getAllErrors().stream().map(ObjectError::getDefaultMessage).collect(Collectors.joining("; ")));}return ResponseVo.error(ResponseEnum.BODY_NOT_MATCH.getResultCode(), ResponseEnum.BODY_NOT_MATCH.getResultMsg());}
?定義參數校驗的controller
import com.yx.light.element.mybatis.vo.UserVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import javax.validation.constraints.Email;@RestController
@RequestMapping(value = "/valid")
@Validated
@Slf4j
public class ValidController {/*** 單參數校驗,單參數需要在controller上加上@Validated配合使用* @param email* @return*/@PostMapping(value = "/oneParam", produces = "application/json; charset=UTF-8")public String oneParam(@Email(message = "郵箱格式有誤") String email) {log.info("校驗郵箱為:{}", email);return "郵箱校驗成功";}/*** body類型校驗* @param vo* @return*/@PostMapping(value = "/bodyParam", produces = "application/json; charset=UTF-8")public String bodyParam(@Validated @RequestBody UserVo vo) {log.info("校驗body為:{}", vo);return "body校驗成功";}/*** 表單參數校驗* @param vo* @return*/@PostMapping(value = "/formParam", produces = "application/json; charset=UTF-8")public String formParam(@Validated UserVo vo) {log.info("校驗表單為:{}", vo);return "表單校驗成功";}
}
oneParam接口
bodyParam接口
formParam接口
?二、自定義參數校驗
當然你也可以自定義參數校驗,出現有些復雜的邏輯基本注解是沒辦法兼容的,需要我們自己去實現自動校驗。
自定義校驗注解
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;@Documented
@Retention(RUNTIME)
@Target({FIELD, METHOD, PARAMETER, TYPE})
@Constraint(validatedBy = UserValidation.UniqueUserValidator.class)
public @interface UniqueUser {String message() default "用戶編碼、手機號碼、郵箱不允許與現存用戶重復";Class<?>[] groups() default {};Class<? extends Payload>[] payload() default {};
}
?想讓自定義驗證注解生效,需要實現ConstraintValidator接口。接口的第一個參數是 自定義注解類型,第二個參數是 被注解字段的類。
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.annotation.Annotation;
import java.util.function.Predicate;@Slf4j
public class UserValidation<T extends Annotation> implements ConstraintValidator<T, UserVo> {protected Predicate<UserVo> predicate = c -> true;@Overridepublic boolean isValid(UserVo user, ConstraintValidatorContext constraintValidatorContext) {return predicate.test(user);}/*** 校驗用戶是否唯一* 即判斷數據庫是否存在當前新用戶的信息,如用戶編碼,手機,郵箱*/public static class UniqueUserValidator extends UserValidation<UniqueUser> {@Overridepublic void initialize(UniqueUser uniqueUser) {predicate = c -> !existsUser(c.getUserCode(), c.getEmail(), c.getPhone());}private boolean existsUser(String userCode, String email, String phone) {//TODO 寫一個數據庫查詢判斷是否存在相同的用戶,省略...return true;}}
}
在controller中添加測試接口
@PostMapping(value = "/addUser", produces = "application/json; charset=UTF-8")
public String addUser(@UniqueUser @Validated @RequestBody UserVo vo) {log.info("校驗body為:{}", vo);return "新增成功";
}
addUser接口?
?三、分組校驗
如果你的一個實體中的字段某一些是新增的時候必傳,某一些修改時又不用傳,那么對于不用傳的字段肯定不需要校驗的,這時候如果我們共用一個實體作為多個接口參數那肯定存在兼容問題,此時你就可以考慮將參數分組判斷。
定義一個分組接口ValidGroup讓其繼承javax.validation.groups.Default,再在分組接口中定義出多個不同的操作類型,Create,Update,Query,Delete。
import javax.validation.groups.Default;public interface ValidGroup extends Default {interface Crud extends ValidGroup {interface Create extends Crud {}interface Update extends Crud {}interface Query extends Crud {}interface Delete extends Crud {}}
}
稍微修改一下原來的vo,給他們加上分組參數groups
import com.yx.light.element.mybatis.validation.ValidGroup;
import lombok.Data;
import org.hibernate.validator.constraints.Length;import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;@Data
public class UserVo {@Length(min = 4, max = 12, message = "賬號長度必須位于4到10之間", groups = ValidGroup.Crud.Update.class)private String userCode;@NotBlank(message = "用戶名不能為空", groups = ValidGroup.Crud.Create.class)private String userName;@Email(message = "郵箱格式有誤")private String email;@Pattern(regexp = "^1[3456789]\\d{9}$", message = "手機號格式錯誤")private String phone;/*** 0:普通用戶;VIP:會員用戶;2:超級VIP用戶*/@Pattern(regexp = "[012]", message = "用戶類型有誤,請輸入0:普通用戶;VIP:會員用戶;2:超級VIP用戶")private String type;
}
在controller中添加如下方法,這里我們通過value屬性給addUserV2()和updateUser()方法分別指定Create和Update分組。
@PostMapping(value = "/addUserV2", produces = "application/json; charset=UTF-8")
public String addUserV2(@Validated(value = ValidGroup.Crud.Create.class) @RequestBody UserVo vo) {log.info("校驗body為:{}", vo);return "新增成功";
}@PostMapping(value = "/updateUser", produces = "application/json; charset=UTF-8")
public String updateUser(@Validated(value = ValidGroup.Crud.Update.class) @RequestBody UserVo vo) {log.info("校驗body為:{}", vo);return "更新成功";
}
addUserV2接口
updateUser接口