使用Hibernate-Validator優雅的驗證RESTful Web Services的引數

Cross__發表於2018-08-13

何為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.一個簡單的測試。

驗證失敗的案例.png

  • 驗證複雜引數的案例

    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.一個簡單的測試

複雜引數校驗失敗.png

我個人比較推薦使用全域性異常攔截處理的方式去處理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;
複製程式碼
  • 一個簡單的測試

自定義註解的驗證.png

我們甚至可以在自定義註解中做更加靈活的處理,甚至把與資料庫的資料校驗的也寫成自定義註解,來進行資料驗證的呼叫。

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

      保留分組的測試.png

    • 去掉ValidGroupA.class

      去掉分組後的測試.png

使用分組能極大的複用需要驗證的類資訊。而不是按業務重複編寫冗餘的類。然而Hibernate-Validator還提供組序列的形式進行順序式校驗,此處就不重複列舉了。我認為順序化的校驗,場景更多的是在業務處理類,例如聯動的屬性驗證,值的有效性很大程度上不能從程式碼的列舉或常量類中來校驗。

部分引用及參考的文章

相關文章