Bean Validation Spring參數校驗

設置依賴

spring boot的bean validation 由validation start支持,maven依賴以下:html

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
複製代碼

這裏能夠找到最新的版本。可是若是引用了spring-boot-starter-web,就不要須要引用validation-starter。java

基礎

本質上來講,validation的工做原理是經過特定的註解修飾對類的字段定義約束。 而後,把類傳遞給驗證器對象,校驗字段約束是否知足。 咱們將會看到更多的細節經過下面這些例子。web

驗證 Spring MVC Controller的輸入

假設已經實現一個Spring REST 服務,而且想要驗證客戶端傳入的參數,咱們能夠驗證任意HTTP請求的3個部分:正則表達式

  • request body
  • path variable (e.g. /user/{id})
  • query parameters 具體看下每一個部分的詳細
驗證request body

在post和get請求中,通用的作法是在request body裏面傳入一個json串。spring自動把json串映射爲一個java對象。如今,咱們想要檢查這個java對象是否知足需求。 輸入的Java對象:spring

class Input {

  @Min(1)
  @Max(10)
  private int numberBetweenOneAndTen;

  @Pattern(regexp = "^[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}$")
  private String ipAddress;
  
  // ...
}
複製代碼

對象擁有一個取值範圍在1-10之間的int類型字段,除此以外,還有一個包含ip地址的字符串類型字段。 從request body中接受參數對象而且驗證:編程

@RestController
class ValidateRequestBodyController {

  @PostMapping("/validateBody")
  ResponseEntity<String> validateBody(@Valid @RequestBody Input input) {
    return ResponseEntity.ok("valid");
  }

}
複製代碼

簡單的加個@Valid註解修飾輸入的參數,同時用@RequestBody標記應該從request body中解析參數。經過這個註解,咱們告訴spring在作其餘任何操做以前先把參數對象傳遞給Validator。 注意:若是待校驗對象的某個字段也是須要校驗的複雜類型(組合語法),這個字段也須要用@Valid修飾:json

@Valid
private ContactInfo contactInfo;
複製代碼

若是校驗失敗,會觸發MethodArgumentNotValidException異常,Spring默認會把這個一場轉爲400(Bad Request)。 經過集成測試驗證下:api

@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = ValidateRequestBodyController.class)
class ValidateRequestBodyControllerTest {

  @Autowired
  private MockMvc mvc;

  @Autowired
  private ObjectMapper objectMapper;

  @Test
  void whenInputIsInvalid_thenReturnsStatus400() throws Exception {
    Input input = invalidInput();
    String body = objectMapper.writeValueAsString(input);

    mvc.perform(post("/validateBody")
            .contentType("application/json")
            .content(body))
            .andExpect(status().isBadRequest());
  }
}
複製代碼
校驗path變量和query參數

驗證path變量和query參數有一些細微差異。由於路徑變量和請求參數是基本類型例如int 或者他們的包裝類型Integer或者String。 直接在Controller方法參數上加註解約束:bash

@RestController
@Validated
class ValidateParametersController {

  @GetMapping("/validatePathVariable/{id}")
  ResponseEntity<String> validatePathVariable(
      @PathVariable("id") @Min(5) int id) {
    return ResponseEntity.ok("valid");
  }
  
  @GetMapping("/validateRequestParameter")
  ResponseEntity<String> validateRequestParameter(
      @RequestParam("param") @Min(5) int param) { 
    return ResponseEntity.ok("valid");
  }
}
複製代碼

注意,同時必須在類級別機上@Validated註解告訴Spring須要校驗方法參數上的約束。 在這種場景裏@Validated註解只能修飾類級別,可是,它也容許被用在方法上(容許用在方法級別爲了解決validation group。) 校驗失敗會觸發ConstraintViolationException 異常,可是spring沒有提供默認處理這個一場的handler,因此會報500(Internal Server Error)。 若是咱們想要返回400 替代500,能夠在controller中增長以自定義的異常處理。數據結構

@RestController
@Validated
class ValidateParametersController {

  // request mapping method omitted
  
  @ExceptionHandler(ConstraintViolationException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) {
    return new ResponseEntity<>("not valid due to validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST);
  }

}
複製代碼

校驗Spring service層的參數

前面都是校驗controller級別,同時也支持校驗任何層級的參數。只須要組合使用@Validated 和 @Valid:

@Service
@Validated
class ValidatingService{

    void validateInput(@Valid Input input){
      // do something
    }

}
複製代碼

一樣的,@Validated註解只能做用於類級別,不要放在方法上。測試:

@ExtendWith(SpringExtension.class)
@SpringBootTest
class ValidatingServiceTest {

  @Autowired
  private ValidatingService service;

  @Test
  void whenInputIsInvalid_thenThrowsException(){
    Input input = invalidInput();

    assertThrows(ConstraintViolationException.class, () -> {
      service.validateInput(input);
    });
  }

}
複製代碼

實現自定義的校驗器

若是提供的註解約束沒有知足使用場景,也能夠本身實現一個。 在上面Input類中,咱們使用正則表達式來檢驗字符串字段是否爲有效的IP地址,可是這個正則表達式不夠完整,他容許每一段超過255. 實現一個IP校驗器替代正則表達式。 首先:新建一個IpAddress註解類

@Target({ FIELD })
@Retention(RUNTIME)
@Constraint(validatedBy = IpAddressValidator.class)
@Documented
public @interface IpAddress {

  String message() default "{IpAddress.invalid}";

  Class<?>[] groups() default { };

  Class<? extends Payload>[] payload() default { };

}
複製代碼

一個自定義註解約束須要下面這些:

  • message指向ValidationMessages.properties文件中一個參數key。
  • groups容許在不一樣校驗器狀況下有不一樣的校驗約束
  • payload 容許一些額外參數傳遞給校驗器
  • @Constraint註解指向實現了ConstraintValidator 接口的Validator。 Validator的實現像這樣:
class IpAddressValidator implements ConstraintValidator<IpAddress, String> {

  @Override
  public boolean isValid(String value, ConstraintValidatorContext context) {
    Pattern pattern = 
      Pattern.compile("^([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})\\.([0-9]{1,3})$");
    Matcher matcher = pattern.matcher(value);
    try {
      if (!matcher.matches()) {
        return false;
      } else {
        for (int i = 1; i <= 4; i++) {
          int octet = Integer.valueOf(matcher.group(i));
          if (octet > 255) {
            return false;
          }
        }
        return true;
      }
    } catch (Exception e) {
      return false;
    }
  }
}
複製代碼

如今,就可使用@IpAddress註解想其餘註解約束同樣:

class InputWithCustomValidator {

  @IpAddress
  private String ipAddress;
  
  // ...

}
複製代碼

經過編程的方式校驗

有一些場景,我想經過程序來調用校驗器而不是依賴Spring的支持。 在這種狀況下咱們能夠手動建立一個Validator而後觸發校驗。

class ProgrammaticallyValidatingService {
  
  void validateInput(Input input) {
    ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
    Validator validator = factory.getValidator();
    Set<ConstraintViolation<Input>> violations = validator.validate(input);
    if (!violations.isEmpty()) {
      throw new ConstraintViolationException(violations);
    }
  }
  
}
複製代碼

不須要Spring支持。 可是,Spring提供了與配置的驗證其實例,咱們能夠直接注入到service中而不是手動去建立它:

@Service
class ProgrammaticallyValidatingService {

  private Validator validator;

  ProgrammaticallyValidatingService(Validator validator) {
    this.validator = validator;
  }

  void validateInputWithInjectedValidator(Input input) {
    Set<ConstraintViolation<Input>> violations = validator.validate(input);
    if (!violations.isEmpty()) {
      throw new ConstraintViolationException(violations);
    }
  }
}
複製代碼

測試:

@ExtendWith(SpringExtension.class)
@SpringBootTest
class ProgrammaticallyValidatingServiceTest {

  @Autowired
  private ProgrammaticallyValidatingService service;

  @Test
  void whenInputIsInvalid_thenThrowsException(){
    Input input = invalidInput();

    assertThrows(ConstraintViolationException.class, () -> {
      service.validateInput(input);
    });
  }

  @Test
  void givenInjectedValidator_whenInputIsInvalid_thenThrowsException(){
    Input input = invalidInput();

    assertThrows(ConstraintViolationException.class, () -> {
      service.validateInputWithInjectedValidator(input);
    });
  }

}
複製代碼

使用不一樣的驗證組驗證不一樣用例下的同一個對象

常常會有兩個相同的Service使用同一個領域對象。 好比在實現CRUD操做時,建立操做和更新操做極可能使用用一個對象做爲參數,可是在兩種狀況下可能會觸發不一樣的驗證:

  • 建立狀況
  • 更新狀況
  • 二者同時存在 Validation Groups容許使用不一樣的規則來驗證。 剛剛已經看到一個約束註解必須有一個groups字段。他能夠被傳遞到任何一個定義了驗證組的Validator裏面。
class InputWithGroups {

  @Null(groups = OnCreate.class)
  @NotNull(groups = OnUpdate.class)
  private Long id;
  
  // ...
  
}
複製代碼

會確保ID在建立操做中是空的,而在更新操做中必定不爲空。 Spring經過@Validated註解修飾驗證組:

@Service
@Validated
class ValidatingServiceWithGroups {

    @Validated(OnCreate.class)
    void validateForCreate(@Valid InputWithGroups input){
      // do something
    }

    @Validated(OnUpdate.class)
    void validateForUpdate(@Valid InputWithGroups input){
      // do something
    }

}
複製代碼

注意:@Validated類再次被用到了類級別,這是由於在告訴Spring須要啓動方法上的約束註解(@Min),同時爲了激活驗證組group,必須把它做用在方法上。

返回結構化的響應

當校驗失敗時須要返回有意義的錯誤信息給客戶端。爲了能讓客戶端展現錯誤信息,咱們須要返回一個數據結構,其中包含每一個錯誤驗證信息。 首先,定義一個返回體:

public class ValidationErrorResponse {

  private List<Violation> violations = new ArrayList<>();

  // ...
}

public class Violation {

  private final String fieldName;

  private final String message;

  // ...
}
複製代碼

而後,定義一個全局的切面處理Controller級別的ConstraintViolationExceptions 異常和Request body級別的MethodArgumentNotValidExceptions異常。

@ControllerAdvice
class ErrorHandlingControllerAdvice {

  @ExceptionHandler(ConstraintViolationException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  ValidationErrorResponse onConstraintValidationException(
      ConstraintViolationException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (ConstraintViolation violation : e.getConstraintViolations()) {
      error.getViolations().add(
        new Violation(violation.getPropertyPath().toString(), violation.getMessage()));
    }
    return error;
  }

  @ExceptionHandler(MethodArgumentNotValidException.class)
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ResponseBody
  ValidationErrorResponse onMethodArgumentNotValidException(
      MethodArgumentNotValidException e) {
    ValidationErrorResponse error = new ValidationErrorResponse();
    for (FieldError fieldError : e.getBindingResult().getFieldErrors()) {
      error.getViolations().add(
        new Violation(fieldError.getField(), fieldError.getDefaultMessage()));
    }
    return error;
  }

}
複製代碼

經過捕獲異常而且轉換爲結構的錯誤信息返回。

總結

咱們已經完成了使用Spring Boot構建應用過程當中可能須要全部的校驗特性。 固然,複雜的業務規則,建議你們使用Spring或者Guava裏面Assert類來判斷,好比這種複雜的業務規則判斷:

  • column A value is > 10.
  • column B value > 10
  • column A +column B > 30. 因此,Bean Validation只使用與參數的校驗,不要讓它參與業務邏輯。 文章翻譯自這裏。建議看完原文後,看看下面的討論,你有疑惑的,美國的工程師也有疑惑。因此文章下面會有不少看文章時不太明白的解答。 本身整理的記憶思路(約定默認的註解稱之爲約束註解eg:@Null,@Min,@NotNull...):
  • Controller類中,校驗Dto: @Valid + Dto 約束註解
  • Controller類中,校驗路徑變量或者參數變量:Class @Validated + Method 約束註解
  • Service層,校驗Dto:Class @Validated + Method @Valid + Dto 約束註解
  • Service層,校驗普通參數變量: Class @Validated + Method 約束註解 //TODO記憶思路必定不能依賴,最好的記憶辦法是去研究源碼,理解透徹再回來填這個坑。
相關文章
相關標籤/搜索