何為Hibernate-Validator
在RESTful Web Services的接口服務中,會有各種各樣的入參,我們不可能完全不做任何校驗就直接進入到業務處理的環節,通常我們會有一個基礎的資料驗證的機制,待這些驗證過程完畢,結果無誤後,參數才會進入到正式的業務進行中。而資料驗證又分為兩種,一種是無業務關聯的規則性驗證,一種是根據現有資料進行的關聯性資料驗證(簡單來說,參數的合理性,需要查資料庫)。而Hibernate-Validator則适合做無業務關聯的規則性驗證。
Hibernate-Validator的相關依賴
如果項目的架構是spring boot的話,在spring-boot-starter-web 中已經包含了Hibernate-validator的依賴,我們點開spring-boot-starter-web的pom.xml則可以看到相關的依賴内容。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starters</artifactId>
<version>1.5.10.RELEASE</version>
</parent>
<artifactId>spring-boot-starter-web</artifactId>
<name>Spring Boot Web Starter</name>
<description>Starter for building web, including RESTful, applications using Spring
MVC. Uses Tomcat as the default embedded container</description>
<url>http://projects.spring.io/spring-boot/</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>http://www.spring.io</url>
</organization>
<properties>
<main.basedir>${basedir}/../..</main.basedir>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
</dependency>
</dependencies>
如果是其他的架構風格的話,引入如下的依賴就可以了。
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.10.Final</version>
</dependency>
初步使用Hibernate-Validator
以下代碼環境均在Spring boot 1.5.10的版本下運作。
Hibernate-Validator的主要使用的方式就是注解的形式,并且是“零配置”的,無需配置也可以使用。下面用一個最簡單的案例。
-
Hibernate-Validator 最基本的使用
1.添加一個普通的接口資訊,參數是@RequestParam類型的,傳入的參數是id,且id不能小于10。
@RestController
@RequestMapping("/example")
@Validated
public class ExampleController {
/**
* 用于測試
* @param id id數不能小于10 @RequestParam類型的參數需要在Controller上增加@Validated
* @return
*/
@RequestMapping(value = "/info",method = RequestMethod.GET)
public String test(@Min(value = 10, message = "id最小隻能是10") @RequestParam("id")
Integer id){
return "恭喜你拿到參數了";
}
}
2.在全局異常攔截中添加驗證異常的處理
@Slf4j
@ControllerAdvice
@Component
public class GlobalExceptionHandler {
@ExceptionHandler
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handle(ConstraintViolationException exception, HttpServletRequest request) {
Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
StringBuffer errorInfo = new StringBuffer();
for (ConstraintViolation<?> item : violations) {
/**列印驗證不通過的資訊*/
errorInfo.append(item.getMessage());
errorInfo.append(",");
}
log.error("{}接口參數驗證失敗,内容如下:{}",request.getRequestURI(),errorInfo.toString());
return "您的請求失敗,參數驗證失敗,失敗資訊如下:"+ errorInfo.toString();
}
}
3.一個簡單的測試。
- 驗證複雜參數的案例
1.添加一個vo的實體資訊。
/**
* 使用者的vo類
* @author dengyun
*/
@Data
public class ExampleVo {
@NotBlank(message = "使用者名不能為空")
private String userName;
@Range(min = 18,max = 60,message = "隻能填報年齡在18~60歲的")
private String age;
}
2.添加一個POST請求的接口。
/**
* 用于測試
* @param vo 按照vo的驗證
* @return
*/
@RequestMapping(value = "/info1",method = RequestMethod.POST)
public String test1(@Valid @RequestBody ExampleVo vo){
return "恭喜你拿到參數了";
}
3.在全局異常攔截中添加驗證處理的結果
@ResponseBody
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public String handle(MethodArgumentNotValidException exception,HttpServletRequest request) {
StringBuffer errorInfo=new StringBuffer();
List<ObjectError> errors = exception.getBindingResult().getAllErrors();
for(int i=0;i<errors.size();i++){
errorInfo.append(errors.get(i).getDefaultMessage()+",");
}
log.error("{},接口參數驗證失敗:{}",request,errorInfo.toString());
return "您的請求失敗,參數驗證失敗,失敗資訊如下:"+errorInfo.toString();
}
4.一個簡單的測試
我個人比較推薦使用全局異常攔截處理的方式去處理Hibernate-Validator的驗證失敗後的處理流程,這樣能能減少Controller層或Services層的代碼邏輯處理。雖然它也能在Controller中增加BindingResult的執行個體來擷取資料,但是并不推薦。
更加靈活的運用
首先列舉一下Hibernate-Validator所有的内置驗證注解。
@Null 被注釋的元素必須為 null@NotNull
被注釋的元素必須不為 null
@AssertTrue 被注釋的元素必須為 true
@AssertFalse被注釋的元素必須為 false
@Min(value) 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值
@Max(value) 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值
@DecimalMin(value) 被注釋的元素必須是一個數字,其值必須大于等于指定的最小值
@DecimalMax(value) 被注釋的元素必須是一個數字,其值必須小于等于指定的最大值
@Size(max=, min=) 被注釋的元素的大小必須在指定的範圍内
@Digits (integer, fraction) 被注釋的元素必須是一個數字,其值必須在可接受的範圍内
@Past 被注釋的元素必須是一個過去的日期
@Future 被注釋的元素必須是一個将來的日期
@Pattern(regex=,flag=) 被注釋的元素必須符合指定的正規表達式
Hibernate Validator 附加的 constraint
@NotBlank(message =) 驗證字元串非null,且長度必須大于0
@Email 被注釋的元素必須是電子郵箱位址
@Length(min=,max=) 被注釋的字元串的大小必須在指定的範圍内
@NotEmpty 被注釋的字元串的必須非空
@Range(min=,max=,message=) 被注釋的元素必須在合适的範圍内
這些注解能适應我們絕大多數的驗證場景,但是為了應對更多的可能性,我們需要增加注解功能配合Hibernate-Validator的其他的特性,來滿足驗證的需求。
1. 自定義注解
- 添加自定義注解
我們一定會用到這麼一個業務場景,vo中的屬性必須符合枚舉類中的枚舉。Hibernate-Validator中還沒有關于枚舉的驗證規則,那麼,我們則需要自定義一個枚舉的驗證注解。
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumCheckValidator.class)
public @interface EnumCheck {
/**
* 是否必填 預設是必填的
* @return
*/
boolean required() default true;
/**
* 驗證失敗的消息
* @return
*/
String message() default "枚舉的驗證失敗";
/**
* 分組的内容
* @return
*/
Class<?>[] groups() default {};
/**
* 錯誤驗證的級别
* @return
*/
Class<? extends Payload>[] payload() default {};
/**
* 枚舉的Class
* @return
*/
Class<? extends Enum<?>> enumClass();
/**
* 枚舉中的驗證方法
* @return
*/
String enumMethod() default "validation";
}
- 注解的業務邏輯實作類
public class EnumCheckValidator implements ConstraintValidator<EnumCheck,Object> {
private EnumCheck enumCheck;
@Override
public void initialize(EnumCheck enumCheck) {
this.enumCheck =enumCheck;
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext constraintValidatorContext) {
// 注解表明為必選項 則不允許為空,否則可以為空
if (value == null) {
return this.enumCheck.required()?false:true;
}
//最終的傳回結果
Boolean result=Boolean.FALSE;
// 擷取 參數的資料類型
Class<?> valueClass = value.getClass();
try {
Method method = this.enumCheck.enumClass().getMethod(this.enumCheck.enumMethod(), valueClass);
result = (Boolean)method.invoke(null, value);
result= result == null ? false : result;
//所有異常需要在開發測試階段發現完畢
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}finally {
return result;
}
}
}
- 編寫枚舉類
public enum Sex{
MAN("男",1),WOMAN("女",2);
private String label;
private Integer value;
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
public Integer getValue() {
return value;
}
public void setValue(Integer value) {
this.value = value;
}
Sex(String label, int value) {
this.label = label;
this.value = value;
}
/**
* 判斷值是否滿足枚舉中的value
* @param value
* @return
*/
public static boolean validation(Integer value){
for(Sex s:Sex.values()){
if(Objects.equals(s.getValue(),value)){
return true;
}
}
return false;
}
}
- 使用方式
@EnumCheck(message = "隻能選男:1或女:2",enumClass = Sex.class)
private Integer sex;
- 一個簡單的測試
我們甚至可以在自定義注解中做更加靈活的處理,甚至把與資料庫的資料校驗的也寫成自定義注解,來進行資料驗證的調用。
2. Hibernate-Validator的分組驗證
同一個校驗規則,不可能适用于所有的業務場景,對此,對每一個業務場景去編寫一個校驗規則,又顯得特别備援。這裡我們剛好可以用到Hibernate-Validator的分組功能。
- 添加一個名為ValidGroupA的接口(接口内容可以是空的,是以就不列舉代碼)
- 添加一個需要分組校驗的字段
@Data
public class ExampleVo {
@NotNull(message = "主鍵不允許為空",groups = ValidGroupA.class)
private Integer id;
@NotBlank(message = "使用者名不能為空",groups = Default.class)
private String userName;
@Range(min = 18,max = 60,message = "隻能填報年齡在18~60歲的",groups = Default.class)
private String age;
@EnumCheck(message = "隻能選男:1或女:2",enumClass = Sex.class,groups = Default.class)
private Integer sex;
}
- 改動接口的内容
@RequestMapping(value = "/info1",method = RequestMethod.POST)
public String test1(@Validated({ValidGroupA.class,Default.class}) @RequestBody ExampleVo vo){
return "恭喜你拿到參數了";
}
這裡我們可以注意一下,校驗的注解由 @Valid 改成了 @Validated。
- 進行測試,保留ValidGroupA.class和去掉ValidGroupA.class的測試。
- 保留ValidGroupA.class
- 去掉ValidGroupA.class
使用分組能極大的複用需要驗證的類資訊。而不是按業務重複編寫備援的類。然而Hibernate-Validator還提供組序列的形式進行順序式校驗,此處就不重複列舉了。我認為順序化的校驗,場景更多的是在業務處理類,例如關聯的屬性驗證,值的有效性很大程度上不能從代碼的枚舉或常量類中來校驗。