參數校驗
在開發中常常須要寫一些字段校驗的代碼,好比字段非空,字段長度限制,郵箱格式驗證等等,寫這些與業務邏輯關係不大的代碼我的感受有兩個麻煩:java
驗證代碼繁瑣,重複勞動
方法內代碼顯得冗長
每次要看哪些參數驗證是否完整,須要去翻閱驗證邏輯代碼
你看這樣?我感受不行 ~有啥好辦法不git
public String test1(String name) {
if (name == null) {
throw new NullPointerException("name 不能爲空");
}
if (name.length() < 2 || name.length() > 10) {
throw new RuntimeException("name 長度必須在 2 - 10 之間");
}
return "success";
}
1
2
3
4
5
6
7
8
9
使用hibernate-validator
spring-boot-starter-web包裏面有hibernate-validator包,不須要引用hibernate validator依賴。在 pom.xml 中添加上 spring-boot-starter-web 的依賴便可web
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
1
2
3
4
建立以下實體正則表達式
@Data
public class Book {
private Integer id;
@NotBlank(message = "name 不容許爲空")
@Length(min = 2, max = 10, message = "name 長度必須在 {min} - {max} 之間")
private String name;
}
1
2
3
4
5
6
7
實體校驗
而後呢在 controller 中這樣寫便可驗證spring
驗證加@RequestBody 的參數app
@RequestMapping("/test")
public String test(@Validated @RequestBody Book book) {
return "success";
}
1
2
3
4
這時候呢會出現MethodArgumentNotValidException異常能夠在ControllerAdvice 作全局異常處理ide
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ResponseEntity<ModelMap> ex(Exception e) {
log.error("請求參數不合法。", e);
ModelMap modelMap = new ModelMap();
if (e instanceof MethodArgumentNotValidException) {
modelMap.put("message", getErrors(((MethodArgumentNotValidException) e).getBindingResult()));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
}spring-boot
private Map<String, String> getErrors(BindingResult result) {
Map<String, String> map = new HashMap<>();
List<FieldError> list = result.getFieldErrors();
for (FieldError error : list) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
若是不加呢?post
@RequestMapping("/test")
public String test(@Validated Book book) {
return "success";
}
1
2
3
4
則會出BindException 異常,則又能夠在ControllerAdvice 中加入判斷ui
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ResponseEntity<ModelMap> ex(Exception e) {
log.error("請求參數不合法。", e);
ModelMap modelMap = new ModelMap();
if (e instanceof BindException) {
modelMap.put("message", getErrors(((BindException) e).getBindingResult()));
} else if (e instanceof MethodArgumentNotValidException) {
modelMap.put("message", getErrors(((MethodArgumentNotValidException) e).getBindingResult()));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
}
private Map<String, String> getErrors(BindingResult result) {
Map<String, String> map = new HashMap<>();
List<FieldError> list = result.getFieldErrors();
for (FieldError error : list) {
map.put(error.getField(), error.getDefaultMessage());
}
return map;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
驗證參數
若是是get請求參數呢?
@RequestMapping("/test")
public String test(@Validated @NotBlank(message = "name 不容許爲空") String name) {
System.out.println("111");
return "success";
}
1
2
3
4
5
咱們發現這樣根本很差使,其實呢這種須要在類上加入
@Validated
@RestController
public class TestController {
@RequestMapping("/test")
public String test(@NotBlank(message = "name 不容許爲空") String name) {
System.out.println("111");
return "success";
}
}
1
2
3
4
5
6
7
8
9
這樣才能夠生效,此時呢返回ConstraintViolationException 異常能夠在全局異常中這樣處理
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(Exception.class)
public ResponseEntity<ModelMap> ex(Exception e) {
log.error("請求參數不合法。", e);
ModelMap modelMap = new ModelMap();
if (e instanceof HttpMediaTypeException) {
modelMap.put("message", "請求體不對");
} else if (e instanceof ConstraintViolationException) {
ConstraintViolationException exs = (ConstraintViolationException) e;
Set<ConstraintViolation<?>> violations = exs.getConstraintViolations();
modelMap.put("message", getErrors(violations));
}
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(modelMap);
}
private Map<String, String> getErrors(Set<ConstraintViolation<?>> violations) {
Map<String, String> map = new HashMap<>();
for (ConstraintViolation<?> item : violations) {
map.put(item.getPropertyPath().toString(), item.getMessage());
}
return map;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Model校驗
如過不是想驗證傳參呢?就是想驗證一個實體怎麼玩呢?
這樣就能夠解決了
@RestController
public class TestController {
@Autowired
private Validator validator;
@RequestMapping("/test")
public Map<String, String> test() {
Book book = new Book();
book.setId(1).setName("");
Set<ConstraintViolation<Book>> violationSet = validator.validate(book);
return getErrors(violationSet);
}
private <T> Map<String, String> getErrors(Set<ConstraintViolation<T>> violations) {
Map<String, String> map = new HashMap<>();
for (ConstraintViolation<?> item : violations) {
map.put(item.getPropertyPath().toString(), item.getMessage());
}
return map;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
對象級聯校驗
在好比book那個實體中加入了一個具備對象這時候改怎麼辦呢?
只須要在實體上加入@Valid 便可
@Data
@Accessors(chain = true)
public class Book {
private Integer id;
@NotBlank(message = "name 不容許爲空")
@Length(min = 2, max = 10, message = "name 長度必須在 {min} - {max} 之間")
private String name;
@Valid
private Author author;
@Data
@Accessors(chain = true)
public static class Author {
@NotBlank(message = "Author.name 不容許爲空")
private String name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
hibernate-validator 的校驗模式
這時候要說點東西了
上面例子中一次性返回了全部驗證不經過的集合,一般按順序驗證到第一個字段不符合驗證要求時,就能夠直接拒絕請求了。Hibernate Validator有如下兩種驗證模式
普通模式(默認是這個模式)
普通模式(會校驗完全部的屬性,而後返回全部的驗證失敗信息)
快速失敗返回模式
快速失敗返回模式(只要有一個驗證失敗,則返回)
true 快速失敗返回模式 false普通模式
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.failFast( true )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
1
2
3
4
5
或者這樣配
ValidatorFactory validatorFactory = Validation.byProvider( HibernateValidator.class )
.configure()
.addProperty( "hibernate.validator.fail_fast", "true" )
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
1
2
3
4
5
這樣配置就好了
@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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
分組校驗
分組順序校驗時,按指定的分組前後順序進行驗證,前面的驗證不經過,後面的分組就不行驗證。
有這樣一種場景,新增的時候,不須要驗證Id(由於系統生成);修改的時候須要驗證Id,這時候可用用戶到validator的分組驗證功能。
設置validator爲普通驗證模式("hibernate.validator.fail_fast", "false"),用到的驗證GroupA、GroupB和實體:
GroupA、GroupB
1
public interface GroupA {
}
public interface GroupB {
}
1
2
3
4
而後改造一下Book實體
@Data
@Accessors(chain = true)
public class Book {
@NotBlank
@Range(min = 1, max = Integer.MAX_VALUE, message = "必須大於0", groups = {GroupA.class})
private Integer id;
@NotBlank(message = "name 不容許爲空")
@Length(min = 4, max = 20, message = "name 長度必須在 {min} - {max} 之間", groups = {GroupB.class})
private String name;
@NotBlank
@Range(min = 0, max = 100, message = "年齡必須在[0,100]", groups = {Default.class})
private Integer age;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
GroupA驗證字段id;
GroupB驗證字段name;
Default驗證字段age(Default是Validator自帶的默認分組)
這樣去驗證
@RequestMapping("/test")
public void test() {
Book book = new Book();
/**GroupA驗證不經過*/
book.setId(-10086);
/**GroupA驗證經過*/
//book.setId(10010);
book.setName("a");
book.setAge(110);
Set<ConstraintViolation<Book>> validate = validator.validate(book, GroupA.class, GroupB.class);
for (ConstraintViolation<Book> item : validate) {
System.out.println(item);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
或者這樣
@RequestMapping("/test")
public void test(@Validated({GroupA.class, GroupB.class}) Book book, BindingResult result) {
if (result.hasErrors()) {
List<ObjectError> allErrors = result.getAllErrors();
for (ObjectError error : allErrors) {
System.out.println(error);
}
}
}
1
2
3
4
5
6
7
8
9
固然這樣驗證務必要給 組一個序列,否則不行的仍是沒法實現
@GroupSequence({GroupA.class, GroupB.class, Default.class})
public interface GroupOrder {
}
1
2
3
這樣就行了而後這樣玩
Set<ConstraintViolation<Book>> validate = validator.validate(book, GroupOrder.class);
1
@Validated({GroupOrder.class})Book book, BindingResult result
1
注意項
若是不想全局攔截異常想看到直觀的錯誤能夠在方法參數中加入BindingResult result
單一的能夠這樣玩
public void test()(@Validated DemoModel demo, BindingResult result)
1
驗證多個的話能夠這樣玩
public void test()(@Validated DemoModel demo, BindingResult result,@Validated DemoModel demo2, BindingResult result2)
1
自定義驗證器
通常狀況,自定義驗證能夠解決不少問題。但也有沒法知足狀況的時候,此時,咱們能夠實現validator的接口,自定義本身須要的驗證器。
首先呢定義個註解,在註解上加入註解@Constraint 綁定驗證類
@Target({FIELD, PARAMETER})
@Retention(RUNTIME)
@Constraint(validatedBy = DateTimeValidator.class)
public @interface DateTime {
String message() default "格式錯誤";
String format() default "yyyy-MM-dd";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
而後看驗證類 實現ConstraintValidator<A extends Annotation, T>便可 A是註解 T是標註的參數
public class DateTimeValidator implements ConstraintValidator<DateTime, String> {
private DateTime dateTime;
@Override
public void initialize(DateTime dateTime) {
this.dateTime = dateTime;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 若是 value 爲空則不進行格式驗證,爲空驗證可使用 @NotBlank @NotNull @NotEmpty 等註解來進行控制,職責分離
if (value == null) {
return true;
}
String format = dateTime.format();
if (value.length() != format.length()) {
return false;
}
SimpleDateFormat simpleDateFormat = new SimpleDateFormat(format);
try {
simpleDateFormat.parse(value);
} catch (ParseException e) {
return false;
}
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
而後這樣用就行啦
@Validated
@RestController
public class ValidateController {
@GetMapping("/test")
public String test(@DateTime(message = "您輸入的格式錯誤,正確的格式爲:{format}", format = "yyyy-MM-dd HH:mm") String date) {
return "success";
}
}
1
2
3
4
5
6
7
8
JSR-303 註釋介紹
hibernate-validator均實現了 JSR-303 這裏只列舉了 javax.validation 包下的註解,同理在 spring-boot-starter-web 包中也存在 hibernate-validator 驗證包,裏面包含了一些 javax.validation 沒有的註解,有興趣的能夠看看
註解 說明@NotNull 限制必須不爲null@NotEmpty 驗證註解的元素值不爲 null 且不爲空(字符串長度不爲0、集合大小不爲0)@NotBlank 驗證註解的元素值不爲空(不爲null、去除首位空格後長度爲0),不一樣於@NotEmpty,@NotBlank只應用於字符串且在比較時會去除字符串的空格@Pattern(value) 限制必須符合指定的正則表達式@Size(max,min) 限制字符長度必須在 min 到 max 之間(也能夠用在集合上)@Email 驗證註解的元素值是Email,也能夠經過正則表達式和flag指定自定義的email格式@Max(value) 限制必須爲一個不大於指定值的數字@Min(value) 限制必須爲一個不小於指定值的數字@DecimalMax(value) 限制必須爲一個不大於指定值的數字@DecimalMin(value) 限制必須爲一個不小於指定值的數字@Null 限制只能爲null(不多用)@AssertFalse 限制必須爲false (不多用)@AssertTrue 限制必須爲true (不多用)@Past 限制必須是一個過去的日期@Future 限制必須是一個未來的日期@Digits(integer,fraction) 限制必須爲一個小數,且整數部分的位數不能超過 integer,小數部分的位數不能超過 fraction (不多用)哦對,這些校驗不只能在controller層用 在任何地方均可以的