使用Hibernate-Validator優雅的校驗參數


何為Hibernate-Validator

       在RESTful 的接口服務中,會有各種各樣的入參,我們不可能完全不做任何校驗就直接進入到業務處理的環節,通常我們會有一個基礎的數據驗證的機制,待這些驗證過程完畢,結果無誤后,參數才會進入到正式的業務處理中。而數據驗證又分為兩種,一種是無業務關聯的規則性驗證,一種是根據現有數據進行的聯動性數據驗證(簡單來說,參數的合理性,需要查數據庫)。而Hibernate-Validator則適合做無業務關聯的規則性驗證,而這類驗證的代碼大多是可復用的。

JSR 303和JSR 349
       簡單來說,就是Java規定了一套關於驗證器的接口。開始的版本是Bean Validation 1.0(JSR-303),然后是Bean Validation 1.1(JSR-349),目前最新版本是Bean Validation 2.0(JSR-380),大概是這樣。

       從上可以看出Bean Validation並不是一項技術而是一種規范,需要對其實現。hibernate團隊提供了參考實現,Hibernate validator 5是Bean Validation 1.1的實現,Hibernate Validator 6.0是Bean Validation 2.0規范的參考實現。新特性可以到官網查看,筆者最喜歡的兩個特性是:跨參數驗證(比如密碼和確認密碼的驗證)和在消息中使用EL表達式,其他的還有如方法參數/返回值驗證、CDI和依賴注入、分組轉換等。對於方法參數/返回值驗證,大家可以參閱《hibernate官方文檔)

       如果項目的框架是spring boot的話,在依賴spring-boot-starter-web 中已經包含了Hibernate-validator的依賴。Hibernate-Validator的主要使用的方式就是注解的形式,並且是“零配置”的,無需配置也可以使用。下面展示一個最簡單的案例。

1. 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 "恭喜你拿到參數了";
        }
    }
  1. 在全局異常攔截中添加驗證異常的處理
/** * 統一異常處理類 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException exception) {
        StringBuilder errorInfo = new StringBuilder();
        BindingResult bindingResult = exception.getBindingResult();
        for(int i = 0; i < bindingResult.getFieldErrors().size(); i++){
            if(i > 0){
                errorInfo.append(",");
            }
            FieldError fieldError = bindingResult.getFieldErrors().get(i);
            errorInfo.append(fieldError.getField()).append(" :").append(fieldError.getDefaultMessage());
        }

        //返回BaseResponse
        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg(errorInfo.toString());
        response.setCode(DefaultErrorCode.error);
        return response;
    }


    @ExceptionHandler(ConstraintViolationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleConstraintViolationException(ConstraintViolationException exception) {
        StringBuilder errorInfo = new StringBuilder();
        String errorMessage ;

        Set<ConstraintViolation<?>> violations = exception.getConstraintViolations();
        for (ConstraintViolation<?> item : violations) {
            errorInfo.append(item.getMessage()).append(",");
        }
        errorMessage = errorInfo.toString().substring(0, errorInfo.toString().length()-1);

        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg(errorMessage);
        response.setCode(DefaultErrorCode.error);
        return response;
    }



    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public BaseResponse<String> handleDefaultException(Exception exception) {

        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg("其他錯誤");
        response.setCode(DefaultErrorCode.error);
        return response;
    }
}

2.內置的校驗注解

首先列舉一下Hibernate-Validator所有的內置驗證注解。

  • 常用的
注解 使用
@NotNull 被注釋的元素(任何元素)必須不為 null, 集合為空也是可以的。沒啥實際意義
@NotEmpty 用來校驗字符串、集合、map、數組不能為null或空
(字符串傳入空格也不可以)(集合需至少包含一個元素)
@NotBlank 只用來校驗字符串不能為null,空格也是被允許的 。校驗字符串推薦使用@NotEmpty
-
@Size(max=, min=) 指定的字符串、集合、map、數組長度必須在指定的max和min內
允許元素為null,字符串允許為空格
@Length(min=,max=) 只用來校驗字符串,長度必須在指定的max和min內 允許元素為null
@Range(min=,max=) 用來校驗數字或字符串的大小必須在指定的min和max內
字符串會轉成數字進行比較,如果不是數字校驗不通過
允許元素為null
@Min() 校驗數字(包括integer short long int 等)的最小值,不支持小數即double和float
允許元素為null
@Max() 校驗數字(包括integer short long int 等)的最小值,不支持小數即double和float
允許元素為null
-
@Pattern() 正則表達式匹配,可用來校驗年月日格式,是否包含特殊字符(regexp = "^[a-zA-Z0-9\u4e00-\u9fa5

除了@Empty要求字符串不能全是空格,其他的字符串校驗都是允許空格的。
message是可以引用常量的,但是如@Size里max不允許引用對象常量,基本類型常量是可以的。
注意大部分規則校驗都是允許參數為null,即當不存在這個值時,就不進行校驗了

  • 不常用的
@Null 被注釋的元素必須為 null
@AssertTrue 被注釋的元素必須為 true
@AssertFalse 被注釋的元素必須為 false
@DecimalMin(value) 被注釋的元素必須是一個數字,其值必須大於等於指定的最小值
@DecimalMax(value) 被注釋的元素必須是一個數字,其值必須小於等於指定的最大值
@Digits (integer, fraction) 被注釋的元素必須是一個數字,其值必須在可接受的范圍內
@Past 被注釋的元素必須是一個過去的日期
@Future 被注釋的元素必須是一個將來的日期
@Email 被注釋的元素必須是電子郵箱地址

3. 分組校驗、順序校驗、級聯校驗

       同一個校驗規則,不可能適用於所有的業務場景,對每一個業務場景去編寫一個校驗規則,又顯得特別冗余。實際上我們可以用到Hibernate-Validator的分組功能。

添加一個名為Save 、Update 的接口(接口內容是空的)

public @interface Save {
}

public @interface Update {
}

需要分組校驗的DTO

  /** * 注解@GroupSequence指定分組校驗的順序,即先校驗Save分組的,如果不通過就不會去做后面分組的校驗了 */
@Data
@ApiModel("用戶添加修改對象")
@GroupSequence({Save.class, Update.class, UserDto.class})
public class UserDto {

    @NotEmpty(message = DefaultErrorCode.ARGUMENTS_MISSING, groups = Update.class)
    @ApiModelProperty(notes = "用戶id", example = "2441634686")
    private String id;

    @NotEmpty(message = DefaultErrorCode.ARGUMENTS_MISSING, groups = Save.class)
    @Size(min = 1, max = RestfulConstants.NAME_MAX_LENGTH, message = CountGroupErrorCode.USER_NAME_IS_ILLEGAL)
    @Pattern(regexp = ValidatorConstant.LEGAL_CHARACTER, message = CountGroupErrorCode.USER_NAME_IS_ILLEGAL)
    @ApiModelProperty(notes = "用戶姓名", example = "張飛")
    private String name;

    @NotNull(message = DefaultErrorCode.ARGUMENTS_MISSING)
    @Min(value = 0, message = DefaultErrorCode.ARGUMENTS_MISSING, groups = Save.class)
    @ApiModelProperty(notes = "年齡", example = "12")
    private Integer age;

    @ApiModelProperty(notes = "手機號", example = "18108195635")
    @Pattern(regexp = ValidatorConstant.MOBILE)
    private String phone;

    @ApiModelProperty(notes = "出生日期,格式如2018-08-08", example = "2018-08-08")
    private LocalDate birthday;

    @EnumCheck(enumClass = SexEnum.class, message = CountGroupErrorCode.USER_SEX_ILLEGAL)
    @ApiModelProperty(notes = "性別,1-男,2-女,3-未知", example = "2")
    private Integer sex;

    /** * 級聯校驗只需要添加@Valid * 注解@ConvertGroup用於分組的轉換,只能和@Valid一起使用。(一般用不到) */
    @Size(max = RestfulConstants.DIRECTION_MAX_NUMBER, message = CountGroupErrorCode.DIRECTION_NUMBER_IS_ILLEGAL)
    @ApiModelProperty(notes = "包含的方向列表")
    @Valid
    //@ConvertGroup(from = Search.class, to = Update.class)
    private List<DirectionDto> list;

}

校驗的接口

	/** * 這里的@Validated({Save.class, Default.class}) 其中Default.class是校驗注解默認的分組, * (也就說明自定義校驗注解時要加上) */
    @PostMapping(value = "/add")
    @ApiOperation(value = "添加用戶")
    public BaseResponse addUser(@Validated({Save.class, Default.class}) @RequestBody UserDto addDto) {
        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg("添加成功");
        return response;
    }

    @PostMapping(value = "/update")
    @ApiOperation(value = "修改用戶")
    public BaseResponse updatedUser(@Validated({Update.class, Default.class}) @RequestBody UserDto updateDto) {
        BaseResponse<String> response = new BaseResponse<>();
        response.setMsg("修改成功");
        return response;
    }

       使用分組能極大的復用需要驗證的類信息。而不是按業務重復編寫冗余的類。其中@GroupSequence提供組序列的形式進行順序式校驗,即先校驗@Save分組的,如果校驗不通過就不進行后續的校驗多了。我認為順序化的校驗,場景更多的是在業務處理類,例如聯動的屬性驗證,值的有效性很大程度上不能從代碼的枚舉或常量類中來校驗。

4. 自定義校驗注解(枚舉)、組合校驗注解

       上面這些注解能適應我們絕大多數的驗證場景,但是為了應對更多的可能性,我們可以自定義注解來滿足驗證的需求。

       我們一定會用到這么一個業務場景,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 static final Logger logger = LoggerFactory.getLogger(EnumCheckValidator.class);

    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();
        }

        Boolean result = Boolean.FALSE;
        Class<?> valueClass = value.getClass();
        try {
            //通過反射執行枚舉類中validation方法
            Method method = this.enumCheck.enumClass().getMethod(this.enumCheck.enumMethod(), valueClass);
            result = (Boolean)method.invoke(null, value);
            if(result == null){
                return false;
            }
        } catch (Exception e) {
            logger.error("custom EnumCheckValidator error", e);
        }
        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;

我們甚至可以在自定義注解中做更加靈活的處理,甚至把與數據庫的數據校驗的也寫成自定義注解,來進行數據驗證的調用。

也可以使用組合校驗注解。

5. validate如何集成進springboot的國際化處理

       項目開發中,validate返回的錯誤信息我們也想和springboot處理國際化一樣拋出國際化處理過的內容,但是validator和messages的消息配置文件不一致,且處理器看起來也是不一樣的,那么接下來分析一下他們的不同和聯系。

       首先看validator,其中有一個處理消息的概念:interpolator,即“篡改”,也就是對於消息可進行最終輸出前的處理,如占位符替換等,否則將直接輸出配置的內容。需要具體的配置加載:org.hibernate.validator.spi.resourceloading.ResourceBundleLocator,作用就是根據國際化環境(en或zh等)加載相應的資源加載器,具體有以下幾個:
org.hibernate.validator.resourceloading.AggregateResourceBundleLocator
org.hibernate.validator.resourceloading.CachingResourceBundleLocator
org.hibernate.validator.resourceloading.DelegatingResourceBundleLocator
org.hibernate.validator.resourceloading.PlatformResourceBundleLocator
org.springframework.validation.beanvalidation.MessageSourceResourceBundleLocator
       以上幾個類(非加粗)實現是基於裝飾者模式的,基礎的是 PlatformResourceBundleLocator,原理通過name去classpath路徑加載對應環境的資源文件,然后從內容里獲取key的值,另外幾個從名字上也可以看出加強了什么:Aggregate聚合,cache緩存,delegate代理。
可以看到,其實最終起作用的是:java.util.ResourceBundle,即本地資源文件。再看加粗類,它是基於messages中MessageSource的,而這正是和message聯系的橋梁。

       然后我們來看專門處理國際化消息的org.springframework.context.MessageSource,它下邊的實現有很多,基本是各種不同來源,有基於本地內存的(map)、java.util.ResourceBundle等,與上面validate發生聯系的就是基於java.util.ResourceBundle的org.springframework.context.support.ResourceBundleMessageSource,這也是我們經常使用的。

so,到此,兩者可以聯系起來了,但是兩者的默認資源名稱是不同的。validator是ValidationMessages,message是messages。
所以,messages可以是validator的某個實現,也就是他倆是有交集的,當且僅當均使用同一個MessageSource,validator使用MessageSourceResourceBundleLocator時,可以通過messageSource進行統一控制。好了,那這樣的話,我們統一也就很簡單了。

最后,配置起來其實很簡單(下文的前提是已經做好國際化的配置使用):

	@Autowired
    private MessageSource messageSource;
    
    @Bean
    public Validator validator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        factoryBean.setMessageInterpolator(new MessageInterpolatorFactory().getObject());
        factoryBean.setValidationMessageSource(messageSource);
        return factoryBean;
    }

以上即可把validate的錯誤信息加入到國際化的處理中,其中messageSource使用的是默認的,也可以使用自定義的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM