常见注解
@NotNull: 说一个字段不能为空。
@NotEmpty: 表示列表字段不能为空。
@NotBlank: 表示字符串字段不能是空字符串(即它必须至少有一个字符)。
@Min和@Max: 表示数字字段仅在其值高于或低于某个值时才有效。
@Pattern: 表示一个字符串字段只有在匹配某个正则表达式时才有效。
@Email: 表示字符串字段必须是有效的电子邮件地址。
@Validated和@Valid
使用这两个注解,spring会进行自动验证
@Validated:用于类级别
@Valid:用在方法参数或字段。
验证请求正文
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);
}
}
结合@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验证时,需要思考这是不是必要的?
当到处都是这种验证时,如果遇到意外的验证错误,不会那么容易找到触发验证的位置。这时就会导致浪费大量的时间