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 REST 服务,而且想要验证客户端传入的参数,咱们能够验证任意HTTP请求的3个部分:正则表达式
在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参数有一些细微差异。由于路径变量和请求参数是基本类型例如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);
}
}
复制代码
前面都是校验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 { };
}
复制代码
一个自定义注解约束须要下面这些:
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操做时,建立操做和更新操做极可能使用用一个对象做为参数,可是在两种状况下可能会触发不一样的验证:
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类来判断,好比这种复杂的业务规则判断: