文章目錄
何為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 最基本的使用
- 添加一個普通的接口信息,參數是@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 "恭喜你拿到參數了";
}
}
- 在全局異常攔截中添加驗證異常的處理
/** * 統一異常處理類 */
@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使用的是默認的,也可以使用自定義的。