web-dev-qa-db-fra.com

Spring @Validated in service layer

Hej,

Je veux utiliser l'annotation @Validated(group=Foo.class) pour valider un argument avant d'exécuter une méthode comme celle-ci:

public void doFoo(Foo @Validated(groups=Foo.class) foo){}

Lorsque je mets cette méthode dans le contrôleur de mon application Spring, le @Validated est exécuté et génère une erreur lorsque l'objet Foo n'est pas valide. Cependant, si je mets la même chose dans une méthode dans la couche Service de mon application, la validation n'est pas exécutée et la méthode s'exécute juste même lorsque l'objet Foo n'est pas valide.

Ne pouvez-vous pas utiliser le @Validated annotation dans la couche service? Ou dois-je faire quelque chose de plus pour le faire fonctionner?

Mise à jour:

J'ai ajouté les deux beans suivants à mon service.xml:

<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean"/>
<bean class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor"/>

et remplacé le @Validate avec @Null ainsi:

public void doFoo(Foo @Null(groups=Foo.class) foo){}

Je sais que c'est une annotation assez idiote à faire, mais je voulais vérifier que si j'appelle la méthode maintenant et en passant null, elle déclencherait une exception de violation, ce qu'elle fait. Alors pourquoi exécute-t-il le @Null annotation et non @Validate annotation? Je sais que l'un vient de javax.validation et l'autre vient du printemps mais je ne pense pas que cela ait quelque chose à voir avec ça?

20
Tom

Aux yeux d'une pile Spring MVC, il n'existe pas de couche de service. La raison pour laquelle cela fonctionne pour @Controller les méthodes de gestionnaire de classe sont que Spring utilise un HandlerMethodArgumentResolver spécial appelé ModelAttributeMethodProcessor qui effectue la validation avant de résoudre l'argument à utiliser dans votre méthode de gestionnaire.

La couche de service, comme nous l'appelons, n'est qu'un simple bean sans aucun comportement supplémentaire ajouté à partir de la pile MVC (DispatcherServlet). En tant que tel, vous ne pouvez vous attendre à aucune validation de Spring. Vous devez rouler le vôtre, probablement avec AOP.


Avec MethodValidationPostProcessor, jetez un œil au javadoc

Les méthodes applicables ont des annotations de contrainte JSR-303 sur leurs paramètres et/ou sur leur valeur de retour (dans ce dernier cas spécifié au niveau de la méthode, généralement sous forme d'annotation en ligne).

Les groupes de validation peuvent être spécifiés via l'annotation validée de Spring au niveau du type de la classe cible contenant, s'appliquant à toutes les méthodes de service public de cette classe. Par défaut, JSR-303 sera validé uniquement par rapport à son groupe par défaut.

Le @Validated l'annotation n'est utilisée que pour spécifier un groupe de validation, elle ne force elle-même aucune validation. Vous devez utiliser l'un des javax.validation annotations comme @Null ou @Valid. N'oubliez pas que vous pouvez utiliser autant d'annotations que vous le souhaitez sur un paramètre de méthode.

13

@pgiecek Vous n'avez pas besoin de créer une nouvelle annotation. Vous pouvez utiliser:

@Validated
public class MyClass {

    @Validated({Group1.class})
    public myMethod1(@Valid Foo foo) { ... }

    @Validated({Group2.class})
    public myMethod2(@Valid Foo foo) { ... }

    ...
}
5
rubensa

En guise de note complémentaire sur la validation printanière des méthodes:

Puisque Spring utilise des intercepteurs dans son approche, la validation elle-même n'est effectuée que lorsque vous parlez à la méthode d'un Bean:

Lorsque vous parlez à une instance de ce bean via les interfaces du validateur Spring ou JSR-303, vous parlerez au validateur par défaut du ValidatorFactory sous-jacent. Ceci est très pratique dans la mesure où vous n'avez pas à effectuer un autre appel en usine, en supposant que vous utiliserez presque toujours le validateur par défaut de toute façon.

Ceci est important car si vous essayez d'implémenter une validation de cette manière pour les appels de méthode dans la classe, cela ne fonctionnera pas. Par exemple.:

@Autowired
WannaValidate service;
//...
service.callMeOutside(new Form);

@Service
public class WannaValidate {

    /* Spring Validation will work fine when executed from outside, as above */
    @Validated
    public void callMeOutside(@Valid Form form) {
         AnotherForm anotherForm = new AnotherForm(form);
         callMeInside(anotherForm);
    }

    /* Spring Validation won't work for AnotherForm if executed from inner method */
    @Validated
    public void callMeInside(@Valid AnotherForm form) {
         // stuff
    }        
}

J'espère que quelqu'un trouvera cela utile. Testé avec Spring 4.3, les choses peuvent donc être différentes pour les autres versions.

4
Vladimir Salin

Comme indiqué ci-dessus, la spécification des groupes de validation n'est possible que via @Validated annotation au niveau de la classe. Cependant, ce n'est pas très pratique car parfois vous avez une classe contenant plusieurs méthodes avec la même entité comme paramètre mais chacune nécessitant un sous-ensemble différent de propriétés pour valider. C'était aussi mon cas et ci-dessous, vous pouvez trouver plusieurs mesures à prendre pour le résoudre.

1) Implémentez une annotation personnalisée qui permet de spécifier des groupes de validation au niveau de la méthode en plus des groupes spécifiés via @Validated au niveau de la classe.

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

    Class<?>[] value() default {};
}

2) Étendez la méthode MethodValidationInterceptor et remplacez la méthode determineValidationGroups comme suit.

@Override
protected Class<?>[] determineValidationGroups(MethodInvocation invocation) {
    final Class<?>[] classLevelGroups = super.determineValidationGroups(invocation);

    final ValidatedGroups validatedGroups = AnnotationUtils.findAnnotation(
            invocation.getMethod(), ValidatedGroups.class);

    final Class<?>[] methodLevelGroups = validatedGroups != null ? validatedGroups.value() : new Class<?>[0];
    if (methodLevelGroups.length == 0) {
        return classLevelGroups;
    }

    final int newLength = classLevelGroups.length + methodLevelGroups.length;
    final Class<?>[] mergedGroups = Arrays.copyOf(classLevelGroups, newLength);
    System.arraycopy(methodLevelGroups, 0, mergedGroups, classLevelGroups.length, methodLevelGroups.length);

    return mergedGroups;
}

3) Implémentez votre propre MethodValidationPostProcessor (copiez simplement celui de Spring) et dans la méthode afterPropertiesSet utilisez un intercepteur de validation implémenté à l'étape 2.

@Override
public void afterPropertiesSet() throws Exception {
    Pointcut pointcut = new AnnotationMatchingPointcut(Validated.class, true);
    Advice advice = (this.validator != null ? new ValidatedGroupsAwareMethodValidationInterceptor(this.validator) :
            new ValidatedGroupsAwareMethodValidationInterceptor());
    this.advisor = new DefaultPointcutAdvisor(pointcut, advice);
}

4) Enregistrez votre post-processeur de validation au lieu de Spring One.

<bean class="my.package.ValidatedGroupsAwareMethodValidationPostProcessor"/> 

C'est ça. Vous pouvez maintenant l'utiliser comme suit.

@Validated(groups = Group1.class)   
public class MyClass {

    @ValidatedGroups(Group2.class)
    public myMethod1(Foo foo) { ... }

    public myMethod2(Foo foo) { ... }

    ...
}
4
pgiecek

Soyez prudent avec l'approche de rubensa.

Cela ne fonctionne que lorsque vous déclarez @Valid comme seule annotation. Lorsque vous le combinez avec d'autres annotations comme @NotNull tout sauf le @Valid sera ignoré.

Le ce qui suit ne fonctionnera pas et le @NotNull sera ignoré:

@Validated
public class MyClass {

    @Validated(Group1.class)
    public void myMethod1(@NotNull @Valid Foo foo) { ... }

    @Validated(Group2.class)
    public void myMethod2(@NotNull @Valid Foo foo) { ... }

}

En combinaison avec d'autres annotations, vous devez déclarer le javax.validation.groups.Default Groupez aussi, comme ceci:

@Validated
public class MyClass {

    @Validated({ Default.class, Group1.class })
    public void myMethod1(@NotNull @Valid Foo foo) { ... }

    @Validated({ Default.class, Group2.class })
    public void myMethod2(@NotNull @Valid Foo foo) { ... }

}
4
thunderhook