3
votes

Spring Boot @ConditionalOnMissingBean et types génériques

Existe-t-il un moyen d'obtenir l'instanciation de ces deux beans:

@Bean
@ConditionalOnMissingBean
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingBean
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

@ConditionalOnMissingBean ne prend en compte que la classe de bean, ce qui rend faussement bookContainer et computerContainer du même type, donc un seul d'entre eux est enregistré.

Je pourrais donner des qualificatifs explicites à chacun, mais comme cela fait partie d'un Spring Boot Starter , cela rendrait son utilisation ennuyeuse, car l'utilisateur serait alors obligé de connaître d'une manière ou d'une autre le nom exact à remplacer au lieu du type uniquement.

Approche potentielle: p >

Puisqu'il est possible de demander à Spring un bean par son type générique complet, je pourrais peut-être implémenter une fabrique conditionnelle qui essaiera d'obtenir une instance entièrement typée et d'en produire une si elle n'existe pas déjà. J'étudie maintenant si / comment cela peut être fait. Étonnamment, lors de l'implémentation d'une condition personnalisée (à utiliser avec @Conditional ), il n'a pas accès au type de bean ...

Alors que tous les autres frameworks d'injection modernes fonctionnent sur types complets, Spring fonctionne toujours avec des classes brutes et des noms de chaînes (!) ce qui me souffle juste ... Y a-t-il une solution de contournement pour cela?


3 commentaires

avez-vous essayé de définir son paramètre de nom


@neo Vous avez raison, je pourrais. Mais cela fait partie d'un Spring Boot Starter, donc des noms explicites rendraient son utilisation ennuyeuse, car l'utilisateur serait alors obligé de connaître d'une manière ou d'une autre le nom exact à remplacer au lieu du type uniquement. Mise à jour de la question avec cette information.


Lors de l'implémentation d'une condition personnalisée, l'implémentation sous-jacente de la classe @AnnotatedTypeMetadata est en fait @StandardMethodMetadata .qui a une méthode publique nommée #getIntrospectedMethod , alors vous peut obtenir la référence de la méthode correspondante


3 Réponses :


2
votes

Y a-t-il une solution de contournement pour cela?

Malheureusement, je ne pense pas. Le problème est que les génériques en Java n'existent pas dans le bytecode compilé - ils ne sont utilisés que pour la vérification de type au moment de la compilation et ils n'existent que dans le code source. Par conséquent, au moment de l'exécution, lorsque Spring Boot voit votre classe de configuration de bean, il ne voit que deux définitions de bean qui créent toutes deux un conteneur. Il ne voit aucune différence entre eux à moins que vous ne les fournissiez vous-même en leur donnant des identifiants.


2 commentaires

Ce n'est pas vraiment vrai ... Bien que les informations génériques soient perdues sur les instances, elles sont toujours présentes dans le type de retour de la méthode, les types d'arguments, etc. Voir Method # getGenericReturnType pour un exemple de ce que je veux dire. Donc Spring a accès au type complet du bean en regardant le type générique complet de la méthode qui l'a produit. CDI et d'autres frameworks en font également usage. Et cela fonctionne normalement aussi au printemps, le problème est avec @ConditionalOnMissingBean dans ce cas.


Voir ma réponse pour une solution de contournement que j'ai trouvée ...



4
votes

Voici une solution entièrement fonctionnelle. J'ai abandonné cette approche, mais je la poste au cas où quelqu'un la trouverait utile.

J'ai fait une annotation personnalisée:

Class<?> declaringClass = ClassUtils.forName(methodMeta.getDeclaringClassName(), context.getClassLoader());

Et une condition personnalisée à emporter avec:

@ConditionalOnMissingGenericBean(declaringClass=CustomConfig.class)

Ceux-ci me permettent de faire ce qui suit:

@Bean
@ConditionalOnMissingGenericBean(containerType=Container.class, typeParameters=Computer.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

Les deux beans seront maintenant chargés comme ils ' re de différents types génériques. Si l'utilisateur du démarreur enregistrait un autre bean de type Container ou Container , alors le bean par défaut ne serait pas chargé, exactement comme souhaité.

Telle qu'elle est implémentée, il est également possible d'utiliser l'annotation de cette manière:

@Bean
//Load this bean only if Container<Car> isn't present
@ConditionalOnMissingGenericBean(containerType=Container.class, typeParameters=Car.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

Remarques: Le chargement de la classe de configuration dans la condition n'est peut-être pas sûr, mais dans ce cas précis, cela peut aussi l'être ... Je n'en ai aucune idée. Je n'ai pas pris la peine d'enquêter plus avant car j'ai opté pour une approche totalement différente (interface différente pour chaque bean). Si vous avez des informations à ce sujet, veuillez commenter.

Si des types explicites sont fournis dans l'annotation, par exemple

@Bean
@ConditionalOnMissingGenericBean
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingGenericBean
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}

, ce problème disparaît, mais ceci l'approche ne fonctionnera pas si les paramètres de type sont également génériques, par exemple Container>.

Une autre façon de garantir la sécurité serait d'accepter la classe déclarante dans l'annotation:

public class MissingGenericBeanCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        if (!metadata.isAnnotated(ConditionalOnMissingGenericBean.class.getName()) || context.getBeanFactory() == null) {
            return false;
        }
        Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnMissingGenericBean.class.getName());
        Class<?> containerType = (Class<?>) attributes.get("containerType");
        Class<?>[] typeParameters = (Class<?>[]) attributes.get("typeParameters");

        ResolvableType resolvableType;
        if (Void.class.equals(containerType)) {
            if (!(metadata instanceof MethodMetadata) || !metadata.isAnnotated(Bean.class.getName())) {
                throw error();
            }
            //When resolving beans within the starter
            if (metadata instanceof StandardMethodMetadata) {
                resolvableType = ResolvableType.forType(((StandardMethodMetadata) metadata).getIntrospectedMethod().getGenericReturnType());
            } else {
                //When resolving beans in an application using the starter
                MethodMetadata methodMeta = (MethodMetadata) metadata;
                try {
                    // This might not be a safe thing to do. See the notes below.
                    Class<?> declaringClass = ClassUtils.forName(methodMeta.getDeclaringClassName(), context.getClassLoader());
                    Type returnType = Arrays.stream(declaringClass.getDeclaredMethods())
                            .filter(m -> m.isAnnotationPresent(Bean.class))
                            .filter(m -> m.getName().equals(methodMeta.getMethodName()))
                            .findFirst().map(Method::getGenericReturnType)
                            .orElseThrow(MissingGenericBeanCondition::error);
                    resolvableType = ResolvableType.forType(returnType);
                } catch (ClassNotFoundException e) {
                    throw error();
                }
            }
        } else {
            resolvableType = ResolvableType.forClassWithGenerics(containerType, typeParameters);
        }

        String[] names = context.getBeanFactory().getBeanNamesForType(resolvableType);
        return names.length == 0;
    }

    private static IllegalStateException error() {
        return new IllegalStateException(ConditionalOnMissingGenericBean.class.getSimpleName()
                + " is missing the explicit generic type and the implicit type can not be determined");
    }
}


0 commentaires

1
votes

C'est possible depuis Spring Boot v2.1.0.

Cette version a introduit le nouveau champ parameterizedContainer sur ConditionalOnMissingBean.

@Bean
@ConditionalOnMissingBean(value = Book.class, parameterizedContainer = Container.class)
public Container<Book> bookContainer() {
    return new Container<>(new Book());
}

@Bean
@ConditionalOnMissingBean(value = Computer.class, parameterizedContainer = Container.class)
public Container<Computer> computerContainer() {
    return new Container<>(new Computer());
}


0 commentaires