web-dev-qa-db-fra.com

Utilisez @Validated et @Valid avec validateur de ressort

Un bean Java est utilisé pour envoyer des messages JSON à un @RestController printanier et la configuration de validation de bean est exécutée à l’aide de @Valid. Mais je veux passer à Protobuf/Thrift et m'éloigner de REST. C'est une API interne et beaucoup de grandes entreprises ont supprimé REST en interne. Cela signifie en réalité que je n'ai plus le contrôle des objets de message - ils sont générés à l'extérieur. Je ne peux plus mettre d'annotations dessus.

Alors maintenant, ma validation doit être programmatique. Comment puis-je faire cela? J'ai codé une Validator et cela fonctionne très bien. Mais il n'utilise pas l'annotation Nice @Valid. Je dois faire ce qui suit:

@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();
        }
    }
}

Ça pue. Je dois le faire dans chaque méthode. Maintenant, je peux mettre beaucoup de cela dans une méthode qui jette BindException et qui en fait une ligne de code à ajouter à chaque méthode. Mais ce n'est toujours pas génial.

Ce que je veux, c'est que ça ressemble à ça:

@Service
@Validated
public StuffEndpoint implements StuffThriftDef.Iface {

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

Et toujours obtenir le même résultat. N'oubliez pas que mon haricot n'a pas d'annotations. Et oui, je sais que je peux utiliser l'annotation @InitBinder sur une méthode. Mais cela ne fonctionne que pour les requêtes Web.

Cela ne me dérange pas d'injecter la bonne Validator dans cette classe, mais je préférerais que mon ValidatorFactory puisse extraire la bonne en fonction de la méthode supports().

Est-ce possible? Existe-t-il un moyen de configurer la validation du bean pour qu'il utilise réellement la validation Spring à la place? Dois-je pirater un aspect quelque part? Pirater la LocalValidatorFactory ou la MethodValidationPostProcessor?

Merci.

6
sbzoom

C'est assez compliqué de combiner la validation Spring et la contrainte JSR-303. Et il n'y a pas de moyen prêt à utiliser. Le principal inconvénient est que la validation Spring utilise BindingResult et que JSR-303 utilise ConstraintValidatorContext comme résultat de la validation.

Vous pouvez essayer de créer votre propre moteur de validation en utilisant Spring AOP. Considérons ce que nous devons faire pour cela. Tout d'abord, déclarez les dépendances AOP (si vous ne l'avez pas encore fait):

<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>

J'utilise Spring de la version 4.2.4.RELEASE, mais vous pouvez utiliser la vôtre. AspectJ nécessaire pour utiliser l'annotation d'aspect. La prochaine étape consiste à créer un registre de validateur simple:

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;
    }
}

Comme vous le voyez, c'est une classe très simple, qui nous permet de trouver un validateur pour objet. Créons maintenant des annotations, qui seront des méthodes de marquage, qui doivent être validées:

package com.mydomain.validation;

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

Étant donné que la classe BindingException standard n'est pas RuntimeException, nous ne pouvons pas l'utiliser dans les méthodes surchargées. Cela signifie que nous devons définir notre propre exception:

public class CustomValidatorException extends RuntimeException {

    private BindingResult bindingResult;

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

    public BindingResult getBindingResult() {
        return bindingResult;
    }
}

Nous sommes maintenant prêts à créer un aspect qui fera la plus grande partie du travail. Aspect s'exécutera avant les méthodes marquées de l'annotation 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) signifie que cet aspect sera appliqué à toutes les méthodes publiques de beans, marquées avec l'annotation @CustomValidation. Notez également que pour marquer les paramètres validés, nous utilisons l'annotation standard org.springframework.validation.annotation.Validated. Mais de cause nous pourrions faire notre coutume. Je pense que l’autre code d’aspect est très simple et n’a besoin d’aucun commentaire. Code de l'exemple de validateur:

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");
        }
    }
}

Nous avons maintenant ajusté la configuration et tout est prêt à être utilisé:

@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;
    }    
}

Remarque: proxyTargetClass est true car nous utiliserons un proxy de classe cglib


Exemple de méthode cible dans la classe de service:

@Service
public class PersonService{

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

}

En raison de @CustomValidation, l'aspect de l'annotation sera appliqué et en raison de @Validated, l'annotation person sera validée. Et exemple d'utilisation du service dans le contrôleur (ou toute autre classe):

@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";
    }

}

N'oubliez pas que si vous appelez @CustomValidation à partir de méthodes de la classe PersonService, la validation ne fonctionnera pas. Parce qu'il invoquera des méthodes de classe d'origine, mais pas de proxy. Cela signifie que vous ne pouvez appeler cette méthode que de l'extérieur de la classe (d'autres classes), si vous souhaitez que la validation fonctionne (par exemple, @Transactional works same way).

Désolé pour long post. Ma réponse ne concerne pas «la simple manière déclarative», et vous n'en aurez peut-être pas besoin. Mais j'étais curieux résoudre ce problème.

10
Ken Bekov

J'ai marqué la réponse de @ Ken comme étant correcte parce qu'elle l'est. Mais je l'ai poussé un peu plus loin et je voulais poster ce que j'ai fait. J'espère que quiconque viendra sur cette page le trouvera intéressant. J'essaierai peut-être de le faire savoir aux gens de Spring pour voir si cela pourrait figurer dans les prochaines versions.

L'idée est d'avoir une nouvelle annotation pour remplacer @Valid. Alors je l'ai appelé @SpringValid. Utiliser cette annotation lancerait le système mis en place ci-dessus. Voici toutes les pièces:

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);
      }
    }
  }
}

Les exemples de Spring montrent des classes annotées avec @Validated, donc je voulais le garder. L'aspect ci-dessus cible uniquement les classes avec @Validated au niveau de la classe. Et, tout comme lorsque vous utilisez @Valid, il recherche l'annotation @SpringValid collée à un paramètre de méthode.

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;
  }
}

Tout comme la première réponse, un endroit pour enregistrer toutes les classes qui implémentent l'interface org.springframework.validation.Validator de Spring.

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 {

}

Ceci est juste une sauce supplémentaire pour faciliter l’enregistrement/la recherche Validators. Vous pouvez enregistrer tous vos Validators à la main ou vous pouvez les trouver par réflexion. Donc, cette partie n'est pas obligatoire, je pensais simplement que cela facilitait les choses.

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());
  }
}

Voir, scannez votre chemin de classe et recherchez les classes @SpringValidator et enregistrez-les. Puis enregistrez l'Aspect et c'est parti.

Voici un exemple d'un tel Validateur: 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");
    }
  }
}

Et voici la classe de service qui utilise l'annotation @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!";
  }
}

J'espère que cela a du sens pour quelqu'un à un moment donné. Personnellement, je pense que c'est très utile. De nombreuses entreprises commencent à déplacer leurs API internes de REST vers quelque chose comme Protobuf ou Thrift. Vous pouvez toujours utiliser la validation de bean, mais vous devez utiliser XML et ce n’est pas tout à fait Nice. J'espère donc que cela sera utile aux personnes qui souhaitent continuer à valider leurs programmes.

3
sbzoom

J'espère que ça aide quelqu'un. Je l'ai fait en ajoutant la configuration suivante:

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();
     }

 }

Le service est ensuite annoté de la même manière (@Validated sur la classe et @Valid sur le paramètre) et peut être injecté dans un autre bean où la méthode peut être appelée directement et où la validation est effectuée.

2
Joel