对于研发人员来说,想要写出鲁棒性高的代码,稳定性强的代码,对于参数的校验是必不可少的。防御式编程就是这样一种思想,对用户输入的参数持怀疑态度,尽可能的考虑使用场景,对输入进行校验,这样可以在某些恶意请求,或者异常访问场景下减少系统宕机的可能

觉得有收获的话可以关注微信公众号:程序员HopeX

🍒线上案例

一个平平无常的夜晚,大家准备下班的时候,突然系统出现大量rpc调用超时的异常告警,看监控面板,某个服务出现大量的fullgc,查询日志和对应的服务调用链,发现存在慢sql,且有大量结果集的返回;从慢sql监控日志里面看到有个sql执行时间最大快要达到200多秒,平均执行时间44多秒;发现是执行的查询sql没有带任何查询参数,直接全表扫描,且查询的是一张大表,返回结果700多万行;于是导致查询的服务短时间内频繁fullgc,并因此形成连锁反应,牵一发而动全身,依赖该该服务的其他服务全都不可用,蝴蝶效应引起核心服务的不可用

fullgc

xvvBon.png

慢sql监控

xvv4oR.png
xvvhw9.png

很多血淋淋的案例都表明参数校验的重要性,如果有提前做好参数校验,查询的时候没有全表查询,全表结果返回,就不会有这次线上事故了;如果有做服务降级,如果一个服务不可用,可以针对该服务进行降级,保证核心服务可用,也不会导致系统的全面崩溃,服务降级是后话,本次主要分享一些常见的参数校验;避免因为一个马掌钉损失了一匹战马,进而损失一个军队,一场战争,一个国家...

🍓参数校验

参数校验分为Controller层入参和Service层校验。Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303 规范,是标准 JSR-303 的一个变种),javax提供了@Valid(标准JSR-303规范),配合 BindingResult 可以直接提供参数验证结果。
springBoot参数校验是基于Validator来做的,首先需要添加hibernate-validator和validation-api依赖,由于spring-boot-starter-web依赖hibernate-validator,而hibernate-validate又依赖validation-api,所以项目中只需要添加spring-boot-starter-web依赖即可
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-web</artifactId>
                    <version>${spring.boot.starter.version}</version>
                </dependency>

🍇controller层入参校验

在检验 Controller 的入参是否符合规范时,使用 @Validated 或者 @Valid 在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
  • 分组校验
    @Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制。
    @Valid:作为标准JSR-303规范,还不支持分组的功能。
  • 嵌套验证
    由于@Validated不能用在成员属性(字段)上,但是@Valid能加在成员属性(字段)上,而且@Valid类注解上也说明了它支持嵌套验证功能,那么我们能够推断出:@Valid加在方法参数时并不能够自动进行嵌套验证,而是用在需要嵌套验证类的相应字段上,来配合方法参数上@Validated或@Valid来进行嵌套验证。
🍟@Validated分组校验
    @PostMapping({"/notPass", "batchRepulse"})
        public ResponseMap batchRepulse(HttpServletRequest request,@Validated(BatchRepulseGroup.class)@RequestBody CommentReq req) {
            log.info("打回重传请求参数:{}", JSON.toJSONString(req));
            Long employeeId = Long.valueOf(request.getHeader("employeeId"));
            paganiService.batchRepulse(req.getCommentIds(), req.getCancelReason(), employeeId);
            return ResponseMap.successResponse();
        }
校验分组

/**
 * <h3>批量点评分组
 *
 * @author <a>stephenXu</a>
 **/
public interface BatchCommentGroup {
}
/**
 * <h3>批量点评打回分组
 *
 * @author <a>stephenXu</a>
 **/
public interface BatchRepulseGroup {
}
/**
 * <h3>单个点评分组
 *
 * @author <a>stephenXu</a>
 **/
public interface SingleCommentGroup {
}
校验参数
@Data
public class CommentReq implements Serializable {

    private static final long serialVersionUID = 42L;
    /**
     * 点评id
     */
    @NotNull(message = "commentId must not be null!", groups = SingleCommentGroup.class)
    private Integer commentId;
    /**
     * 批量点评id集合
     */
    @NotEmpty(message = "commentIds must not be empty!", groups = {BatchCommentGroup.class, BatchRepulseGroup.class})
    private List<Integer> commentIds;
    /**
     * 批量打回的理由
     * @see rpc.api.domain.enums.review.CommentCancelReason
     */
    @NotNull(message = "cancelReason must not be null!", groups = BatchRepulseGroup.class)
    private Integer cancelReason;
    /**
     * 点评文本内容
     */
    private String content;
    /**
     * 点评语音
     */
    private List<CommentContentReq> contents;
}
🍞@Valid校验
     @GetMapping("list")
        public SimpleResponse<S3CommentListDetailDTO> list(@Valid S3VoiceCommentListReq req) {
            //组装请求参数
            S3CommentListReq listReq = S3CommentListReq.builder().build();
            BeanUtils.copyProperties(req, listReq);
            IRpcResult<S3CommentListDetailDTO> dto = remoteClassUserReviewService
                .getS3CommentListInfo(listReq);
            if (!dto.checkSuccess() || dto.getData() == null) {
                return SimpleResponse.failResponse(dto.getMessage());
            }
            return SimpleResponse.successResponse(dto.getData());
        }
校验参数
    @Data
    @NoArgsConstructor
    public class S3VoiceCommentListReq implements Serializable {

        private static final long serialVersionUID = 42L;
        private static final Integer DEFAULT_PAGE_NO = 1;
        private static final Integer DEFAULT_PAGE_SIZE = 10;
        private static final Integer MAX_PAGE_SIZE = 40;
        /**
         * 班级id
         */
        @NotNull(message = "classId must not be null!")
        private Integer classId;
        /**
         * 老师id
         */
        @NotNull(message = "teacherId must not be null!")
        private Integer teacherId;

        /**
         * 用户id
         */
        private Integer userId;

        /**
         * 课程key
         */
        @NotBlank(message = "courseKey must not be blank!")
        private String courseKey;
        /**
         * 页码
         */
        private Integer pageNum;
        /**
         * 每页大小
         */
        private Integer pageSize;

        /**
         * 点评状态 0 未点评
         */
        private Integer commentStatus;
        /**
         * 是否发布
         */
        private Boolean isPublish;
        /**
         * 课时key
         */
        private String lessonKey;

        /**
         * 开始上传日期
         */
        private Date uploadDateStart;
        /**
         * 结束上传日期
         */
        private Date uploadDateEnd;

        public void setUploadDateStart(Long uploadDateStart) {
            if (uploadDateStart != null) {
                this.uploadDateStart = new Date(uploadDateStart);
            }
        }

        public void setUploadDateEnd(Long uploadDateEnd) {
            if (uploadDateEnd != null) {
                this.uploadDateEnd = new Date(uploadDateEnd);
            }
        }

        public void setPageNum(Integer pageNum) {
            if (pageNum == null || pageNum < DEFAULT_PAGE_NO) {
                this.pageNum = DEFAULT_PAGE_NO;
                return;
            }
            this.pageNum = pageNum;
        }


        public void setPageSize(Integer pageSize) {
            if (pageSize == null || pageSize < DEFAULT_PAGE_SIZE) {
                this.pageSize = DEFAULT_PAGE_SIZE;
                return;
            }
            if (pageSize > MAX_PAGE_SIZE) {
                this.pageSize = MAX_PAGE_SIZE;
                return;
            }
            this.pageSize = pageSize;
        }
    }

参见

🍦service层入参校验

service层校验如果有接口需要在接口里面标注需要校验的参数,且实现类里面需要使用@Validated注解标识,让spring的参数校验拦截器MethodValidationInterceptor生效
  • 接口

public interface IUserCommentService {
....
    /**
     * 打回用户作业列表
     *
     * @param condition 点评条件
     * @return List<CancelCommentDTO>
     */
    IRpcResult<PageInfoCommentDTO<RepulseCommentDTO>> getRepulseInfo(CommentCondition condition);

    /**
     * 删除用户上传的作业
     *
     * @param homeworkDTO 上传作业相关信息
     * @return 删除结果
     */
    IRpcResult<Boolean> deleteUserUploadHomework(@Valid HomeworkDTO homeworkDTO);
}   
  • 实现类上要标明@Validated注解,这样Spring默认的参数校验拦截器才会在Aop里面对参数校验进行处理
    @Service
    @Slf4j
    @Validated
    public class UserCommentServiceImpl implements IUserCommentService {
    ...
    @Override
     public IRpcResult<Boolean> deleteUserUploadHomework(HomeworkDTO homeworkDTO) {
         LessonStepByTypeReq req = new LessonStepByTypeReq();
         req.setLessonId(homeworkDTO.getLessonId());
         req.setType(homeworkDTO.getStepType().intValue());
         LessonStepDto lessonStep = lessonStepOuterService.lessonStepByType(req);
         HomeworkMsg uploadHomework = HomeworkMsg.builder()
                 .userId(homeworkDTO.getUserId())
                 .classId(homeworkDTO.getClassId())
                 .lessonId(homeworkDTO.getLessonId())
                 .teacherId(commentBizService.getTeacherId(homeworkDTO.getUserId().intValue()
                         , homeworkDTO.getClassId().intValue()))
                 .stepId(Opt.map(lessonStep, LessonStepDto::getId))
                 .stepType(homeworkDTO.getStepType())
                 .courseId(null)
                 .commentType(null)
                 .deleteTime(new Date())
                 .build();
         Assert.notNull(kafKaService, "kafKaService must not be null!");
         kafKaService.sendHomeworkDeleteMsg(uploadHomework);
         return DefaultRpcResult.successWithData(Boolean.TRUE);
     }
    }
springBoot参数校验原理:AOP是关键
Controller层是入参校验PayloadMethodArgumentResolver,ModelAttributeMethodProcessor参数解析器处理的
Service层是在方法校验拦截器MethodValidationInterceptor切面拦截处理的
    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(ExecutableValidator.class)
    @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider")
    @Import(PrimaryDefaultValidatorPostProcessor.class)
    public class ValidationAutoConfiguration {

        @Bean
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        @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) {
            MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
            boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
            processor.setProxyTargetClass(proxyTargetClass);
            processor.setValidator(validator);
            return processor;
        }

    }

MethodValidationPostProcessor后置处理器里面最终采用MethodValidationInterceptor切面拦截处理

    public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
            implements InitializingBean {

        private Class<? extends Annotation> validatedAnnotationType = Validated.class;

        @Nullable
        private Validator validator;


        /**
         * Set the 'validated' annotation type.
         * The default validated annotation type is the {@link Validated} annotation.
         * <p>This setter property exists so that developers can provide their own
         * (non-Spring-specific) annotation type to indicate that a class is supposed
         * to be validated in the sense of applying method validation.
         * @param validatedAnnotationType the desired annotation type
         */
        public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
            Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
            this.validatedAnnotationType = validatedAnnotationType;
        }

        /**
         * Set the JSR-303 Validator to delegate to for validating methods.
         * <p>Default is the default ValidatorFactory's default Validator.
         */
        public void setValidator(Validator validator) {
            // Unwrap to the native Validator with forExecutables support
            if (validator instanceof LocalValidatorFactoryBean) {
                this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
            }
            else if (validator instanceof SpringValidatorAdapter) {
                this.validator = validator.unwrap(Validator.class);
            }
            else {
                this.validator = validator;
            }
        }

        /**
         * Set the JSR-303 ValidatorFactory to delegate to for validating methods,
         * using its default Validator.
         * <p>Default is the default ValidatorFactory's default Validator.
         * @see javax.validation.ValidatorFactory#getValidator()
         */
        public void setValidatorFactory(ValidatorFactory validatorFactory) {
            this.validator = validatorFactory.getValidator();
        }


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

        /**
         * Create AOP advice for method validation purposes, to be applied
         * with a pointcut for the specified 'validated' annotation.
         * @param validator the JSR-303 Validator to delegate to
         * @return the interceptor to use (typically, but not necessarily,
         * a {@link MethodValidationInterceptor} or subclass thereof)
         * @since 4.2
         */
        protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
            return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
        }

    }

🍭MethodValidationInterceptor

    public class MethodValidationInterceptor implements MethodInterceptor {

        private final Validator validator;


        /**
         * Create a new MethodValidationInterceptor using a default JSR-303 validator underneath.
         */
        public MethodValidationInterceptor() {
            this(Validation.buildDefaultValidatorFactory());
        }

        /**
         * Create a new MethodValidationInterceptor using the given JSR-303 ValidatorFactory.
         * @param validatorFactory the JSR-303 ValidatorFactory to use
         */
        public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
            this(validatorFactory.getValidator());
        }

        /**
         * Create a new MethodValidationInterceptor using the given JSR-303 Validator.
         * @param validator the JSR-303 Validator to use
         */
        public MethodValidationInterceptor(Validator validator) {
            this.validator = validator;
        }


        @Override
        @SuppressWarnings("unchecked")
        public Object invoke(MethodInvocation invocation) throws Throwable {
            // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
            if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
                return invocation.proceed();
            }

            Class<?>[] groups = determineValidationGroups(invocation);

            // Standard Bean Validation 1.1 API
            ExecutableValidator execVal = this.validator.forExecutables();
            Method methodToValidate = invocation.getMethod();
            Set<ConstraintViolation<Object>> result;

            try {
                result = execVal.validateParameters(
                        invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
            }
            catch (IllegalArgumentException ex) {
                // Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
                // Let's try to find the bridged method on the implementation class...
                methodToValidate = BridgeMethodResolver.findBridgedMethod(
                        ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
                result = execVal.validateParameters(
                        invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
            }
            if (!result.isEmpty()) {
                throw new ConstraintViolationException(result);
            }

            Object returnValue = invocation.proceed();

            result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
            if (!result.isEmpty()) {
                throw new ConstraintViolationException(result);
            }

            return returnValue;
        }

🍰总结

参数校验非常重要,但参数校验可以不用if-else这种冗长的代码来做,可以交给sprign来处理,使用AOP切面编程,将参数校验和业务逻辑隔离,更优雅,更高效!

觉得有收获的话可以关注微信公众号:程序员HopeX


BraveHope
7 声望0 粉丝