开发中,通常操作逻辑都是先进性数据校验,校验完毕以后才进行真正的逻辑处理。往往数据的校验逻辑比较众多,并且校验的逻辑由易到难,并且相同的校验逻辑在多处使用。例如,添加商品规格时,对价格,库存,商品重量等数值需要进行大于等于0的数值校验;对关联的商品id进行id的数据有效性校验。在修改商品规格时,也同样会执行相似的校验。这时,我们会想到应该对相似的业务逻辑进行抽象处理,封装校验逻辑。
这时,可以使用javax.validation校验注解,类似于AOP面向切面的实现数据的校验。在springboot中引入spring-boot-starter-web依赖,就会自动引入hibernate-validator。如图:
内置注解
使用javax.validation校验参数,一般在controller层,对方法参数进行校验。常用的注解包括:@NotNull,值不能为空,@Positive,数字为正数,@Size,字符串大小限制。示例:
/**
* @Author iloveoverfly
**/
@Data
public class UserAddDto implements Serializable {
private static final long serialVersionUID = -6630904002198113779L;
@NotEmpty(message = "用户名称为空")
private String username;
@NotNull(message = "用户的类型为空")
private Integer category;
}
扩展注解
在系统中,会根据业务新增不同的校验逻辑,例如,电话号码的校验,数字类型枚举值校验,数据id的有效性校验等等。示例,定义注解@EffectiveValue
进行数据有效性校验,定义ValueValidator接口实现具体的校验逻辑,EffectiveValue注解定义如下:
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(EffectiveValue.List.class)
@Documented
@Constraint(
validatedBy = {EffectiveValueConstraint.class}
)
public @interface EffectiveValue {
String message() default "{javax.validation.constraints.EffectiveValue.message}";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
/**
* spring 容器的中服务bean
*/
Class extends ValueValidator> serviceBean();
/**
* 被校验值允许为空
*/
boolean shouldBeNull() default false;
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface List {
EffectiveValue[] value();
}
}
ValueValidator接口定义如下:
public interface ValueValidator {
boolean validate(T value);
}
示例,定义产品id数据有效性校验,代码如下:
@Slf4j
@Component
public class IdOfProductValidator implements ValueValidator {
@Autowired
private ProductService productService;
@Override
public boolean validate(Long id) {
if (Objects.isNull(id)) {
return false;
}
// id对应数据在数据库已经存在
return Objects.nonNull(productService.get(ProductPageQueryVo.builder()
.id(id)
.notStatus(ProductStatus.ABANDONED.getStatus()).build()));
}
}
在添加商品规格和修改商品时,对商品id进行校验,使用注解实现了类似AOP的横切处理,示例如下:
// 添加商品规格时,产品id校验
public class AddGoodsVo implements Serializable {
private static final long serialVersionUID = -2980901104711589147L;
@NotNull(message = PRODUCT_ID_IS_NULL)
@EffectiveValue(shouldBeNull = true, serviceBean = IdOfProductValidator.class, message = PRODUCT_ID_IS_INVALID)
private Long productId;
}
// 修改商品规格时,产品id校验
public class UpdateGoodsVo implements Serializable {
// 如果需要修改产品id,就对该id就行数据有效性校验
@EffectiveValue(shouldBeNull = true, serviceBean = IdOfProductValidator.class, message = PRODUCT_ID_IS_INVALID, groups = {ValidateGroup.Second.class})
private Long productId;
}
@Validated与@Valid使用
Validated和Valid都可以实现参数校验。Validated能够对参数校验可以进行分组,用于根据不同的分组生效指定的校验逻辑。定义ValidateGroup,用于却分不同校验的分组,UserAddOrUpdateDto,用于用户新增或者修改的共同参数,代码如下:
// 校验规则的分组
public class ValidateGroup {
/**
* 新增分组
*/
public interface Add {
}
/**
* 修改操作分组
*/
public interface Update {
}
}
// 用户新增或者修改的参数部分规则
@Data
public class UserAddOrUpdateDto implements Serializable {
private static final long serialVersionUID = -3968061262394340781L;
@NotNull(message = "id为空", groups = {ValidateGroup.Update.class})
private Long id;
@NotEmpty(message = "名称为空", groups = {ValidateGroup.Add.class, ValidateGroup.Update.class})
private String name;
}
// 用户新增和修改api
@RestController
@RequestMapping("/users")
@Slf4j
public class UserController {
@Autowired
private IUserManager userManager;
// 用户新增
@PostMapping("/add")
public Response addUser(@Validated(ValidateGroup.Add.class) @RequestBody UserAddOrUpdateDto userAddDto) {
return Response.success(this.userManager.addUser(userAddDto));
}
// 用户修改
@PutMapping("/update")
public Response updateUser(@Validated(ValidateGroup.Update.class) @RequestBody UserAddOrUpdateDto userUpdateDto) {
return Response.success(userManager.userUpdateDto(userUpdateDto));
}
}
Valid用于生效包含类参数的校验逻辑,示例,用户参数中的权限校验参数中,权限编码不能为空。代码如下:
// 用户新增或者修改的参数部分规则
@Data
public class UserAddOrUpdateDto implements Serializable {
private static final long serialVersionUID = -3968061262394340781L;
@NotNull(message = "id为空", groups = {ValidateGroup.Update.class})
private Long id;
@NotEmpty(message = "名称为空", groups = {ValidateGroup.Add.class, ValidateGroup.Update.class})
private String name;
// 权限校验字段生效
@Valid
private List permissions;
// 用户拥有权限
@Data
public final class PermissionDto implements Serializable {
private static final long serialVersionUID = 9057606562562888975L;
private Long id;
private String name;
@NotEmpty(message = "权限编码为空")
private String code;
}
}
校验顺序的控制
在逻辑校验的过程中,一般会有一个校验顺序。例如,对产品id进行校验时,首先判断id是否存在;其次,根据该id查询数据库进行有效性校验。这就存在一个先后顺序。在validation中,可以通过分组来实现。例如,定义分组的类,代码如下:
public class ValidateGroup {
/**
* 第一组
*/
public interface First {
}
/**
* 第二组
*/
public interface Second {
}
}
在校验注解上添加对应的分组,代码如下:
public class AddGoodsVo implements Serializable {
private static final long serialVersionUID = -2980901104711589147L;
// 第一组校验
@NotNull(message = PRODUCT_ID_IS_NULL, groups = {ValidateGroup.First.class})
// 第二组校验
@EffectiveValue(shouldBeNull = true, serviceBean = IdOfProductValidator.class, message = PRODUCT_ID_IS_INVALID, groups = {ValidateGroup.Second.class})
private Long productId
}
在校验的对象上,使用@Validated定义校验的顺序,代码如下:
// 定义 First ,Second的校验顺序
public Response saveGoods(@Validated({ValidateGroup.First.class, ValidateGroup.Second.class})
@RequestBody AddGoodsVo addGoodsVo) {
.......
}