• 立体式校验保护,让你的系统避免 90% 以上的 bug


    1. 概览

    在实际开发过程中,数据校验是最为重要的一环,问题数据一旦进入系统,将对系统造成不可估量的损失。轻者,查询时触发空指针异常,导致整个页面不可用;重者,业务逻辑错误,造成流量甚至金钱上的损失。

    1.1. 背景

    数据校验,天天都在做,但因此而引起的bug也一直没有中断。没有经验的同学精力只定在正常流程,对于边界条件视而不见;有经验的同学,编写大量的代码,对数据进行验证,确实大幅提升了系统的健壮性,但也耗费了大量精力。

    对此,我们需要:

    1. 一套完整方法论和工具,对系统进行立体式防护;
    2. 简单快捷,快速接入,降低开发负担;

    1.2. 目标

    首先,先看下应用程序架构,其中的每一个层次都需不同的验证机制进行保障。

     

    常用应用架构

    构建完整的验证体系,从各个层次对应用服务提供保护,需考虑:

    1. 应用层参数验证,包括:
    2. 入参验证
    3. 嵌入对象验证
    4. 自定义逻辑验证
    5. 领域层业务验证。
    6. 业务规则插件化
    7. 存储层规则验证;
    8. 入库前规则校验

    2. 快速入门

    2.1. Spring Validator 入门

    Spring 对 Validator 提供了支持,可以对简单属性进行验证,大大降低编码量。

    添加 vlidator starter 依赖,具体如下:

    1. <dependency>
    2.     <groupId>org.springframework.boot</groupId>
    3.     <artifactId>spring-boot-starter-validation</artifactId>
    4. </dependency>

    Starter 会自动引入 hibernate-validator,并完成与 Spring MVC 和 Spring AOP 的集成。此时,便可以使用验证注解对入参或属性进行标注,Bean Validation 内置的注解如下:

    注解

    含义

    @Valid

    标记的元素为一个对象,对其所有字段进行检测

    @Null

    被标注的元素必须为 null

    @NotNull

    被标注的元素必须不为 null

    @AssertTrue

    被标记的元素必须为 true

    @AssertFalse

    被标记的元素必须为 false

    @Min(value)

    被标记的元素为数值,并且大于等于最小值

    @Max(value)

    被标记的元素为数值,并且小于等于最大值

    @DecimalMin(value)

    被标记的元素为数值,并且大于等于最小值

    @DecimalMax(value)

    被标记的元素为数值,并且小于等于最大值

    @Size(max, min)

    被标记的元素必须指定范围内

    @Digits (integer, fraction)

    被注释的元素必须是一个数字,其值必须在可接受的范围内

    @Past

    被注释的元素必须是一个过去的日期

    @Future

    被注释的元素必须是一个将来的日期

    @Pattern(value)

    被注释的元素必须符合指定的正则表达式

    Hibernat Validator 扩展注解如下:

    注解

    含义

    @Email

    被标注的元素必须是邮箱

    @Length(min=, max=)

    被标注的字符串必须在指定范围内

    @NotEmpty

    被标注的字符串不能为空串

    @Range(min=, max=)

    被标注的元素必须在指定范围内

    @NotBlank

    被标注的字符串不能为空串

    @URL(protocol=,host=, port=, regexp=, flags=)

    被标记的元素必须为有效的 url

    @CreditCardNumber

    被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性

    @ScriptAssert(lang=, script=, alias=)

    要有Java Scripting API 即JSR 223 的实现

    @SafeHtml(whitelistType=, additionalTags=)

    classpath中要有jsoup包

    2.2. 基础参数验证

    基础参数验证是最简单的验证,直接使用 validator 提供的注解便可完成验证。

    2.2.1. 开启验证 AOP

    在接口或实现类上添加 @Validated 注解,将启动
    MethodValidationInterceptor 对方法进行验证拦截。

    具体代码如下:

    1. @Validated
    2. public interface ApplicationValidateService {
    3. }

    建议将 @Validated 注解添加到接口上,其所有实现类都会开启方法验证。

    2.2.2. 简单类型入参验证

    简单类型是最常见的入参,如需对其进行验证,只需在入参上添加对应注解即可,示例如下:

    void singleValidate(@NotNull(message = "id 不能为null") Long id);
    

    运行测试用例:

    applicationValidateService.singleValidate((Longnull);
    

    抛出如下异常:

    1. javax.validation.ConstraintViolationException: singleValidate.id: id 不能为null
    2.     at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    3.     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    4.     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)

    2.2.3. 对象类型入参验证

    为了方便,经常将多个属性封装到一个对象中,并使用该对象作为入参,如果想对对象类型的入参进行验证需要:

    1. 在对象的属性上根据需求增加验证注解,示例如下:
    1. @Data
    2. public class SingleForm {
    3.     @NotNull(message = "id不能为null")
    4.     private Long id;
    5.     @NotEmpty(message = "name不能为空")
    6.     private String name;
    7. }
    1. 在方法入参处使用 @Valid 注解,示例如下:
    void singleValidate(@Valid @NotNull(message = "form 不能为 null") SingleForm singleForm);
    

    此时,singleValidate 便拥有:

    1. singleForm 入参不能为空验证
    2. singleForm 示例属性验证
    3. id 不能为null
    4. name 不能为空

    运行单元测试:

    this.applicationValidateService.singleValidate((SingleForm) null);
    

    抛出如下异常:

    1. javax.validation.ConstraintViolationException: singleValidate.singleForm: form 不能为 null
    2.     at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    3.     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    4.     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)

    运行单元测试:

    1. SingleForm singleForm = new SingleForm();
    2. this.applicationValidateService.singleValidate(singleForm);

    抛出如下异常:

    1. javax.validation.ConstraintViolationException: singleValidate.singleForm.name: name不能为空, singleValidate.singleForm.id: id不能为null
    2.     at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    3.     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    4.     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)

    2.3. 扩展 Validation 框架

    有时仅仅验证单个属性无法满足业务需求,比如在修改密码时,需要用户输入两次密码,用以保障输入密码的准确性。

    在这种情况下,可以对 Validation 框架进行扩展,具体如下:

    1. 创建一个验证对象 Password,用于存储两次输入的值,示例如下:
    1. @Data
    2. public class Password {
    3.     @NotEmpty(message = "密码不能为空")
    4.     private String input1;
    5.     @NotEmpty(message = "确认密码不能为空")
    6.     private String input2;
    7. }

    其中,Password 中的两个属性全部添加了验证注解。

    1. 创建一个验证组件 PasswordValidator,用于对“两次密码是否一致”进行验证,示例如下:
    1. public class PasswordValidator implements ConstraintValidator<PasswordConsistency, Password> {
    2.     @Override
    3.     public boolean isValid(Password password, ConstraintValidatorContext constraintValidatorContext) {
    4.         if (password == null){
    5.             return true;
    6.         }
    7.         if (password.getInput1() == null){
    8.             return true;
    9.         }
    10.         if (password.getInput1().equals(password.getInput2())){
    11.             return true;
    12.         }
    13.         return false;
    14.     }
    15. }

    验证组件实现 ConstraintValidator 接口,仅当两次密码一致时通过验证。

    1. 创建验证注解 PasswordConsistency,代码如下:
    1. @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    2. @Retention(RetentionPolicy.RUNTIME)
    3. @Documented
    4. @Constraint(
    5.         validatedBy = PasswordValidator.class
    6. )
    7. public @interface PasswordConsistency {
    8.     String message() default "{javax.validation.constraints.password.consistency.message}";
    9.     Class[] groups() default {};
    10.     Class[] payload() default {};
    11. }

    其中 @Constraint 用于说明该注解使用的验证器为 PasswordValidator。

    一切准备好之后,并可以使用自定义验证组件,具体如下:

    void customSingleValidate(@NotNull @Valid @PasswordConsistency(message = "两次密码不相同") Password password);
    

    其中

    1. @NotNull 表明入参 password 不能为 null
    2. @Valid 表明对Password 的属性进行校验
    3. @PasswordConsistency 表明使用 PasswordValidator 进行验证

    运行单元测试:

    this.applicationValidateService.customSingleValidate(null);
    

    运行结果如下:

    1. javax.validation.ConstraintViolationException: customSingleValidate.password: 不能为null
    2.     at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    3.     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    4.     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)

    运行单元测试:

    1. Password password = new Password();
    2. this.applicationValidateService.customSingleValidate(password);

    运行结果如下:

    1. javax.validation.ConstraintViolationException: customSingleValidate.password.input1: 密码不能为空, customSingleValidate.password.input2: 确认密码不能为空
    2.     at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    3.     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    4.     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)

    运行单元测试:

    1. Password password = new Password();
    2. password.setInput1("123");
    3. password.setInput2("456");
    4. this.applicationValidateService.customSingleValidate(password);

    运行结果如下:

    1. javax.validation.ConstraintViolationException: customSingleValidate.password: 两次密码不相同
    2.     at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    3.     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    4.     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)

    2.4. 添加 Validateable 验证

    扩展验证规则非常繁琐,一个验证需要新建注解和验证类,并完成两者的配置,在实际开发中使用的频次极低。

    相反,在开发中更习惯调用对象上的验证方法进行数据验证,示例如下:

    1. if(!createUserCommand.validate()){
    2.     throw new XXXXException();
    3. }

    对于这种非常通用的解决方案,lego 提供了框架支持。

    2.4.1. 引入 lego starter

    在配置文件中添加 lego starter,示例如下:

    1. <dependency>
    2.     <groupId>com.geekhalo.lego</groupId>
    3.     <artifactId>lego-starter</artifactId>
    4.     <version>0.1.6-validator-SNAPSHOT</version>
    5. </dependency>

    基于 Spring Boot 的自动配置机制,
    ValidatorAutoConfiguration 将自动添加 ValidateableMethodValidationInterceptor,对方法进行拦截,进行数据校验。

    2.4.2. 应用 Validateable

    比如,用户注册时,系统要求密码与用户名不能相同。使用 Validateable 进行验证具体如下:
    让对象继承自 Validateable,并实现 validate 接口,示例代码如下:

    1. @Data
    2. public class UserValidateForm implements Validateable {
    3.     @NotEmpty
    4.     private String name;
    5.     @NotEmpty
    6.     private String password;
    7.     @Override
    8.     public void validate(ValidateErrorHandler validateErrorHandler) {
    9.         if (getName().equals(getPassword())){
    10.             validateErrorHandler.handleError("user""1""用户名密码不能相同");
    11.         }
    12.     }
    13. }

    验证方法如下:

    void validateForm(@NotNull @Valid UserValidateForm userValidateForm);
    

    运行单元测试:

    this.applicationValidateService.validateForm(null);
    

    运行结果如下:

    1. javax.validation.ConstraintViolationException: validateForm.userValidateForm: 不能为null
    2.     at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    3.     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    4.     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)

    运行单元测试:

    1. UserValidateForm userValidateForm = new UserValidateForm();
    2. this.applicationValidateService.validateForm(userValidateForm);

    运行结果如下:

    1. javax.validation.ConstraintViolationException: validateForm.userValidateForm.name: 不能为空, validateForm.userValidateForm.password: 不能为空
    2.     at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:120)
    3.     at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    4.     at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:763)

    运行单元测试:

    1. UserValidateForm userValidateForm = new UserValidateForm();
    2. userValidateForm.setName("name");
    3. userValidateForm.setPassword("name");
    4. this.applicationValidateService.validateForm(userValidateForm);

    运行结果如下:

    1. javax.validation.ConstraintViolationException: null: 用户名密码不能相同
    2.     at com.geekhalo.lego.starter.validator.ValidatorAutoConfiguration.lambda$validateErrorReporter$1(ValidatorAutoConfiguration.java:61)
    3.     at com.geekhalo.lego.starter.validator.ValidatorAutoConfiguration$$Lambda$749/562345204.handleErrors(Unknown Source)
    4.     at com.geekhalo.lego.core.validator.ValidateableMethodValidationInterceptor.invoke(ValidateableMethodValidationInterceptor.java:39)

    2.5. 业务规则插件化

    在一些复杂流程中,业务规则校验逻辑占比非常重,大量的 if-else 充斥在主流程中非常不便于维护。

    在这种场景下,建议将验证组件插件化,使得每个验证逻辑全部封装在一个类中,将逻辑进行拆分,最终实现“开闭原则”。

    2.5.1. 初识 ValidateService

    ValidateService 整体架构如下:

     

    image

    其中,包括两个核心组件:

    1.BeanValidator。业务验证接口,由开发人员实现,用于承载验证逻辑,包括:

    • support 方法(继承自SmartComponent)用于定义组件应用场景
    • validate 方法,实现业务逻辑

    2.ValidateService。验证服务的入口,主要职责包括:

    • 管理所有的 BeanValidator 实现,由 Spring 完成所有的 BeanValidator 实例注入,并对其进行统一管理;
    • 对外提供 validate 方法,从 BeanValidator 实例中选择对应的组件,并调用 BeanValidator 的 validate 方法;

    整体介绍完成后,让我们看一个真实案例。比如,在一个生单流程中,我们需要保障:

    1. 用户必须存在,并且为可用状态;
    2. 商品必须存在,并且为售卖状态;
    3. 库存余量必须大于购买数量;

    这三个规则相互独立,没有太多关联关系,如果在一个方法中编写,便会产生强耦合,不利于应对未来的变更。这种情况下,最佳方案是将其封装到不同的组件中。示例如下:

    1. UserStatusValidator
    1. @Order(1)
    2. @Component
    3. public class UserStatusValidator
    4.         extends FixTypeBeanValidator {
    5.     @Override
    6.     public void validate(CreateOrderContext context, ValidateErrorHandler validateErrorHandler) {
    7.         if (context.getUser() == null){
    8.             validateErrorHandler.handleError("user""1""用户不存在");
    9.         }
    10.         if (!context.getUser().isEnable()){
    11.             validateErrorHandler.handleError("user""2""当前用户不可以");
    12.         }
    13.     }
    14. }
    1. ProductStatusValidator
    1. @Component
    2. @Order(2)
    3. public class ProductStatusValidator
    4.         extends FixTypeBeanValidator {
    5.     @Override
    6.     public void validate(CreateOrderContext context, ValidateErrorHandler validateErrorHandler) {
    7.         if(context.getProduct() == null){
    8.             validateErrorHandler.handleError("product""2""商品不存在");
    9.         }
    10.         if (!context.getProduct().isSaleable()){
    11.             validateErrorHandler.handleError("product""3""商品不可售卖");
    12.         }
    13.     }
    14. }
    1. StockCapacityValidator
    1. @Component
    2. @Order(3)
    3. public class StockCapacityValidator
    4.         extends FixTypeBeanValidator {
    5.     @Override
    6.     public void validate(CreateOrderContext context, ValidateErrorHandler validateErrorHandler) {
    7.         if (context.getStock() == null){
    8.             validateErrorHandler.handleError("stock""3""库存不存在");
    9.         }
    10.         if (context.getStock().getCount() < context.getCount()){
    11.             validateErrorHandler.handleError("stock""4""库存不足");
    12.         }
    13.     }
    14. }

    三个验证组件具有以下特征:

    1. 继承自 FixTypeBeanValidator,仅对 CreateOrderContext 进行处理
    2. 使用 @Component 将其声明为 Spring 的托管bean,从而被框架所感知;
    3. 使用 @Order(n) 标记运行顺序

    其中,FixTypeBeanValidator 会根据泛型进行类型判断,自动完成组件的筛选。代码如下:

    1. public abstract class FixTypeBeanValidator<A> implements BeanValidator<A>{
    2.     private final Class<A> type;
    3.     protected FixTypeBeanValidator(){
    4.         Class<A> type = (Class<A>)((ParameterizedType)getClass()
    5.                 .getGenericSuperclass())
    6.                 .getActualTypeArguments()[0];
    7.         this.type = type;
    8.     }
    9.     protected FixTypeBeanValidator(Class<A> type) {
    10.         this.type = type;
    11.     }
    12.     @Override
    13.     public final boolean support(Object a) {
    14.         return this.type.isInstance(a);
    15.     }
    16. }

    有了验证组件后,可以直接使用 ValidateService 进行验证,具体示例代码如下:

    1. @Override
    2. public void createOrder(CreateOrderContext context) {
    3.     validateService.validate(context);
    4. }

    运行测试用例:

    1. CreateOrderContext context = new CreateOrderContext();
    2. context.setUser(User.builder()
    3.         .build());
    4. context.setProduct(Product.builder()
    5.         .build());
    6. context.setStock(Stock.builder()
    7.         .count(0)
    8.         .build());
    9. context.setCount(1);
    10. this.domainValidateService.createOrder(context);

    运行结果如下:

    1. ValidateException(name=stock, code=4, msg=库存不足)
    2.     at com.geekhalo.lego.core.validator.BeanValidator.lambda$validate$0(BeanValidator.java:17)
    3.     at com.geekhalo.lego.core.validator.BeanValidator$$Lambda$1383/1570024586.handleError(Unknown Source)
    4.     at com.geekhalo.lego.validator.StockValidator.validate(StockValidator.java:24)
    5.     at com.geekhalo.lego.validator.StockValidator.validate(StockValidator.java:13)

    该设计符合开闭原则:

    1. 新增验证规则时,只需编写新的验证组件;
    2. 修改验证规则时,只需修改对应的验证组件,其他逻辑不受影响;

    2.5.2. 与 LazyLoad 集成

    有了灵活的验证体系,最麻烦的就是对 Context 的维护,主要矛盾为:

    1. 如果一次性加载 Context 的全部数据,可能在第一个验证组件就中断流程,白白加载了过多数据;
    2. 可以在获取的时候进行判断,只有为 null 的时候才进行加载。但,如果多个组件依赖同一组数据,将会:
    3. 每个组件都需要写一遍加载逻辑
    4. 为了避免多次加载,需要将数据写回到 Context 实例
    5. 加载逻辑和验证逻辑放在一起,职责混乱

    对于这种情况,最好的方式便是让 Context 具有延时加载的能力,其特征如下:

    1. 只有在调用 getter 方法时,才触发加载,避免全部加载产生的浪费
    2. 成功加载后,将数据通过 setter 写回到 Context,由其他组件进行共享

    这正是 LazyLoad 的设计初衷,示例如下:
    定义一个具有延时加载能力的 Context,代码如下:

    1. @Data
    2. public class CreateOrderContextV2 implements CreateOrderContext{
    3.     private CreateOrderCmd cmd;
    4.     @LazyLoadBy("#{@userRepository.getById(cmd.userId)}")
    5.     private User user;
    6.     @LazyLoadBy("#{@productRepository.getById(cmd.productId)}")
    7.     private Product product;
    8.     @LazyLoadBy("#{@addressRepository.getDefaultAddressByUserId(user.id)}")
    9.     private Address defAddress;
    10.     @LazyLoadBy("#{@stockRepository.getByProductId(product.id)}")
    11.     private Stock stock;
    12.     @LazyLoadBy("#{@priceService.getByUserAndProduct(user.id, product.id)}")
    13.     private Price price;
    14. }

    基于 CreateOrderContextV2 编写验证组件,代码如下:

    1. @Component
    2. @Order(3)
    3. public class StockCapacityV2Validator
    4.         extends FixTypeBeanValidator {
    5.     @Override
    6.     public void validate(CreateOrderContextV2 context, ValidateErrorHandler validateErrorHandler) {
    7.         if (context.getStock() == null){
    8.             validateErrorHandler.handleError("stock""3""库存不存在");
    9.         }
    10.         if (context.getStock().getCount() < context.getCmd().getCount()){
    11.             validateErrorHandler.handleError("stock""4""库存不足");
    12.         }
    13.     }
    14. }

    编写验证服务,代码如下:

    1. @Override
    2. public void createOrder(CreateOrderCmd cmd) {
    3.     CreateOrderContextV2 context = new CreateOrderContextV2();
    4.     context.setCmd(cmd);
    5.     CreateOrderContextV2 contextProxy = this.lazyLoadProxyFactory.createProxyFor(context);
    6.     this.validateService.validate(contextProxy);
    7. }

    lazyLoadProxyFactory 生成具有延迟加载能力的 Context 对象。

    运行单元测试,核心代码如下:

    1. CreateOrderCmd cmd = new CreateOrderCmd();
    2. cmd.setCount(10000);
    3. cmd.setProductId(100L);
    4. cmd.setUserId(100L);
    5. this.domainValidateService.createOrder(cmd);

    运行结果如下:

    1. ValidateException(name=stock, code=4, msg=库存不足)
    2.     at com.geekhalo.lego.core.validator.BeanValidator.lambda$validate$0(BeanValidator.java:17)
    3.     at com.geekhalo.lego.core.validator.BeanValidator$$Lambda$1388/1691696909.handleError(Unknown Source)
    4.     at com.geekhalo.lego.validator.StockCapacityV2Validator.validate(StockCapacityV2Validator.java:25)
    5.     at com.geekhalo.lego.validator.StockCapacityV2Validator.validate(StockCapacityV2Validator.java:14)
    6.     at com.geekhalo.lego.core.validator.BeanValidator.validate(BeanValidator.java:16)
    7.     at com.geekhalo.lego.core.validator.ValidateService.lambda$validate$5(ValidateService.java:34)

    2.6. 持久化前规则验证

    将问题数据写入到数据库是一个高危操作,轻则出现展示问题,比如 空指针异常;重则出现逻辑问题,比如金额对不上等。

    一个最常见的例子便是 订单系统的金额计算。随着业务的发展,金额计算变得越来越复杂,比如优惠券、满赠、满减、VIP 用户折扣等,这些业务都会对 订单上的金额进行操作,一旦出现bug将导致严重的问题。

    由于上层的更新入口太多,很难有一套行之有效的机制保障其不出问题。不如换个视角,在将变更同步到数据库前,有没有一种比较通用的检测机制能发现金额问题?

    其实是有的,无论上层业务怎么变化,金额恒等式是不变的,及:

    用户支付金额 = 商品总售卖金额(售价 * 数量) - 优惠总金额 - 手工改价金额
    

    只需在变更写回数据库前运行校验逻辑,如果不符合公式,则直接抛出异常。

    很多框架都提供了对实体生命周期的扩展,比如 JPA 就提供了大量注解,以便在实体生命周期中嵌入回调方法。

    以标准的Order设计为例,具体如下:

    1. @Entity
    2. @Table(name = "validate_order")
    3. @Data
    4. public class ValidateableOrder {
    5.     @Id
    6.     @GeneratedValue(strategy = GenerationType.IDENTITY)
    7.     private Long id;
    8.     /**
    9.      * 支付金额
    10.      */
    11.     private Integer payPrice;
    12.     /**
    13.      * 售价
    14.      */
    15.     private Integer sellPrice;
    16.     /**
    17.      * 购买数量
    18.      */
    19.     private Integer amount;
    20.     /**
    21.      * 折扣价
    22.      */
    23.     private Integer discountPrice;
    24.     /**
    25.      * 手工改价
    26.      */
    27.     private Integer manualPrice;
    28.     @PrePersist
    29.     @PreUpdate
    30.     void checkPrice(){
    31.         Integer realPayPrice = sellPrice * amount - discountPrice - manualPrice;
    32.         if (realPayPrice != payPrice){
    33.             throw new ValidateException("order""570""金额计算错误");
    34.         }
    35.     }
    36. }

    其中,@PrePersist 和 @PreUpdate 注解表明,checkPrice 方法在保存前和更新前进行回调,用以验证是否破坏了金额计算逻辑。

    使用 JpaRepository 对数据进行保存,具体如下:

    1. public void createOrder(ValidateableOrder order){
    2.     this.repository.save(order);
    3. }

    运行单元测试,代码如下:

    1. ValidateableOrder order = new ValidateableOrder();
    2. order.setSellPrice(20);
    3. order.setAmount(2);
    4. order.setDiscountPrice(5);
    5. order.setManualPrice(1);
    6. order.setPayPrice(35);
    7. this.applicationService.createOrder(order);

    运行结果如下:

    1. ValidateException(name=ordercode=570, msg=金额计算错误)
    2.     at com.geekhalo.lego.validator.ValidateableOrder.checkPrice(ValidateableOrder.java:53)
    3.     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    4.     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    5.     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    6.     at java.lang.reflect.Method.invoke(Method.java:483)
    7.     at org.hibernate.jpa.event.internal.EntityCallback.performCallback(EntityCallback.java:50)

    不仅如此,Spring 对事务进行回滚,避免脏数据进入到数据库。

    3. 小结

    对应用程序提供一套立体式的验证保障机制,包括:

    1. 应用层的基础数据校验
    2. 业务层的业务逻辑校验
    3. 存储层的持久化前校验

    这些措施共同发力,彻底将问题数据拒绝于系统之外。

  • 相关阅读:
    spring和springMVC整合父子容器问题:整合Spring时Service层为什么不做全局包扫描详解
    selenium 网页自动化-在访问一个网页时弹出的浏览器窗口,我该如何处理?
    vue3中遇到的问题
    Yolov8-pose关键点检测:原创自研&涨点系列篇 | 一种新颖的轻量化网络,用于提升遥感图像中的小物体检测 | 2024年二区YOLOv5改进最新成果
    【论文通读】CLIP改进工作综述
    Selenium结合Jenkins进行持续集成
    CentOS安装Docker
    动力总成悬置系统刚度及模态有效质量计算公式推导
    JNI 的数据类型以及和Java层之间的数据转换
    DRM全解析 —— encoder详解(1)
  • 原文地址:https://blog.csdn.net/m0_74931226/article/details/127934557