Используйте @Validated и @Valid с весенним валидатором

У меня есть Java-бин, который используется для отправки сообщений JSON в Spring @RestController, и у меня есть настройка проверки bean-компонента и она отлично работает, используя @Valid. Но я хочу перейти на Protobuf/Thrift и уйти от REST. Это внутренний API, и многие крупные компании отказались от REST внутри компании. На самом деле это означает, что я больше не контролирую объекты сообщений — они генерируются извне. Я больше не могу ставить на них аннотации.

Так что теперь моя проверка должна быть программной. Как мне это сделать? Я закодировал Validator, и он отлично работает. Но он не использует красивую аннотацию @Valid. Я должен сделать следующее:

@Service
public StuffEndpoint implements StuffThriftDef.Iface {

    @Autowired
    private MyValidator myValidator;

    public void things(MyMessage msg) throws BindException {
        BindingResult errors = new BeanPropertyBindingResult(msg, msg.getClass().getName());
        errors = myValidator.validate(msg);
        if (errors.hasErrors()) {
            throw new BindException(errors);
        } else {
            doRealWork();
        }
    }
}

Это воняет. Я должен сделать это в каждом методе. Теперь я могу поместить многое из этого в один метод, который выдает BindException, и это позволяет добавлять одну строку кода к каждому методу. Но это все еще не здорово.

Я хочу, чтобы это выглядело так:

@Service
@Validated
public StuffEndpoint implements StuffThriftDef.Iface {

    public void things(@Valid MyMessage msg) {
        doRealWork();
    }
}

И все равно получить тот же результат. Помните, мой bean-компонент не имеет аннотаций. И да, я знаю, что могу использовать аннотацию @InitBinder для метода. Но это работает только для веб-запросов.

Я не против внедрить правильный Validator в этот класс, но я бы предпочел, чтобы мой ValidatorFactory мог получить правильный на основе метода supports().

Это возможно? Есть ли способ настроить проверку bean-компонентов для фактического использования проверки Spring вместо этого? Должен ли я где-то угонять Аспект? Взломать LocalValidatorFactory или MethodValidationPostProcessor?

Спасибо.


person sbzoom    schedule 18.03.2016    source источник
comment
Итак, вам нужно, чтобы Spring Validator работал с проверкой JSR-303?   -  person Ken Bekov    schedule 18.03.2016
comment
Да. Это вещь? Или я лаю не на то дерево?   -  person sbzoom    schedule 18.03.2016
comment
Я никогда не думал об объединении этих двух подходов. Но это очень интересная идея. В любом случае, у них другая идеология. Bean Validation выдает исключение в случае ошибки, а Spring Validation собирает все ошибки в результат привязки. Это будет не просто.   -  person Ken Bekov    schedule 18.03.2016
comment
Кстати, в случае public void things(@Valid MyMessage msg) вам не нужно аннотировать MyMessage. Он будет передан в ConstraintValidator<> как значение. Вы можете написать свою собственную аннотацию к сообщениям. Но это означает, что вы будете обрабатывать исключение проверки за пределами вашего класса обслуживания.   -  person Ken Bekov    schedule 18.03.2016
comment
Возможно, это устарело... но если вы все ищете более простой подход, вы можете попробовать SpringValidatorAdapter, как описано здесь Как объединить класс JSR-303 и Spring Validator на сервисном уровне?   -  person Jose-Rdz    schedule 21.11.2017


Ответы (3)


Довольно сложно сочетать проверку Spring и ограничения JSR-303. И нет «готового к использованию» способа. Основное неудобство заключается в том, что проверка Spring использует BindingResult, а JSR-303 использует ConstraintValidatorContext в результате проверки.

Вы можете попробовать создать свой собственный механизм проверки, используя Spring AOP. Рассмотрим, что нам для этого нужно сделать. Прежде всего, объявите АОП-зависимости (если вы еще этого не сделали):

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.2.4.RELEASE</version>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjrt</artifactId>
   <version>1.8.8</version>
   <scope>runtime</scope>
</dependency>
<dependency>
   <groupId>org.aspectj</groupId>
   <artifactId>aspectjweaver</artifactId>
   <version>1.8.8</version>
</dependency>

Я использую Spring версии 4.2.4.RELEASE, но вы можете использовать свою собственную. AspectJ необходим для использования аннотации аспекта. Следующим шагом нам нужно создать простой реестр валидатора:

public class CustomValidatorRegistry {

    private List<Validator> validatorList = new ArrayList<>();

    public void addValidator(Validator validator){
        validatorList.add(validator);
    }

    public List<Validator> getValidatorsForObject(Object o) {
        List<Validator> result = new ArrayList<>();
        for(Validator validator : validatorList){
            if(validator.supports(o.getClass())){
                result.add(validator);
            }
        }
        return result;
    }
}

Как видите, это очень простой класс, который позволяет нам найти валидатор для объекта. Теперь давайте создадим аннотацию, которая будет помечать методы, которые необходимо проверить:

package com.mydomain.validation;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomValidation {
}

Поскольку стандартный класс BindingException не является RuntimeException, мы не можем использовать его в переопределенных методах. Это означает, что нам нужно определить собственное исключение:

public class CustomValidatorException extends RuntimeException {

    private BindingResult bindingResult;

    public CustomValidatorException(BindingResult bindingResult){
        this.bindingResult = bindingResult;
    }

    public BindingResult getBindingResult() {
        return bindingResult;
    }
}

Теперь мы готовы создать аспект, который выполнит большую часть работы. Аспект будет выполняться перед методами, помеченными аннотацией CustomValidation:

@Aspect
@Component
public class CustomValidatingAspect {

    @Autowired
    private CustomValidatorRegistry registry; //aspect will use our validator registry


    @Before(value = "execution(public * *(..)) && annotation(com.mydomain.validation.CustomValidation)")
    public void doBefore(JoinPoint point){
        Annotation[][] paramAnnotations  =
                ((MethodSignature)point.getSignature()).getMethod().getParameterAnnotations();
        for(int i=0; i<paramAnnotations.length; i++){
            for(Annotation annotation : paramAnnotations[i]){
                //checking for standard org.springframework.validation.annotation.Validated
                if(annotation.annotationType() == Validated.class){
                    Object arg = point.getArgs()[i];
                    if(arg==null) continue;
                    validate(arg);
                }
            }
        }
    }

    private void validate(Object arg) {
        List<Validator> validatorList = registry.getValidatorsForObject(arg);
        for(Validator validator : validatorList){
            BindingResult errors = new BeanPropertyBindingResult(arg, arg.getClass().getSimpleName());
            validator.validate(arg, errors);
            if(errors.hasErrors()){
                throw new CustomValidatorException(errors);
            }
        }
    }
}

execution(public * *(..)) && @annotation(com.springapp.mvc.validators.CustomValidation) означает, что этот аспект будет применяться к любым публичным методам bean-компонентов, которые помечены аннотацией @CustomValidation. Также обратите внимание, что для пометки проверенных параметров мы используем стандартную аннотацию org.springframework.validation.annotation.Validated. Но, конечно, мы могли бы сделать наш обычай. Я думаю, что другой код аспекта очень прост и не нуждается в комментариях. Дальнейший код примера валидатора:

public class PersonValidator implements Validator {
    @Override
    public boolean supports(Class<?> aClass) {
        return aClass==Person.class;
    }

    @Override
    public void validate(Object o, Errors errors) {
        Person person = (Person)o;
        if(person.getAge()<=0){
            errors.rejectValue("age", "Age is too small");
        }
    }
}

Теперь мы настроили конфигурацию и все готово к использованию:

@Configuration
@ComponentScan(basePackages = "com.mydomain")
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig{

    .....

    @Bean
    public CustomValidatorRegistry validatorRegistry(){
        CustomValidatorRegistry registry = new CustomValidatorRegistry();
        registry.addValidator(new PersonValidator());
        return registry;
    }    
}

Обратите внимание, что proxyTargetClass равно true, потому что мы будем использовать прокси класса cglib.


Пример целевого метода в классе обслуживания:

@Service
public class PersonService{

    @CustomValidation
    public void savePerson(@Validated Person person){        
       ....
    }

}

Из-за @CustomValidation будет применен аспект аннотации, а из-за @Validated аннотация person будет проверена. И пример использования сервиса в контроллере (или любом другом классе):

@Controller
public class PersonConroller{

    @Autowired
    private PersonService service;

    public String savePerson(@ModelAttribute Person person, ModelMap model){
        try{
            service.savePerson(person);
        }catch(CustomValidatorException e){
            model.addAttribute("errors", e.getBindingResult());
            return "viewname";
        }
        return "viewname";
    }

}

Имейте в виду, что если вы будете вызывать @CustomValidation из методов класса PersonService, проверка не сработает. Потому что он будет вызывать методы исходного класса, а не прокси. Это означает, что вы можете вызывать эти методы только извне класса (из других классов), если вы хотите, чтобы валидация работала (например, @Transactional works same way).

Извините за длинный пост. Мой ответ не о «простом декларативном способе», и, возможно, он вам не понадобится. Но мне было любопытно решить эту проблему.

person Ken Bekov    schedule 19.03.2016
comment
Это на самом деле именно то, что я искал! Большое спасибо, что нашли время. Я начал идти по этому пути, но просто не был уверен, как далеко я хочу зайти. Или сколько времени это займет. То, что вы показали, было не так уж и плохо, всего несколько занятий. Отличный ответ. - person sbzoom; 21.03.2016
comment
Возможно, это устарело... но если вы все ищете более простой подход, вы можете попробовать SpringValidatorAdapter, как описано здесь Как объединить класс JSR-303 и Spring Validator на сервисном уровне? - person Jose-Rdz; 21.11.2017

Я отметил ответ @Ken как правильный, потому что это так. Но я пошел немного дальше и хотел опубликовать то, что я сделал. Надеюсь, кому-нибудь, зашедшему на эту страницу, будет интересно. Я мог бы попытаться представить его людям из Spring, чтобы узнать, может ли он быть включен в будущие выпуски.

Идея состоит в том, чтобы заменить @Valid новой аннотацией. Поэтому я назвал его @SpringValid. Использование этой аннотации запустит систему, собранную выше. Вот все части:

SpringValid.java

package org.springframework.validation.annotation;

import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

@Target({METHOD, FIELD, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface SpringValid {

}

SpringValidationAspect.java

package org.springframework.validation;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.List;

@Aspect
@Component
public class SpringValidationAspect {

  private SpringValidatorRegistry springValidatorRegistry;

  @Autowired
  public SpringValidationAspect(final SpringValidatorRegistry springValidatorRegistry) {
    this.springValidatorRegistry = springValidatorRegistry;
  }

  public SpringValidatorRegistry getSpringValidatorRegistry() {
    return springValidatorRegistry;
  }

  @Before("@target(org.springframework.validation.annotation.Validated) "
      + "&& execution(public * *(@org.springframework.validation.annotation.SpringValid (*), ..)) "
      + "&& args(validationTarget)")
  public void beforeMethodThatNeedsValidation(Object validationTarget) {
    validate(validationTarget);
  }

  private void validate(Object arg) {
    List<Validator> validatorList = springValidatorRegistry.getValidatorsForObject(arg);
    for (Validator validator : validatorList) {
      BindingResult errors = new BeanPropertyBindingResult(arg, arg.getClass().getSimpleName());
      validator.validate(arg, errors);
      if (errors.hasErrors()) {
        throw new SpringValidationException(errors);
      }
    }
  }
}

Примеры Spring показывают классы, аннотированные @Validated, поэтому я хотел сохранить это. Вышеупомянутый аспект нацелен только на классы с @Validated на уровне класса. И точно так же, как когда вы используете @Valid, он ищет аннотацию @SpringValid, прикрепленную к параметру метода.

SpringValidationException.java

package org.springframework.validation;

import org.springframework.validation.BindingResult;

public class SpringValidationException extends RuntimeException {

  private static final long serialVersionUID = 1L;

  private BindingResult bindingResult;

  public SpringValidationException(final BindingResult bindingResult) {
    this.bindingResult = bindingResult;
  }

  public BindingResult getBindingResult() {
    return bindingResult;
  }
}

SpringValidatorRegistry.java

package org.springframework.validation;

import org.springframework.validation.Validator;

import java.util.ArrayList;
import java.util.List;

public class SpringValidatorRegistry {

  private List<Validator> validatorList = new ArrayList<>();

  public void addValidator(Validator validator) {
    validatorList.add(validator);
  }

  public List<Validator> getValidatorsForObject(Object o) {
    List<Validator> result = new ArrayList<>();
    for (Validator validator : validatorList) {
      if (validator.supports(o.getClass())) {
        result.add(validator);
      }
    }
    return result;
  }
}

Как и в первом ответе, место для регистрации всех классов, реализующих интерфейс Spring org.springframework.validation.Validator.

SpringValidator.java

package org.springframework.validation.annotation;

import org.springframework.stereotype.Component;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface SpringValidator {

}

Это просто дополнительный соус, облегчающий регистрацию/нахождение Validators. Вы можете зарегистрировать все свои Validators вручную или найти их с помощью отражения. Так что эта часть не требуется, я просто подумал, что это упрощает задачу.

MyConfig.java

package com.example.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.SpringValidationAspect;
import org.springframework.validation.SpringValidatorRegistry;
import org.springframework.validation.annotation.SpringValidator;

import java.util.Map;

import javax.validation.Validator;

@Configuration
public class MyConfig {

  @Autowired
  private ApplicationContext applicationContext;

  @Bean
  public SpringValidatorRegistry validatorRegistry() {
    SpringValidatorRegistry registry = new SpringValidatorRegistry();
    Map<String, Object> validators =
        applicationContext.getBeansWithAnnotation(SpringValidator.class);
    validators.values()
        .forEach(v -> registry.addValidator((org.springframework.validation.Validator) v));
    return registry;
  }

  @Bean
  public SpringValidationAspect springValidationAspect() {
    return new SpringValidationAspect(validatorRegistry());
  }
}

Смотрите, просканируйте путь к классам, найдите @SpringValidator классов и зарегистрируйте их. Затем зарегистрируйте Аспект и вперед.

Вот пример такого валидатора: MyMessageValidator.java

package com.example.validators;

import com.example.messages.MyMessage;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.SpringValidator;

@SpringValidator
public class MyMessageValidator implements Validator {

  @Override
  public boolean supports(Class<?> clazz) {
    return MyMessage.class.isAssignableFrom(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    ValidationUtils.rejectIfEmpty(errors, "firstField", "{javax.validation.constraints.NotNull}",
    "firstField cannot be null");
    MyMessage obj = (MyMessage) target;
    if (obj.getSecondField != null && obj.getSecondField > 100) {
      errors.rejectField(errors, "secondField", "{javax.validation.constraints.Max}", "secondField is too big");
    }
  }
}

А вот сервисный класс, использующий аннотацию @SpringValid:

MyService.java

package com.example.services;

import com.example.messages.MyMessage;

import org.springframework.validation.annotation.SpringValid;
import org.springframework.validation.annotation.Validated;

import javax.inject.Inject;

@Validated
public class MyService {

  public String doIt(@SpringValid final MyMessage msg) {
    return "we did it!";
  }
}

Надеюсь, это имеет смысл для кого-то в какой-то момент. Я лично считаю, что это весьма полезно. Многие компании начинают переводить свои внутренние API с REST на что-то вроде Protobuf или Thrift. Вы по-прежнему можете использовать Bean Validation, но вам придется использовать XML, а это не так уж и приятно. Поэтому я надеюсь, что это будет полезно для людей, которые хотят по-прежнему выполнять программную проверку.

person sbzoom    schedule 23.03.2016

Надеюсь, это поможет кому-то. У меня это работает, добавив следующую конфигурацию:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@Configuration
public class ValidatorConfiguration {

    @Bean
    public MethodValidationPostProcessor getMethodValidationPostProcessor(){
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
         processor.setValidator(this.validator());
         return processor;
     }

     @Bean
     public LocalValidatorFactoryBean validator(){
         return new LocalValidatorFactoryBean();
     }

 }

Затем служба аннотируется таким же образом (@Validated для класса и @Valid для параметра) и может быть внедрена в другой компонент, где метод может быть вызван напрямую, и произойдет проверка.

person Joel    schedule 04.01.2017
comment
Это выглядит правильно, но не работает. Предоставление журнала сервера: Bean 'validatorConfiguration' типа [com.manishoaham.swadeshi.components.ValidatorConfiguration$$EnhancerBySpringCGLIB$$5d552c0] не подходит для обработки всеми BeanPostProcessors (например: не подходит для автоматического проксирования) - person Manishoaham; 25.05.2020