Spring Boot 中使用自定义 Validator 校验两个参数的大小关系

  1. 创建一个自定义的注解 @RangeCompare

    import javax.validation.Constraint;
    import javax.validation.Payload;
    import java.lang.annotation.*;
    
    @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Constraint(validatedBy = {RangeCompareValidator.class})
    @Documented
    public @interface RangeCompare {
    
        String message() default "起始值必须小于或等于结束值";
    
        Class<?>[] groups() default {};
    
        Class<? extends Payload>[] payload() default {};
    
        String from();
    
        String to();
    
        @Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        public @interface List {
            RangeCompare[] value();
        }
    }
    

    其中最重要的是 @Constraint 注解,用于指示该注解包含哪些验证逻辑。

    @Constraint 注解的源码:

    @Documented
    @Target({ ANNOTATION_TYPE })
    @Retention(RUNTIME)
    public @interface Constraint {
        Class<? extends ConstraintValidator<?, ?>>[] validatedBy();
    }
    
  2. 创建 @Constraint 注解中指定的约束验证器 RangeCompareValidator

    import org.springframework.beans.BeanWrapper;
    import org.springframework.beans.BeanWrapperImpl;
    
    import javax.validation.ConstraintValidator;
    import javax.validation.ConstraintValidatorContext;
    import java.time.LocalDate;
    import java.time.LocalDateTime;
    import java.time.LocalTime;
    
    public class RangeCompareValidator implements ConstraintValidator<RangeCompare, Object> {
    
        private String from;
        private String to;
    
        @Override
        public void initialize(RangeCompare constraint) {
            from = constraint.from();
            to = constraint.to();
        }
    
        @Override
        public boolean isValid(Object value, ConstraintValidatorContext context) {
    
            BeanWrapper beanWrapper = new BeanWrapperImpl(value);
            Object fromValue = beanWrapper.getPropertyValue(from);
            Object toValue = beanWrapper.getPropertyValue(to);
    
            if (fromValue == null || toValue == null) {
                return true;
            }
    
            if (fromValue instanceof Number && toValue instanceof Number) {
                return ((Number) fromValue).doubleValue() <= ((Number) toValue).doubleValue();
            }
    
            if (fromValue instanceof LocalDate && toValue instanceof LocalDate) {
                return !((LocalDate) fromValue).isAfter(((LocalDate) toValue));
            }
    
            if (fromValue instanceof LocalDateTime && toValue instanceof LocalDateTime) {
                return !((LocalDateTime) fromValue).isAfter(((LocalDateTime) toValue));
            }
    
            if (fromValue instanceof LocalTime && toValue instanceof LocalTime) {
                return !((LocalTime) fromValue).isAfter(((LocalTime) toValue));
            }
    
            throw new IllegalArgumentException("只支持数字或日期类型的比较");
        }
    }
    

    ConstraintValidator 接口定义如下:

    package javax.validation;
    
    import java.lang.annotation.Annotation;
    
    import javax.validation.constraintvalidation.SupportedValidationTarget;
    
    public interface ConstraintValidator<A extends Annotation, T> {
    
        default void initialize(A constraintAnnotation) {}
    
        boolean isValid(T value, ConstraintValidatorContext context);
    }
    

    ConstraintValidator 接口支持两个泛型参数:

    • A extends Annotation : 验证对应的注解类型
    • T :验证的目标类型
  3. 添加全局的异常处理器

    参数验证不通过时会被 @ExceptionHandler(MethodArgumentNotValidException.class) 所捕获,这样就不必在每个接口中处理验证结果了。

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.context.support.DefaultMessageSourceResolvable;
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.MethodArgumentNotValidException;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.ResponseStatus;
    
    import java.util.stream.Collectors;
    import java.util.stream.Stream;
    
    @ControllerAdvice
    @Slf4j
    public class GlobalExceptionAdvice {
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        @ResponseStatus(HttpStatus.OK)
        @ResponseBody
        public ResponseInfo<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
            String errors = Stream.concat(
                            ex.getBindingResult().getFieldErrors().stream().map(m -> m.getField() + ":" + m.getDefaultMessage()),
                            ex.getBindingResult().getGlobalErrors().stream().map(DefaultMessageSourceResolvable::getDefaultMessage))
                    .collect(Collectors.joining(","));
            log.warn("参数错误[{}]", errors);
            return ResponseInfo.buildError(errors);
        }
    }
    
  4. 参数类上添加 @RangeCompare 注解

    import io.swagger.annotations.ApiModelProperty;
    import lombok.Data;
    
    import java.time.LocalDate;
    
    @Data
    @RangeCompare(from = "fromDate", to = "toDate", message = "起始日期必须小于或等于结束日期")
    public class TimeRangeParam  {
    
        @ApiModelProperty("起始日期")
        private LocalDate fromDate;
    
        @ApiModelProperty("结束日期")
        private LocalDate toDate;
    
    }
    
  5. 接口参数上添加 @Validated 注解

    @PostMapping(value = "search")
    public ResponseInfo<Object> search(@Validated @RequestBody TimeRangeParam req) {
        // do something
    }
    
由 max-http-header-size 引起的 OOM

在对服务做压力测试时发现内存消耗非常大,而且一旦开始压测,内存消耗增速非常快,很快就 OOM 了。这个项目因为之前一直请求量很少,所以没有出过问题。经老板提示说之前另一个项目也遇到过类似的问题,当时是由于一个请求头部的配置设置过大导致的。

看了下服务的配置文件,确实有一个 server.max-http-header-size 的配置被设置为了 40485760 (大约是 38.6MB),而在 Spring Boot 中这个配置默认为 8KB。(不清楚为什么要设置这么大,对于 Header 来说 8KB 完全够用了)

Spring Boot:自定义模块的自动装配

添加组件扫描的配置类:

package me.liujiajia.spring.boot.auto.configuration.sample.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * @author 佳佳
 */
@Configuration
@ComponentScan(basePackages = {"me.liujiajia.spring.boot.auto.configuration.sample"})
public class MyAutoConfiguration {

}