Validation校检
认识
-
常见注解
@NotNull: 说一个字段不能为空。 @NotEmpty: 表示列表字段不能为空。 @NotBlank: 表示字符串字段不能是空字符串(即它必须至少有一个字符)。 @Min和@Max: 表示数字字段仅在其值高于或低于某个值时才有效。 @Pattern: 表示一个字符串字段只有在匹配某个正则表达式时才有效。 @Email: 表示字符串字段必须是有效的电子邮件地址。
-
@Validated
和@Valid
-
使用这两个注解,spring会进行自动验证
@Validated:用于类级别
@Valid:用在方法参数或字段。
-
基本使用
验证 Spring MVC 控制器的输入
-
验证请求正文
- 如果不满足条件则会触发
MethodArgumentNotValidException
异常 - 使用
@Valid
校检规则会生效
public class Input { // 最小10,最大30 @Min(10) @Max(30) private int age; } @RestController class ValidateRequestBodyController { @PostMapping("/validateBody") // 执行该方法前都会去触发Validator对参数进行校检 ResponseEntity<String> validateBody(@Valid @RequestBody Input input) { return ResponseEntity.ok("valid"); } }
- 如果不满足条件则会触发
-
验证路径变量和请求参数
-
在类上添加
@Validated
注解,用spring处理方法参数上的校检 -
相比
@Valid
,该注解失败将会触发错误500以及ConstraintViolationException
异常。如果想返回错误400,则需要添加自定义异常处理
@RestController @Validated public class ValidateRequestBodyController { // 路径变量 @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"); } // 自定义异常处理 @ExceptionHandler(ConstraintViolationException.class) // 拦截ConstraintViolationException异常 @ResponseStatus(HttpStatus.BAD_REQUEST) // 返回错误400 ResponseEntity<String> handleConstraintViolationException(ConstraintViolationException e) { return new ResponseEntity<>( "validation error: " + e.getMessage(), HttpStatus.BAD_REQUEST); } }
-
验证 service层输入
-
结合
@Validated
和@Valid
,可以验证任何 Spring 组件的输入@Service @Validated class ValidatingService{ void validateInput(@Valid Input input){ //省略... } } // 测试 @ExtendWith(SpringExtension.class) @SpringBootTest class ValidatingServiceTest { @Autowired private ValidatingService service; @Test void whenInputIsInvalid_thenThrowsException(){ assertThrows(ConstraintViolationException.class, () -> { service.validateInput(input); }); } }
验证持久化层-实体输入
-
不建议在该层进行验证,因为意味着上面业务代码已经使用了可能导致无法预料的错误的潜在无效对象
-
验证方式:
@Entity public class Input { @Min(1) @Max(10) private int numberBetweenOneAndTen; }
使用验证组为不同的用例验证不同的对象
-
用于区分验证对象
如:仅在更新时触发验证、或者仅在新增时触发验证。
-
步骤:
-
定义标记接口
interface OnCreate {} // 新增标记接口 interface OnUpdate {} // 更新标记接口
-
使用
class InputWithGroups { @Null(groups = OnCreate.class) // 新增时,不允许空值 @NotNull(groups = OnUpdate.class) // 更新时, 允许空值 private Long id; } @Service @Validated // 必须条件 class ValidatingServiceWithGroups { @Validated(OnCreate.class) // 要激活的验证组 void validateForCreate(@Valid InputWithGroups input){ // do something } @Validated(OnUpdate.class) // // 要激活的验证组 void validateForUpdate(@Valid InputWithGroups input){ // do something } }
-
-
注意事项:使用验证组混合了关注点,需要了解更多信息,因此有必要控制使用这种方式。
如:需要了解到id的新增和更新情况。
自定义验证错误
-
当验证时,希望返回更有意义的信息。
-
通过自定义全局异常处理器
@RestControllerAdvice class ErrorHandlingControllerAdvice { @ExceptionHandler(ConstraintViolationException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) ValidationErrorResponse onConstraintValidationException( ConstraintViolationException e) { // 省略无数... } @ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) ValidationErrorResponse onMethodArgumentNotValidException( MethodArgumentNotValidException e) { // 省略无数... } }
自定义验证器
-
用于扩展验证
-
步骤:
-
自定义约束注解
IpAddress
-
message:当条件不符合时,抛出异常的默认消息
-
groups:验证组
-
@Constraint
指向接口实现的注解ConstraintValidator
。
@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; // ... }
-
以编程方式验证
-
不依赖其他第三方验证支持。
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); } } } // 或者 // springboot提供一个预先配置Validator好的实例, // 当实例时,spring会自动将一个Validator实例注入到构造函数中 @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); } } }
验证反模式(使用问题)
仅在持久层中进行验证
-
持久层为最底层
如:mvc架构,web层(controller) -> 服务层(service) -> 持久层(mapper/dao)
-
只在持久层中进行验证,会导致一些无效的数据传入到了web层和业务层(服务层),因为这两层并没有进行校检。
从而在业务层产生一些错误,因此应当在业务层进行校检。如果需要在持久层进行进一步校检,充当安全网
使用霰弹式验证
-
散弹式验证:到处添加@Validation验证,不管他会不会得到验证。
-
这种方式的验证会导致减低开发速度、减低可读性
- 如:当别人看到这段代码时,会思考为什么这么设置?他一定是有原因的。但是其实他并没有用处,只是习惯性的添加验证。
因此在使用@Validation验证时,需要思考这是不是必要的?
-
当到处都是这种验证时,如果遇到意外的验证错误,不会那么容易找到触发验证的位置。这时就会导致浪费大量的时间
使用验证组进行用例验证
- 验证组违反了单一职责原则,模型类需要知道所有验证规则。如果用于特定用例的验证发生更改,则模型类必须更改。
- 当验证组越来越多时,会很难阅读,因为需要了解他相关的逻辑