Лучшая практика Spring Validation и принцип ее реализации, проверка параметров не так проста!

Spring

Я уже писал оSpring ValidationСтатья б/у, но самоощущение пока поверхностное, в этот раз планирую полностью разобратьсяSpring Validation. В этой статье будет подробно рассказаноSpring ValidationЛучшие практики и принципы их реализации в различных сценариях, умирайте до конца! Исходный код проекта:spring-validation

Простой в использовании

Java APIСпецификация(JSR303)ОпределенныйBeanэталон калибровкиvalidation-api, но реализация не предусмотрена.hibernate validationявляется реализацией этой спецификации и добавляет аннотации проверки, такие как@Email,@LengthЖдать.Spring Validationправдаhibernate validationдополнительный пакет для поддержкиspring mvcПараметры проверяются автоматически. Далее мы используемspring-bootПроект как пример, введениеSpring Validationиспользование.

импортировать зависимости

еслиspring-bootверсия меньше чем2.3.x,spring-boot-starter-webавтоматически войдетhibernate-validatorполагаться. еслиspring-bootверсия больше, чем2.3.x, вам нужно вручную ввести зависимости:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.1.Final</version>
</dependency>

заwebДля услуг, чтобы предотвратить влияние незаконных параметров на бизнес,ControllerСлой должен выполнить проверку параметров! В большинстве случаев параметры запроса делятся на следующие две формы:

  1. POST,PUTзапросить, использоватьrequestBodyпараметры передачи;
  2. GETзапросить, использоватьrequestParam/PathVariableПередать параметры.

Ниже мы кратко представимrequestBodyиrequestParam/PathVariableПараметр проверки реального боя!

requestBodyпроверка параметров

POST,PUTзапрос обычно используетrequestBodyПередайте параметры, в этом случае бэкэнд используетобъект DTOполучить.Просто добавьте объект DTO@ValidatedАннотация может реализовать автоматическую проверку параметров. Например, есть сохранениеUserинтерфейс, требуетсяuserNameдлина2-10,accountиpasswordдлина поля6-20. Если проверка не удалась, выброситьMethodArgumentNotValidExceptionаномальный,SpringПо умолчанию он будет преобразован в400(Bad Request)просить.

DTO представляет собой объект передачи данных (Data Transfer Object), который используется для интерактивной передачи между сервером и клиентом.. В проекте spring-web можно представить метод, используемый для получения параметров запросаBeanобъект.

  • существуетDTOОбъявите аннотации ограничений для полей
@Data
public class UserDTO {

    private Long userId;

    @NotNull
    @Length(min = 2, max = 10)
    private String userName;

    @NotNull
    @Length(min = 6, max = 20)
    private String account;

    @NotNull
    @Length(min = 6, max = 20)
    private String password;
}

  • Объявить аннотации проверки для параметров метода
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

В этой ситуации,использовать@Validи@Validatedвсе будет хорошо.

requestParam/PathVariableпроверка параметров

GETзапрос обычно используетrequestParam/PathVariableПередать параметры. Если параметров много (например, более 6), рекомендуется использоватьDTOОбъект получает. В противном случае рекомендуется размещать параметры один за другим в параметрах метода. при этих обстоятельствах,Должен бытьControllerярлык на классе@Validatedаннотации и объявить аннотации ограничений для входных параметров (например,@MinЖдать). Если проверка не удалась, выброситьConstraintViolationExceptionаномальный. Пример кода выглядит следующим образом:

@RequestMapping("/api/user")
@RestController
@Validated
public class UserController {
    // 路径变量
    @GetMapping("{userId}")
    public Result detail(@PathVariable("userId") @Min(10000000000000000L) Long userId) {
        // 校验通过,才会执行业务逻辑处理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(userId);
        userDTO.setAccount("11111111111111111");
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }

    // 查询参数
    @GetMapping("getByAccount")
    public Result getByAccount(@Length(min = 6, max = 20) @NotNull String  account) {
        // 校验通过,才会执行业务逻辑处理
        UserDTO userDTO = new UserDTO();
        userDTO.setUserId(10000000000000003L);
        userDTO.setAccount(account);
        userDTO.setUserName("xixi");
        userDTO.setAccount("11111111111111111");
        return Result.ok(userDTO);
    }
}

Унифицированная обработка исключений

Как упоминалось ранее, если проверка не пройдена,MethodArgumentNotValidExceptionилиConstraintViolationExceptionаномальный. В реальной разработке проекта обычно используетсяУнифицированная обработка исключенийчтобы вернуть более дружественную подсказку. Например, наша система требует, чтобы независимо от того, какое исключение было отправлено,httpКод состояния должен быть возвращен200служебный код используется для определения нештатной ситуации в системе.

@RestControllerAdvice
public class CommonExceptionHandler {

    @ExceptionHandler({MethodArgumentNotValidException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        BindingResult bindingResult = ex.getBindingResult();
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(", ");
        }
        String msg = sb.toString();
       return Result.fail(BusinessCode.参数校验失败, msg);
    }

    @ExceptionHandler({ConstraintViolationException.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public Result handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.fail(BusinessCode.参数校验失败, ex.getMessage());
    }
}

Расширенное использование

Проверка пакетов

В реальных проектах может потребоваться несколько методов для использования одного и того жеDTOкласс для получения параметров, и правила проверки разных методов, вероятно, будут разными. В это время простоDTOДобавление аннотаций ограничений к полям класса не решает эту проблему. следовательно,spring-validationподдерживаетсяПроверка пакетовФункция предназначена для решения такого рода проблем. Или приведенный выше пример, такой как сохранениеUserкогда,UserIdобнуляемый, но обновляетUserкогда,UserIdЗначение должно>=10000000000000000L; Правила проверки для других полей одинаковы в обоих случаях. использовать это времяПроверка пакетовПример кода выглядит следующим образом:

  • Применимая информация о группировке объявляется в аннотации ограничения.groups
@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}
  • @ValidatedУкажите контрольную группу на аннотации
@PostMapping("/save")
public Result saveUser(@RequestBody @Validated(UserDTO.Save.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

@PostMapping("/update")
public Result updateUser(@RequestBody @Validated(UserDTO.Update.class) UserDTO userDTO) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

Вложенные проверки

В предыдущем примереDTOПоля в классе基本数据类型иStringтип. Но в реальной сцене возможно, что поле также является объектом, в этом случае вы можете использовать嵌套校验. Например, сохраните вышеUserинформация вместе сJobИнформация. должны знать о том,В настоящее времяDTOСоответствующее поле класса должно быть отмечено@Validаннотация.

@Data
public class UserDTO {

    @Min(value = 10000000000000000L, groups = Update.class)
    private Long userId;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 2, max = 10, groups = {Save.class, Update.class})
    private String userName;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String account;

    @NotNull(groups = {Save.class, Update.class})
    @Length(min = 6, max = 20, groups = {Save.class, Update.class})
    private String password;

    @NotNull(groups = {Save.class, Update.class})
    @Valid
    private Job job;

    @Data
    public static class Job {

        @Min(value = 1, groups = Update.class)
        private Long jobId;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String jobName;

        @NotNull(groups = {Save.class, Update.class})
        @Length(min = 2, max = 10, groups = {Save.class, Update.class})
        private String position;
    }

    /**
     * 保存的时候校验分组
     */
    public interface Save {
    }

    /**
     * 更新的时候校验分组
     */
    public interface Update {
    }
}

Вложенные проверки можно использовать в сочетании с групповыми проверками. И есть嵌套集合校验Каждый элемент в коллекции будет проверен, напримерList<Job>поле будетlistкаждый внутриJobОбъекты проверены.

установить чек

Если тело запроса передается напрямуюjsonМассив отдается фону, и есть надежда, что проверка параметров выполняется для каждого элемента в массиве. На данный момент, если мы напрямую используемjava.util.Collectionвнизlistилиsetдля получения данных проверка параметров не вступит в силу! Мы можем использовать пользовательскиеlistСбор для получения параметров:

  • УпаковкаListвведите и объявите@Validаннотация
public class ValidationList<E> implements List<E> {

    @Delegate // @Delegate是lombok注解
    @Valid // 一定要加@Valid注解
    public List<E> list = new ArrayList<>();

    // 一定要记得重写toString方法
    @Override
    public String toString() {
        return list.toString();
    }
}

@DelegateАннотации подлежатlombokограничения версии,1.18.6Вышеупомянутые версии поддерживаются. Если проверка не удалась, он выдастNotReadablePropertyException, которые также можно обрабатывать с помощью унифицированных исключений.

Например, нам нужно сохранить несколькоUserобъект,ControllerМетод слоя можно записать так:

@PostMapping("/saveList")
public Result saveList(@RequestBody @Validated(UserDTO.Save.class) ValidationList<UserDTO> userList) {
    // 校验通过,才会执行业务逻辑处理
    return Result.ok();
}

таможенный чек

Бизнес-требования всегда сложнее, чем простые проверки, предоставляемые платформой, и мы можем настроить проверки в соответствии со своими потребностями. настроитьspring validationОчень просто, если мы настраиваем加密id(по номерам илиa-fсостоит из букв,32-256длина) проверки, которая в основном делится на два этапа:

  • Аннотации пользовательских ограничений
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {

    // 默认错误消息
    String message() default "加密id格式错误";

    // 分组
    Class<?>[] groups() default {};

    // 负载
    Class<? extends Payload>[] payload() default {};
}
  • выполнитьConstraintValidatorВалидатор ограничения записи интерфейса
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {

    private static final Pattern PATTERN = Pattern.compile("^[a-f\\d]{32,256}$");

    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 不为null才进行校验
        if (value != null) {
            Matcher matcher = PATTERN.matcher(value);
            return matcher.find();
        }
        return true;
    }
}

Таким образом, мы можем использовать@EncryptIdПроверка параметров выполнена!

Программная проверка

Приведенные выше примеры основаны на注解Чтобы добиться автоматической проверки, в некоторых случаях мы можем захотеть использовать编程方式Проверка звонка. можно вводить в это времяjavax.validation.Validatorобъект, а затем вызвать егоapi.

@Autowired
private javax.validation.Validator globalValidator;

// 编程式校验
@PostMapping("/saveWithCodingValidate")
public Result saveWithCodingValidate(@RequestBody UserDTO userDTO) {
    Set<ConstraintViolation<UserDTO>> validate = globalValidator.validate(userDTO, UserDTO.Save.class);
    // 如果校验通过,validate为空;否则,validate包含未校验通过项
    if (validate.isEmpty()) {
        // 校验通过,才会执行业务逻辑处理

    } else {
        for (ConstraintViolation<UserDTO> userDTOConstraintViolation : validate) {
            // 校验失败,做其它逻辑
            System.out.println(userDTOConstraintViolation);
        }
    }
    return Result.ok();
}

Быстрая ошибка

Spring ValidationПо умолчанию перед созданием исключения проверяются все поля. Его можно включить с помощью некоторой простой конфигурацииFali Fastрежим и немедленно возвращается, если проверка не удалась.

@Bean
public Validator validator() {
    ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
            .configure()
            // 快速失败模式
            .failFast(true)
            .buildValidatorFactory();
    return validatorFactory.getValidator();
}

@Validи@Validatedразница

разница @Valid @Validated
провайдер Спецификация JSR-303 Spring
Поддерживать ли группировку не поддерживается служба поддержки
Местоположение ярлыка METHOD, FIELD, CONSTRUCTOR, PARAMETER, TYPE_USE TYPE, METHOD, PARAMETER
Вложенные проверки служба поддержки не поддерживается

Принцип реализации

requestBodyПринцип проверки параметров

существуетspring-mvcсередина,RequestResponseBodyMethodProcessorдля разбора@RequestBodyАннотированные параметры и обработка@ResponseBodyАннотировать возвращаемое значение метода. Очевидно, что логика выполнения проверки параметров должна быть в методе разбора параметров.resolveArgument()середина:

public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
    @Override
    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

        parameter = parameter.nestedIfOptional();
        //将请求数据封装到DTO对象中
        Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
        String name = Conventions.getVariableNameForParameter(parameter);

        if (binderFactory != null) {
            WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
            if (arg != null) {
                // 执行数据校验
                validateIfApplicable(binder, parameter);
                if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
                    throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
                }
            }
            if (mavContainer != null) {
                mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
            }
        }
        return adaptArgumentIfNecessary(arg, parameter);
    }
}

можно увидеть,resolveArgument()называетсяvalidateIfApplicable()Выполните проверку параметров.

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
    // 获取参数注解,比如@RequestBody、@Valid、@Validated
    Annotation[] annotations = parameter.getParameterAnnotations();
    for (Annotation ann : annotations) {
        // 先尝试获取@Validated注解
        Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
        //如果直接标注了@Validated,那么直接开启校验。
        //如果没有,那么判断参数前是否有Valid起头的注解。
        if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
            Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
            Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
            //执行校验
            binder.validate(validationHints);
            break;
        }
    }
}

Увидев это, каждый должен понять, почему в этом сценарии@Validated,@ValidДве аннотации можно смешивать. продолжим смотретьWebDataBinder.validate()выполнить.

@Override
public void validate(Object target, Errors errors, Object... validationHints) {
    if (this.targetValidator != null) {
        processConstraintViolations(
            //此处调用Hibernate Validator执行真正的校验
            this.targetValidator.validate(target, asValidationGroups(validationHints)), errors);
    }
}

Наконец обнаружил, что нижний слой, наконец, называетсяHibernate ValidatorВыполните настоящую проверку.

Принцип реализации проверки параметров на уровне метода

Как упоминалось выше, параметры встраиваются в параметры метода один за другим, а затем объявляются перед каждым параметром.约束注解Метод проверки — это проверка параметра на уровне метода. На самом деле, этот метод можно использовать для любогоSpring Beanметод, такой какController/ServiceЖдать.В основе лежит принцип реализацииAOP, в частности,MethodValidationPostProcessorдинамическая регистрацияAOPнарезать, а затем использоватьMethodValidationInterceptorУлучшения плетения для методов pointcut.

public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessorimplements InitializingBean {
    @Override
    public void afterPropertiesSet() {
        //为所有`@Validated`标注的Bean创建切面
        Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
        //创建Advisor进行增强
        this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
    }

    //创建Advice,本质就是一个方法拦截器
    protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
        return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
    }
}

Тогда взглянитеMethodValidationInterceptor:

public class MethodValidationInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //无需增强的方法,直接跳过
        if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
            return invocation.proceed();
        }
        //获取分组信息
        Class<?>[] groups = determineValidationGroups(invocation);
        ExecutableValidator execVal = this.validator.forExecutables();
        Method methodToValidate = invocation.getMethod();
        Set<ConstraintViolation<Object>> result;
        try {
            //方法入参校验,最终还是委托给Hibernate Validator来校验
            result = execVal.validateParameters(
                invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
        }
        catch (IllegalArgumentException ex) {
            ...
        }
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        //真正的方法调用
        Object returnValue = invocation.proceed();
        //对返回值做校验,最终还是委托给Hibernate Validator来校验
        result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
        //有异常直接抛出
        if (!result.isEmpty()) {
            throw new ConstraintViolationException(result);
        }
        return returnValue;
    }
}

В самом деле, будь тоrequestBody参数校验все еще方法级别的校验, в конце концов звонитHibernate Validatorвыполнить проверку,Spring ValidationПросто сделайте слой упаковки.

Нелегко быть оригинальным. Если вы считаете, что статья написана хорошо, пожалуйста, поставьте лайк 👍 и поддержите ее~

Добро пожаловать в мой проект с открытым исходным кодом:Облегченная среда вызовов HTTP для SpringBoot