spring-boot整合validate
在Contrller中進行驗證
創建spring-boot整合validate工程,在Controller中驗證形參
springboot版本是2.0.0.RELEASE已經內置hibernate validate好的,隸屬於jsr303規範
官網:(以官網爲準)
http://hibernate.org/validator/
api doc
https://docs.jboss.org/hibernate/stable/validator/api/
創建工程,添加依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
spring-boot-starter-web已將依賴添加了
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
測試在不進行驗證時候的返回值
編寫Controler代碼
@RestController
@RequestMapping("validate")
@Slf4j
public class ValidateController {
@RequestMapping("validate")
public String validateTest(String address) {
log.info("address={}", address);
return "success";
}
}
訪問http://localhost:8080/validate/validate返回如下
{
"timestamp": "2019-11-20T08:06:15.728+0000",
"status": 400,
"error": "Bad Request",
"message": "Required String parameter 'address' is not present",
"path": "/user/validate"
}
訪問http://localhost:8080/validate/validate?address返回success
測試在進行進行驗證的時候的返回值
請求方法中的請求參數上直接添加驗證規則 如:@NotNull ,需要在該類上面需要添加@Validated
import org.hibernate.validator.constraints.NotBlank;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequestMapping("validate")
@Slf4j
@Validated
public class ValidateController {
@RequestMapping("validate")
public String validateTest(@NotBlank(message = "地址不能爲空!") String address) {
log.info("address={}", address);
return "success";
}
}
再次訪問http://localhost:8080/validate/validate?address如下.成功的進行了驗證
{
"timestamp": "2019-11-20T08:13:53.382+0000",
"status": 500,
"error": "Internal Server Error",
"message": "validateTest.address: 地址不能爲空!",
"path": "/user/validate"
}
驗證實體類
編寫一個實體類,@Past驗證生日字段
import java.util.Date;
import javax.validation.constraints.Past;
import org.springframework.format.annotation.DateTimeFormat;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
@Data
public class User {
private String name;
@Past(message = "生日必須是一個過去的日期")
@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy/MM/dd")
private Date birthday;
}
在Controller中進行驗證
Contrller的類上添加@Validated,方法的形參的實體類上添加@Valid註解,後面緊跟着BindingResult result(必須強制性的),校驗的結果放在了result中。
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.biillrobot.study.spring.validte.dataobject.User;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequestMapping("user")
@Slf4j
@Validated
public class UserController {
@RequestMapping("add")
public String add(@Valid User user, BindingResult result) {
log.info("user={}", user);
return "success";
}
}
測試訪問
http://localhost:8080/user/add?name=litong&birthday=2019/11/21
出現下面的錯誤提示
{
"timestamp": "2019-11-20T08:28:32.600+0000",
"status": 500,
"error": "Internal Server Error",
"message": "add.user.birthday: 生日必須是一個過去的日期",
"path": "/user/add"
}
但是如果格式不合法嗎?
http://localhost:8080/user/add?name=litong&birthday=2019-11-21
因爲在實體類中沒有對生日格式進行驗證,所以在這裏生日格式可以不合法,上面的訪問結果是success,日誌中顯示的是
2019-11-20 16:32:42.888 INFO UserController.add:21 - user=User(name=litong, birthday=null)
國際化
添加國際化驗證提示
在resources目錄下新建一個ValidationMessages_zh_CN.properties的文件,內容如下
user.birthday.past=\u751F\u65E5\u5FC5\u987B\u662F\u4E00\u4E2A\u8FC7\u53BB\u7684\u65E5\u671F
只需要在實體類上message指定用哪個消息key就行了
@Data
public class User {
private String name;
@Past(message = "{user.birthday.past}")
@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy/MM/dd")
private Date birthday;
}
發送請求,進行驗證,如果驗證失敗會返回錯誤的信息如下
同時日誌中也會出現一個ConstraintViolationException異常
javax.validation.ConstraintViolationException: add.user.birthday xxx
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:109)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:185)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:689)
at com.biillrobot.study.spring.validte.controller.UserController$$EnhancerBySpringCGLIB$$2465c54c.add(<generated>)
驗證國際化亂碼問題
[亂碼問題面描述]
ValidationMessages_zh_CN.properties的默認編碼是ISO-8891-1,輸入的漢字會經過unicode轉碼
user.birthday.past=\u751F\u65E5\u5FC5\u987B\u662F\u4E00\u4E2A\u8FC7\u53BB\u7684\u65E5\u671F
驗證失敗時返回的內容不會亂碼
但是如果將ValidationMessages_zh_CN.properties編碼改成UTF-8
返回的信息中就會包含亂碼
[亂碼問題原因]
筆者沒有找到原因,一直沒有解決
分組校驗和自定校驗規則
分組校驗
筆者分組校驗測試失敗,測試過程如下
編寫實體類
定義接口Update
在校驗註解屬性上使用groups指定class類
import java.util.Date;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import org.springframework.format.annotation.DateTimeFormat;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
@Data
public class User {
public interface Update{};
@NotNull(groups=Update.class ,message="更新時id不能爲空")
private Integer id;
private String name;
@Past(message = "{user.birthday.past}")
@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy/MM/dd")
private Date birthday;
}
Controller中
在方法的形參上使用@Validated指明class類
import javax.validation.Valid;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.biillrobot.study.spring.validte.dataobject.User;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequestMapping("user")
@Slf4j
@Validated
public class UserController {
@RequestMapping("add")
public String add(@Valid User user, BindingResult result) {
log.info("user={}", user);
return "success";
}
@RequestMapping("update")
public String update(@Validated({User.Update.class}) User user,BindingResult result){
log.info("user={}",user);
return "success";
}
}
奇怪的是測試失敗,不發送id竟然也是成功
http://localhost:8080/user/update?name=litong&birthday=2019/11/22
使用分組校驗,必須要手動獲取錯誤,定義返回的消息格式,,修改後的Controller代碼如下
@RequestMapping("update")
public String update(@Validated({ User.Update.class }) User user, BindingResult bindingResult) {
log.info("user={}", user);
if (bindingResult.hasErrors()) {
String errorMsg = bindingResult.getFieldError().getDefaultMessage();
log.info("errorMsg={}", errorMsg);
return errorMsg;
}
return "success";
}
分組校驗和其他校驗不同的是,即使校驗失敗,在數據庫中也不會出現異常
自定義校驗規則
自定義校驗器
自定類,實現ConstraintValidator,在泛型中填入註解類名和校驗的數據類型
重新isValid方法對接進行校驗,校驗通過返回true,校驗失敗返回false
下面的校驗規則定義,String類型必須爲空
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class MustEmptyValidator implements ConstraintValidator<MustEmpty, String> {
@Override
public boolean isValid(String input, ConstraintValidatorContext context) {
log.info("input={}", input);
// 驗證通過返回true
if (StringUtils.isEmpty(input)) {
log.info("驗證通過");
return true;
}
log.info("驗證失敗");
// 驗證失敗返回false
return false;
}
}
自定義註解,指定校驗器
@Constraint(validatedBy=MustEmptyValidator.class)
@Documented
@Target({ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MustEmpty {
String message() default "屬性必須爲空";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
使用自定義校規則
在實體類中使用自定義的校驗註解
import java.util.Date;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import org.springframework.format.annotation.DateTimeFormat;
import com.biillrobot.study.spring.validte.annotation.MustEmpty;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
@Data
public class User {
public interface Update{};
public interface Insert{};
@NotNull(groups=Update.class ,message="更新時id不能爲空")
@MustEmpty(groups=Insert.class,message="添加時id必須爲空")
private String id;
private String name;
@Past(message = "{user.birthday.past}")
@JsonFormat(pattern = "yyyy/MM/dd", timezone = "GMT+8")
@DateTimeFormat(pattern = "yyyy/MM/dd")
private Date birthday;
}
Controller變化不大,如下
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.biillrobot.study.spring.validte.dataobject.User;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequestMapping("user")
@Slf4j
@Validated
public class UserController {
@RequestMapping("add")
// public String add(@Valid User user, BindingResult result) {
public String add(@Validated(User.Insert.class) User user, BindingResult bindingResult) {
log.info("user={}", user);
if (bindingResult.hasErrors()) {
String errorMsg = bindingResult.getFieldError().getDefaultMessage();
log.info("errorMsg={}", errorMsg);
return errorMsg;
}
return "success";
}
@RequestMapping("update")
public String update(@Validated({ User.Update.class }) User user, BindingResult bindingResult) {
log.info("user={}", user);
return "success";
}
}
發送請求測試
http://localhost:8080/user/add?id=1&name=litong&birthday=2019/11/22
返回如下
添加時id必須爲空
全局異常攔截器
使用全局異常攔截器
編寫實體類
import javax.validation.constraints.Email;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import lombok.Data;
@Data
public class Student {
@NotBlank(message = "用戶名不能爲空")
private String name;
@Max(value = 120, message = "年齡不能超過120歲")
private int age;
@NotNull
@Size(min = 8, max = 20, message = "密碼必須大於8位並且小於20位")
private String password;
@Email(message = "請輸入符合格式的郵箱")
private String email;
}
編寫Controller
import javax.validation.Valid;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.biillrobot.study.spring.validte.dataobject.Student;
@RestController
@RequestMapping("/stu")
public class StudentController {
@RequestMapping("add")
public Student add(@Valid Student stu) {
// 僅測試驗證過程,省略其他的邏輯
return stu;
}
}
執行請求,發送一個錯誤的郵箱
http://localhost:8080/stu/add?name=litong&age=18&password=00000000&email=litongjava@
返回如下
{
"timestamp": "2019-11-22T06:22:02.768+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"Email.student.email",
"Email.email",
"Email.java.lang.String",
"Email"
],
"arguments": [
{
"codes": [
"student.email",
"email"
],
"arguments": null,
"defaultMessage": "email",
"code": "email"
},
[],
{
"defaultMessage": ".*",
"arguments": null,
"codes": [
".*"
]
}
],
"defaultMessage": "請輸入符合格式的郵箱",
"objectName": "student",
"field": "email",
"rejectedValue": "litongjava@",
"bindingFailure": false,
"code": "Email"
}
],
"message": "Validation failed for object='student'. Error count: 1",
"path": "/stu/add"
}
日誌中並沒有異常堆棧,顯示如下
2019-11-22 14:22:02.766 WARN DefaultHandlerExceptionResolver.logException:193 - Resolved exception caused by Handler execution: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors Field error in object 'student' on field 'email': rejected value [litongjava@]; codes [Email.student.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@538ddcc4,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@66098dbe]; default message [請輸入符合格式的郵箱] |
這是因爲,SpringBoot配置了默認異常處理器DefaultHandlerExceptionResolver,而該處理器僅僅是將異常信息打印出來,顯然,我們並不需要返回如此多的信息,只需要將對應屬性中的message信息給調用者即可,解決的方法有兩種。
1.在需要驗證的方法中加入BindingResult參數,SpringBoot會自動將異常錯誤信息綁定到該參數上,然後處理對應的邏輯,如下
@RequestMapping("add")
public Student add(@Valid Student stu, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 具體的處理邏輯,如封裝錯誤信息等
}
return stu;
}
但是這種方式不是很優雅,因爲對於每一個需要驗證的方法,都需要進行這樣的邏輯(雖然封裝處理可以解決,但依舊每次需要手動調用以及加入BindingResult參數)
2.由於在驗證失敗的時候,會拋出異常,所以可以使用全局異常處理器來捕獲該異常,然後進行統一處理即可,具體的異常類型是org.springframework.validation.BindException具體實現如下所示
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.extern.slf4j.Slf4j;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler({ BindException.class })
public ResultInfo<?> validationErrorHandler(BindException exception) {
// 獲取BindingResult對象
BindingResult bindingResult = exception.getBindingResult();
// 獲取bindingResul中的所有錯誤
List<ObjectError> allErrors = bindingResult.getAllErrors();
//將List<ObjectError>轉爲Stream<ObjectError>
Stream<ObjectError> stream = allErrors.stream();
//獲取Stream<ObjectError>中ObjectError.getDefaultMessage的返回值,組成新加的Stream<String>
Stream<String> map = stream.map(ObjectError::getDefaultMessage);
//Stream<String>轉爲List<String>
List<String> errorInformation = map.collect(Collectors.toList());
log.info("異常已將發現,獲取到的錯誤信息是={}", errorInformation);
return new ResultInfo<>(400, errorInformation.toString(), null);
}
}
發送和上面相同的請求,返回的消息語句如下
{
"code": 400,
"message": "[請輸入符合格式的郵箱]",
"body": null
}
日誌中內容如下
2019-11-22 14:50:18.240 INFO GlobalExceptionHandler.validationErrorHandler:28 - 異常已將發現,獲取到的錯誤信息是=[請輸入符合格式的郵箱] 2019-11-22 14:50:18.243 WARN ExceptionHandlerExceptionResolver.logException:193 - Resolved exception caused by Handler execution: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors Field error in object 'student' on field 'email': rejected value [litongjava@]; codes [Email.student.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@4ec9c0f5,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@1e0fa557]; default message [請輸入符合格式的郵箱] |
ObjectError中有很多字段,筆者可以按需獲取
獲取field和getMessage組合成map返回
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import lombok.extern.slf4j.Slf4j;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler({ BindException.class })
public ResultInfo<?> validationErrorHandler(BindException exception) {
// 獲取BindingResult對象
BindingResult bindingResult = exception.getBindingResult();
// 獲取bindingResul中的所有錯誤
List<ObjectError> allErrors = bindingResult.getAllErrors();
//獲取field和getMessage組合成map返回
Map<String, String> collect = allErrors.stream().collect(Collectors.toMap(item -> ((FieldError) item).getField(),
item -> item.getDefaultMessage(), (oldVal, currVal) -> oldVal));
return new ResultInfo<>(-400, exception.getMessage(), collect);
}
}
返回的信息內容如下
{
"code": -400,
"message": "org.springframework.validation.BeanPropertyBindingResult: 1 errors\nField error in object 'student' on field 'email': rejected value [litongjava@]; codes [Email.student.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@5e24fd07,org.springframework.validation.beanvalidation.SpringValidatorAdapter$ResolvableAttribute@4b426803]; default message [請輸入符合格式的郵箱]",
"body": {
"email": "請輸入符合格式的郵箱"
}
}
常用驗證註解
常用的註解主要有以下幾個,作用及內容如下所示
@Null,標註的屬性值必須爲空
@NotNull,標註的屬性值不能爲空
@AssertTrue,標註的屬性值必須爲true
@AssertFalse,標註的屬性值必須爲false
@Min,標註的屬性值不能小於min中指定的值
@Max,標註的屬性值不能大於max中指定的值
@DecimalMin,小數值,同上
@DecimalMax,小數值,同上
@Negative,負數
@NegativeOrZero,0或者負數
@Positive,整數
@PositiveOrZero,0或者整數
@Size,指定字符串長度,注意是長度,有兩個值,min以及max,用於指定最小以及最大長度
@Digits,內容必須是數字
@Past,時間必須是過去的時間
@PastOrPresent,過去或者現在的時間
@Future,將來的時間
@FutureOrPresent,將來或者現在的時間
@Pattern,用於指定一個正則表達式
@NotEmpty,字符串內容非空
@NotBlank,字符串內容非空且長度大於0
@Email,郵箱
@Range,用於指定數字,注意是數字的範圍,有兩個值,min以及max