Spring/Spring boot JSR-303驗證框架 之 hibernate-validator
JSR-303 與 hibernate-validator
Spring3支援JSR-303驗證框架,JSR-303 是Java EE 6 中的一項子規範,叫做BeanValidation,官方參考實現是hibernate-validator(與Hibernate ORM 沒有關係),JSR 303 用於對Java Bean 中的欄位的值進行驗證。
hibernate-validator實現了JSR-303規範,並擴充套件了一些註解,提供了一套比較完善、便捷的驗證實現方式。
常用驗證:
- @Null 限制只能為null
- @NotNull 限制必須不為null
- @AssertFalse 限制必須為false
- @AssertTrue 限制必須為true
- @DecimalMax(value) 限制必須為一個不大於指定值的數字
- @DecimalMin(value) 限制必須為一個不小於指定值的數字
- @Digits(integer,fraction) 限制必須為一個小數,且整數部分的位數不能超過integer,小數部分的位數不能超過fraction
- @Future 限制必須是一個將來的日期
- @Max(value) 限制必須為一個不大於指定值的數字
- @Min(value) 限制必須為一個不小於指定值的數字
- @Past 限制必須是一個過去的日期
- @Pattern(value) 限制必須符合指定的正則表示式
- @Size(max,min) 限制字元長度必須在min到max之間
- @Past 驗證註解的元素值(日期型別)比當前時間早
- @NotEmpty 驗證註解的元素值不為null且不為空(字串長度不為0、集合大小不為0)
- @NotBlank 驗證註解的元素值不為空(不為null、去除首位空格後長度為0),不同於@NotEmpty,@NotBlank只應用於字串且在比較時會去除字串的空格
- @Email 驗證註解的元素值是Email,也可以通過正則表示式和flag指定自定義的email格式
spring-boot-starter-web包裡面有hibernate-validator包,所以如果開發 web 就不需要重複新增 spring-boot-starter-validation 依賴了。但如果沒用 web 依賴時候想要實現 Bean 驗證,則只要單單加入 spring-boot-starter-validation 依賴即可。
spring-boot-starter-web依賴關係:
<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>
spring-boot-starter-validation依賴關係:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
</dependencies>
常用示例
User.java
@Data // Lombok註解,可以使我們不用再在程式碼裡手動加get、set、toString、equals和hashCode等方法
public class User {
@NotBlank(message = "使用者名稱不能為空")
private String name;
@NotBlank(message = "年齡不能為空")
@Range(min = 0, max = 120, message = "年齡只能從0-120歲")
private String age;
// 如果是空,則不校驗,如果不為空,則校驗
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "出生日期格式不正確")
private String birthday;
}
一、校驗bean(引數封裝成物件)
1、 POST介面 + @Valid + BindingResult 驗證示例:
@Controller
@RequestMapping("/user")
public class UserController {
/**
* 建立使用者
* @requestBody可以將請求體中的JSON字串繫結到相應的bean上
* BindingResult是驗證不通過的結果集合
*/
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
public String postUser(@RequestBody @Valid User user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
return "error";
}
return "success";
}
}
json格式請求引數:
輸出結果:
出生日期格式不正確
使用者名稱不能為空
年齡只能從0-120歲
2、GET介面 + @Valid + BindingResult 驗證示例:
@Controller
@RequestMapping("/user")
public class UserController {
/**
* 建立使用者
*/
@RequestMapping(value = "/create", method = RequestMethod.GET)
@ResponseBody
public String postUser(@RequestBody @Valid User user, BindingResult result) {
if (result.hasErrors()) {
for (ObjectError error : result.getAllErrors()) {
System.out.println(error.getDefaultMessage());
}
return "error";
}
return "success";
}
}
請求引數:
輸出結果:
使用者名稱不能為空
年齡只能從0-120歲
出生日期格式不正確
補充:可以加多個@Valid和BindingResult,如下:
public void test()(
@RequestBody @Valid Model demo1, BindingResult result,
@RequestBody @Valid Model demo2, BindingResult result2) {
}
3、 @Validated + 全域性捕獲異常處理(最佳實踐)
測試時:使用 @Valid + 全域性捕獲異常處理也可以,具體區別有待研究。
當使用了 @Validated + @RequestBody 註解但是沒有在繫結的資料物件後面跟上 Errors 型別的引數宣告的話,Spring MVC 框架會丟擲MethodArgumentNotValidException 異常。
/**
* 建立使用者
*/
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
public String postUser(@RequestBody @Validated User user) {
return "success";
}
全域性捕獲異常處理
@ControllerAdvice
@Component
public class GlobalExceptionHandler{
private static final Logger logger = LoggerFactory.getLogger(ExceptionhandlerController.class);
/**
* 處理所有不可知的異常
*/
@ExceptionHandler(Exception.class)
@ResponseBody
Result handleException(Exception e) {
logger.error(e.getMessage(), e);
return Result.error(CodeMsg.SERVER_ERROR);
}
......
/**
* 處理實體欄位校驗不通過異常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseBody
public Result validationError(MethodArgumentNotValidException ex) {
BindingResult result = ex.getBindingResult();
final List<FieldError> fieldErrors = result.getFieldErrors();
StringBuilder builder = new StringBuilder();
for (FieldError error : fieldErrors) {
builder.append( "\n" + error.getDefaultMessage());
}
logger.error(builder.toString());
return Result.error(CodeMsg.BIND_ERROR);
}
}
json格式請求引數:
執行結果:
2018-07-23 14:31:35.706 ERROR 5432 --- [nio-8080-exec-2] c.h.m.e.GlobalExceptionHandler :
使用者名稱不能為空
出生日期格式不正確
年齡只能從0-120歲
二、@RequestParam引數校驗(一般用在處理Get請求或引數比較少)
使用@Valid註解,對RequestParam對應的引數進行註解,是無效的,需要使用@Validated註解來使得驗證生效。
示例:
1、建立MethodValidationPostProcessor的Bean
@Configuration
@EnableAutoConfiguration
public class FactoryConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
2、方法所在的Controller上加註解@Validated
@Controller
@RequestMapping("/user")
@Validated
public class UserController {
@RequestMapping(value = "/get", method = RequestMethod.GET)
@ResponseBody
public String getUser(
/** 如果只有少數引數,直接把引數寫到Controller層,然後在Controller層進行驗證就可以了 */
@Min(value = 1, message = "id最小隻能1")
@Max(value = 99, message = "id最大隻能99")
@RequestParam(name = "userid", required = true) Integer userid) {
return userid+"";
}
}
請求引數:
http://localhost:8080/user/get?userid=10000
執行結果:
javax.validation.ConstraintViolationException: null
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:147) ~[spring-context-4.3.12.RELEASE.jar:4.3.12.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) ~[spring-aop-4.3.12.RELEASE.jar:4.3.12.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) ~[spring-aop-4.3.12.RELEASE.jar:4.3.12.RELEASE]
可以看到:驗證不通過時,丟擲了ConstraintViolationException異常,可以使用全域性捕獲異常處理。
@ControllerAdvice
@Component
public class GlobalExceptionHandler{
private static final Logger logger = LoggerFactory.getLogger(ExceptionhandlerController.class);
/**
* 處理所有不可知的異常
*/
@ExceptionHandler(Exception.class)
@ResponseBody
Result handleException(Exception e) {
logger.error(e.getMessage(), e);
return Result.error(CodeMsg.SERVER_ERROR);
}
......
/**
* 處理@RequestParam校驗不通過異常
*/
@ExceptionHandler(ConstraintViolationException.class)
@ResponseBody
public Result validationError(ConstraintViolationException ex) {
Set<ConstraintViolation<?>> violations = ex.getConstraintViolations();
StringBuilder builder = new StringBuilder();
for (ConstraintViolation<?> item : violations) {
builder.append( "\n" + item.getMessage());
}
logger.error(builder.toString());
return Result.error(CodeMsg.BIND_ERROR);
}
}
執行結果:
2018-07-23 14:32:31.376 ERROR 5432 --- [nio-8080-exec-4] c.h.m.e.GlobalExceptionHandler :
id最大隻能99
hibernate的校驗模式
1、普通模式(預設是這個模式)
普通模式(會校驗完所有的屬性,然後返回所有的驗證失敗資訊)
2、快速失敗返回模式
快速失敗返回模式(只要有一個驗證失敗,則返回)
配置方式:
fail_fast: true 快速失敗返回模式 false 普通模式
@Configuration
@EnableAutoConfiguration
public class ValidateConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor postProcessor = new MethodValidationPostProcessor();
// 設定validator模式為快速失敗返回模式
postProcessor.setValidator(validator());
return postProcessor;
}
@Bean
public Validator validator() {
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.addProperty("hibernate.validator.fail_fast", "true")
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator;
}
}
model校驗
@RequestMapping("/demo")
@ResponseBody
public String demo() {
User user = new User();
user.setName("hykk");
user.setAge("222");
user.setBirthday("112133");
Set<ConstraintViolation<User>> violationSet = validator.validate(user);
for (ConstraintViolation<User> model : violationSet) {
System.out.println("校驗:" + model.getMessage());
}
return "demo";
}
請求結果:
校驗:年齡只能從0-120歲
自定義驗證器
bean:
public class User {
......
@IsMobile(message = "手機號碼格式錯誤")
private String mobile;
......
}
介面:
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class })
public @interface IsMobile {
boolean required() default true;
String message() default "";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
校驗規則:
public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {
public static final String regex = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$";
private boolean required = false;
public void initialize(IsMobile constraintAnnotation) {
required = constraintAnnotation.required();
}
public boolean isValid(String value, ConstraintValidatorContext context) {
if (required) {
return isMobile(value);
} else {
if (StringUtils.isEmpty(value)) {
return true;
} else {
return isMobile(value);
}
}
}
private boolean isMobile(String phone) {
if (phone.length() != 11) {
return false;
} else {
Pattern p = Pattern.compile(regex);
Matcher m = p.matcher(phone);
return m.matches();
}
}
}
測試:
@RequestMapping("/demo")
@ResponseBody
public String demo() {
User user = new User();
user.setName("Tom");
user.setAge("22");
user.setMobile("123123123");
Set<ConstraintViolation<User>> violationSet = validator.validate(user);
for (ConstraintViolation<User> model : violationSet) {
System.out.println("校驗:" + model.getMessage());
}
return "demo";
}
執行結果:
校驗:手機號碼格式錯誤
分組校驗
分組
有這樣一種場景,新增使用者的時候,不需要驗證主鍵id(因為系統生成);修改的時候需要驗證主鍵id,這時候可以用到validator的分組驗證功能。
首先建立兩個分組介面:
// 代表 新增 分組
public interface AddGroup {
}
// 代表 更新 分組
public interface UpdateGroup {
}
Bean欄位指定需要驗證的分組
@Data
public class User {
@NotNull(message = "主鍵id不能為空", groups = {UpdateGroup.class})
private Long id;
@NotBlank(message = "使用者名稱不能為空", groups = {AddGroup.class, UpdateGroup.class})
private String name;
@NotBlank(message = "年齡不能為空", groups = {AddGroup.class, UpdateGroup.class})
@Range(min = 0, max = 120, message = "年齡只能從0-120歲", groups = {AddGroup.class, UpdateGroup.class})
private String age;
// 如果是空,則不校驗,如果不為空,則校驗
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "出生日期格式不正確", groups = {AddGroup.class, UpdateGroup.class})
private String birthday;
}
示例一:
/**
* 建立使用者
*/
@RequestMapping(value = "/create", method = RequestMethod.POST)
@ResponseBody
public String postUser(@RequestBody @Validated(AddGroup.class) User user) {
return "create success";
}
請求引數:
執行結果:
// 驗證通過
示例二:
/**
* 更新使用者
*/
@RequestMapping(value = "/update", method = RequestMethod.POST)
@ResponseBody
public String updateUser(@RequestBody @Validated(UpdateGroup.class) User user) {
return "update success";
}
請求引數:
執行結果:
2018-07-24 14:59:47.178 ERROR 13320 --- [nio-8080-exec-4] c.h.m.e.GlobalExceptionHandler :
主鍵id不能為空
組序列
除了按組指定是否驗證之外,還可以指定組的驗證順序,前面組驗證不通過的,後面組不進行驗證:
指定組的序列(Group1 > Group2 > Default)
public interface Group1 {
}
public interface Group2 {
}
public interface Group3 {
}
指定組序列
@GroupSequence({Group1.class, Group2.class, Group3.class, Default.class})
public interface GroupOrder {
}
校驗順序 id > age > name > birthday
@Data
public class User {
@NotNull(message = "主鍵id不能為空", groups = {Group1.class})
private Long id;
@NotBlank(message = "使用者名稱不能為空", groups = {Group3.class})
private String name;
@NotBlank(message = "年齡不能為空", groups = {Group2.class})
@Range(min = 0, max = 120, message = "年齡只能從0-120歲", groups = {Group2.class})
private String age;
// 如果是空,則不校驗,如果不為空,則校驗
@Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", message = "出生日期格式不正確")
private String birthday;
}
/**
* 更新使用者
*/
@RequestMapping(value = "/update", method = RequestMethod.POST)
@ResponseBody
public String updateUser(@RequestBody @Validated(GroupOrder.class) User user) {
return "update success";
}
請求引數:
{"id":"1","name":"","age":121,"birthday":"20000-11-120"}
執行結果:
2018-07-24 15:18:20.079 ERROR 12336 --- [nio-8080-exec-3] c.h.m.e.GlobalExceptionHandler :
年齡只能從0-120歲