系统设计之二妙用验证器Validator改善代码

使用 Preconditions 验证入参

在开发中,我们常常会使用 Preconditions 来验证接口的入参是否符合要求,代码如下

@Component
public class FlowCmdServiceImpl implements FlowCmdService {

    @Override
    public void withdraw(@Valid FlowWithdrawCmd cmd) {
        Preconditions.checkArgument(cmd != null,"请求不能为空");
        Preconditions.checkArgument(StringUtils.isNoneBlank(cmd.getFlowReqId()),"reqId不能为空");
        Preconditions.checkArgument(StringUtils.isNoneBlank(cmd.getOperator()),"operator不能为空");

        //验证通过,处理业务
    }
}

如果需要验证的数据很多,会浪费我们大量的精力处理参数的验证,而且会严重污染我们的代码。我们希望把重心放在处理业务逻辑上,一些参数的验证可以从业务代码中剥离处理,统一处理。

如果让我们自己设计一个这样的参数验证功能,该如何实现呢?我们猜测一下:

  1. 首先我们需要定义一个注解类 @Valid 来标识验证的范围。
  2. 还需要一些注解来标示验证的规则(如:@NotNull不能为空,@Min 最小数字等)
  3. 不能做侵入式的,那肯定需要使用动态代理来统一处理。

在spring boot中使用validation验证Controller

spring 已经为我们提供了一套完整的validation,我们先看看它的使用,在说说它的原理和我们猜测的是否一致。

引入相关依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

 定义要验证的请求类

/**
 * 工作流撤回命令
 * @author xiyangyang
 * @date 2021-09-28
 */
@Data
public class FlowWithdrawCmd extends Command {
    /**
     * 工作流申请ID
     */
    @NotBlank(message = "申请单ID不能为空")
    private String flowReqId;

    /**
     * 操作人ERP
     */
    @NotBlank(message = "操作人不能为空")
    private String operator;
}

定义一个申请单 Controller类

      请注意@Validated 和 @Valid 注解的使用

@Validated
@RestController
public class FlowController {

    @Resource
    private FlowCmdService flowCmdService;

    @Resource
    private FlowQueryService flowReqQueryService;

    @PostMapping("/flow/withdraw")
    public Response<FlowDTO> withdraw(@Valid FlowWithdrawCmd cmd){
        flowCmdService.withdraw(cmd);

        return Response.success();
    }

    @RequestMapping("/flow/queryByReqId")
    public String queryByReqId(@NotBlank(message = "reqId is not null") String reqId){
        return null;
    }
}

发送申请单查询请求:http://localhost:8080/flowreq/queryByReqId?reqId=   异常信息如下:

javax.validation.ConstraintViolationException: queryByReqId.reqId: reqId is not null

定义统一异常处理类

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

    /**
     * 处理Validated校验异常
     * <p>
     * 注: 常见的ConstraintViolationException异常, 也属于ValidationException异常
     *
     * @param e
     *         捕获到的异常
     * @return 返回给前端的data
     */
    @ResponseStatus(code = HttpStatus.BAD_REQUEST)
    @ExceptionHandler(value = {BindException.class, ValidationException.class, MethodArgumentNotValidException.class})
    public Response handleParameterVerificationException(Exception e) {
        log.error(" handleParameterVerificationException has been invoked", e);

        Response response = null;

        if (e instanceof BindException) {
            FieldError fieldError = ((BindException) e).getFieldError();
            if (fieldError != null) {
                response = Response.failure("-1",fieldError.getDefaultMessage());
            }
        } else if (e instanceof MethodArgumentNotValidException) {
            BindingResult bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
            FieldError fieldError = bindingResult.getFieldError();
            if (fieldError != null) {
                response = Response.failure("-1",fieldError.getDefaultMessage());
            }
        } else if (e instanceof ConstraintViolationException) {
            response =   Response.failure("-1",e.getMessage());

        } else {
            response = Response.failure("-1","处理参数时异常");
        }

        return response;
    }
}

写MVC的单元测试类 

public class FlowControllerTest extends BootBaseTest {
    @Autowired
    private WebApplicationContext context;

    private MockMvc mockMvc;

    @Before
    public void initMokcMvc() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
    }

    @Test
    public void testWithdraw() throws Exception {
        MockHttpServletRequestBuilder servlet = MockMvcRequestBuilders
                .post("/flow/withdraw")
                .contentType("application/json;charset=UTF-8")
                .content(JSON.toJSONString(new FlowWithdrawCmd()));
        servlet.characterEncoding("utf-8");

        MvcResult result = mockMvc.perform(servlet).andReturn();

        System.out.println(result.getResponse().getContentAsString(Charset.forName("utf-8")));
    }
}

运行测试类 

{"success":false,"code":"-1","message":"申请单ID不能为空"}

 运行的结果和我们期望是一致的,拦截了请求参数的验证信息。

在Service层使用validation

 在服务层使用和在controller 层有 什么不一样吗?

   定义服务接口

       注意需要在方法需要验证的参数前 添加注解 @Valid

public interface FlowProvider {
    /**
     * 取消申请单(移除申请单)
     */
    Response cancel(@Valid @NotNull(message = "req is not null") FlowCancelCmd cmd);
}

   接口实现类定义   

       接口实现类需要和接口定义一致 ,注意@Valid 和 @NotNull 。实现类上需求添加@Validated。使用的@Validated校验参数接口参数和实现类参数要保持一直,不然会报错。

@Validated
@Component
public class FlowProviderImpl implements FlowProvider {
    /**
     * 取消申请单
     */
    @Override
    @ResponseHandler
    public Response cancel(@Valid @NotNull(message = "req is not null") FlowCancelCmd cmd) {
        FlowReq flowReq = flowReqRepo.getByReqId4Update(cmd.getReqId());
        flowReq.cancel();
        return Response.success();
    }
}

添加注册的Bean 

@Bean
    public Validator validator() {
        ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
                .configure()
                .addProperty(HibernateValidatorConfiguration.FAIL_FAST, "true")
                .buildValidatorFactory();
        Validator validator = validatorFactory.getValidator();
        return validator;
    }

    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor(Validator validator) {
        MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
        methodValidationPostProcessor.setValidator(validator);
        return methodValidationPostProcessor;
    }

定义ResponseHandler 注解 

/**
 * RPC response 注解处理
 *
 * @author yangyanping
 * @date 2021-11-04
 */
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ResponseHandler {
}

 定义异常处理 拦截器

@Slf4j
@Aspect
@Component
public class ResponseAspect {
    @Pointcut("@annotation(com.flow.interfaces.provider.aop.ResponseHandler)")
    public void aspect() {
    }

    @Around("aspect()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("ResultAspect#around");


        try {
            Object result = joinPoint.proceed();
            if (result == null) {
                return Response.success();
            }

            if (!(result instanceof Response)) {
                return Response.success(result);
            }

            return result;
        } catch (IllegalArgumentException | ComponentBusinessException e) {
            log.error("ResultAspect#around.ComponentBusinessException", e);

            return Response.failure("-1", e.getMessage());
        } catch (ConstraintViolationException e) {
            log.error("ResultAspect#around.Exception", e);

            Set<ConstraintViolation<?>> set = e.getConstraintViolations();
            for (ConstraintViolation<?> violation : set) {
                return Response.failure("-1", violation.getMessage());
            }

            return Response.failure("-1", "操作异常");
        } catch (Exception e) {
            log.error("ResultAspect#around.Exception", e);

            return Response.failure("-1", "操作异常");
        }
    }
}

 单元测试

public class FlowProviderTest extends BootBaseTest {

    @Resource
    private FlowProvider flowProvider;

    @Test
    public void testWithdraw(){
        FlowWithdrawReq flowWithdrawReq = new FlowWithdrawReq();

        Response response =  flowProvider.withdraw(flowWithdrawReq);
        System.out.println(JSON.toJSONString(response));
    }
}

程序输出:{"code":-1,"message":"操作人不能为空"} 

   嵌套类的验证实例定义

@Data
public class User {
    @NotBlank(message = "name不能为空")
    private String name;

    @Min(value = 0,message = "age最小值为0")
    private int age;

    @Valid
    @NotNull(message = "地址不能为空")
    private List<AddressInfo> addressInfoList;

    @Data
    public static class AddressInfo{
        @NotBlank(message = "city不能为空")
        private String city;

        @NotBlank(message = "address不能为空")
        private String address;
    }
}

Validation实现原理

 spring-boot-autoconfigure的配置

  文件META-INF/spring-factories 里面配置文件 

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-autoconfigure</artifactId>
  <version>2.4.3</version>
</dependency>
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\

 ValidationAutoConfiguration类定义

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({ExecutableValidator.class})
@ConditionalOnResource(
    resources = {"classpath:META-INF/services/javax.validation.spi.ValidationProvider"}
)
@Import({PrimaryDefaultValidatorPostProcessor.class})
public class ValidationAutoConfiguration {
    public ValidationAutoConfiguration() {
    }

    @Bean
    @Role(2)
    @ConditionalOnMissingBean({Validator.class})
    public static LocalValidatorFactoryBean defaultValidator() {
        LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
        MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
        factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
        return factoryBean;
    }

    @Bean
    @ConditionalOnMissingBean
    public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment, @Lazy Validator validator, ObjectProvider<MethodValidationExcludeFilter> excludeFilters) {
        FilteredMethodValidationPostProcessor processor = new FilteredMethodValidationPostProcessor(excludeFilters.orderedStream());
        boolean proxyTargetClass = (Boolean)environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
        processor.setProxyTargetClass(proxyTargetClass);
        processor.setValidator(validator);
        return processor;
    }
}

 MethodValidationPostProcessor 类的层次结构

 BeanPostProcessor 

该接口我们也叫后置处理器,作用是在Bean对象在实例化和依赖注入完毕后,在显示调用初始化方法的前后添加我们自己的逻辑。注意是Bean实例化完毕后及依赖注入完成后触发的。接口的源码如下

public interface BeanPostProcessor {
    @Nullable
    default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }

    @Nullable
    default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
}
方法描述
postProcessBeforeInitialization实例化、依赖注入完毕,
在调用显示的初始化之前完成一些定制的初始化任务
postProcessAfterInitialization实例化、依赖注入、初始化完毕时执行

 MethodValidationPostProcessor

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
    private Class<? extends Annotation> validatedAnnotationType = Validated.class;
    @Nullable
    private Validator validator;

    public MethodValidationPostProcessor() {
    }

    public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
        Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
        this.validatedAnnotationType = validatedAnnotationType;
    }

    public void setValidator(Validator validator) {
        if (validator instanceof LocalValidatorFactoryBean) {
            this.validator = ((LocalValidatorFactoryBean)validator).getValidator();
        } else if (validator instanceof SpringValidatorAdapter) {
            this.validator = (Validator)validator.unwrap(Validator.class);
        } else {
            this.validator = validator;
        }

    }

    public void setValidatorFactory(ValidatorFactory validatorFactory) {
        this.validator = validatorFactory.getValidator();
    }

    public void afterPropertiesSet() {
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        this.advisor = new DefaultPointcutAdvisor(pointcut, this.createMethodValidationAdvice(this.validator));
    }

    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor();
    }
}

 MethodValidationInterceptor

public class MethodValidationInterceptor implements MethodInterceptor {
    private final Validator validator;

    public MethodValidationInterceptor() {
        this(Validation.buildDefaultValidatorFactory());
    }

    public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
        this(validatorFactory.getValidator());
    }

    public MethodValidationInterceptor(Validator validator) {
        this.validator = validator;
    }

    @Nullable
    public Object invoke(MethodInvocation invocation) throws Throwable {
        if (this.isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        } else {
            Class<?>[] groups = this.determineValidationGroups(invocation);
            ExecutableValidator execVal = this.validator.forExecutables();
            Method methodToValidate = invocation.getMethod();
            Object target = invocation.getThis();
            Assert.state(target != null, "Target must not be null");

            Set result;
            try {
                result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
            } catch (IllegalArgumentException var8) {
                methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), target.getClass()));
                result = execVal.validateParameters(target, methodToValidate, invocation.getArguments(), groups);
            }

            if (!result.isEmpty()) {
                throw new ConstraintViolationException(result);
            } else {
                Object returnValue = invocation.proceed();
                result = execVal.validateReturnValue(target, methodToValidate, returnValue, groups);
                if (!result.isEmpty()) {
                    throw new ConstraintViolationException(result);
                } else {
                    return returnValue;
                }
            }
        }
    }

    private boolean isFactoryBeanMetadataMethod(Method method) {
        Class<?> clazz = method.getDeclaringClass();
        if (clazz.isInterface()) {
            return (clazz == FactoryBean.class || clazz == SmartFactoryBean.class) && !method.getName().equals("getObject");
        } else {
            Class<?> factoryBeanType = null;
            if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
                factoryBeanType = SmartFactoryBean.class;
            } else if (FactoryBean.class.isAssignableFrom(clazz)) {
                factoryBeanType = FactoryBean.class;
            }

            return factoryBeanType != null && !method.getName().equals("getObject") && ClassUtils.hasMethod(factoryBeanType, method);
        }
    }

    protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
        Validated validatedAnn = (Validated)AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
        if (validatedAnn == null) {
            Object target = invocation.getThis();
            Assert.state(target != null, "Target must not be null");
            validatedAnn = (Validated)AnnotationUtils.findAnnotation(target.getClass(), Validated.class);
        }

        return validatedAnn != null ? validatedAnn.value() : new Class[0];
    }
}

 Validated 注解类定义

@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
    Class<?>[] value() default {};
}

验证类分析

核心的maven

       <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>    
    <dependency>
      <groupId>org.hibernate.validator</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>6.1.7.Final</version>
      <scope>compile</scope>
    </dependency>

NotBlankValidator实现类

package org.hibernate.validator.internal.constraintvalidators.bv;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.constraints.NotBlank;

public class NotBlankValidator implements ConstraintValidator<NotBlank, CharSequence> {
    public NotBlankValidator() {
    }

    public boolean isValid(CharSequence charSequence, ConstraintValidatorContext constraintValidatorContext) {
        if (charSequence == null) {
            return false;
        } else {
            return charSequence.toString().trim().length() > 0;
        }
    }
}

常用验证注解

注解描述
@Email邮箱验证
@Max被注释的元素必须是一个数字,其值必须小于等于指定的最大值
@Min被注释的元素必须是一个数字,其值必须大于等于指定的最小值
@Negative带注释的元素必须是一个严格的负数(0为无效值)
@NegativeOrZero带注释的元素必须是一个严格的负数(包含0)
@NotBlank同StringUtils.isNotBlank
@NotEmpty同StringUtils.isNotEmpty
@NotNull不能是Null
@Szie带注释的元素大小必须介于指定边界(包括)之间

参考:

解决@Validated注解无效,嵌套对象属性的@NotBlank无效问题 - 路饭网

SpringBoot使用Validation校验参数_JustryDeng-CSDN博客_validation

你还在手动校验参数吗 · 中台开发技术规范要点 · 看云

SpringBoot validator 完美实现+统一封装错误提示_小单的博客专栏-CSDN博客_springboot validator

【已解决】javax.validation.UnexpectedTypeException: HV000030: No validator could be found for constraint - 沧海一滴 - 博客园

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值