spring-boot整合validate

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章