web-dev-qa-db-fra.com

Annotation Spring custom @Enable méta-annotée avec @ComponentScan

J'essaie d'écrire ma propre annotation @Enable Pour le framework Spring, qui doit être utilisée comme suit:

package com.example.package.app;

@SpringBootApplication
@com.example.annotations.EnableCustom("com.example.package.custom")
public class MyApplication {}

J'ai suivi Analyse des composants en utilisant une annotation personnalisée , mais cela pose plusieurs limitations:

  1. Je ne peux pas rendre dynamique la propriété du package de base, c'est-à-dire que je ne peux pas passer "com.example.package.base", Mais j'ai besoin de prédéfinir le package lors de la configuration.

    J'ai jeté un œil à @AliasFor, Mais je n'arrive pas à le faire fonctionner.

  2. Lorsque je laisse de côté le package de base, l'analyse commence à partir du package définissant l'annotation, et non à partir du package de la classe annotée. Dans l'exemple ci-dessus, il ne numériserait et créerait des beans que pour les classes dans com.example.annotations, Mais pas pour com.example.package.*.

    J'ai jeté un coup d'œil à EntityScanPackages.Registrar.class Qui est importé dans l'annotation @EntityScan, Mais c'est une classe interne et mon annotation ne peut pas être importée.

Tout fonctionne si je mets @ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, value = MyAnnotation.class)) sur la classe MyApplication, mais arrête de fonctionner lorsque cela est déplacé vers une méta-annotation de @EnableCustom. Comment dire à Spring Framework de considérer @EnableCustom Comme une manière différente de spécifier @ComponentScan Avec des valeurs par défaut. J'ai essayé de méta-annoter mon annotation avec @Configuration, @Component Et d'autres, en vain:

@Configuration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ComponentScan(
        includeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                value = ApplicationService.class))
public @interface EnableApplicationServices {
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] value() default {};
}

Où puis-je trouver de la documentation à ce sujet ou quel point de départ recommanderiez-vous? Mon objectif à long terme est d'avoir un démarreur Spring Boot qui puisse être utilisé par une multitude de projets.


Un M (N) WE peut être trouvé dans le référentiel suivant: https://github.com/knittl/stackoverflow/tree/spring-enable-annotation

Voici un aperçu des structures des packages:

// com.example.annotations.EnableCustom.Java
@Configuration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
// this annotation is never honored:
@ComponentScan(
        includeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                value = MyAnnotation.class))
//@Import(EnableCustom.EnableCustomConfiguration.class)
public @interface EnableCustom {
    // this annotation works in combination with @Import, but scans the wrong packages.
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(
                    type = FilterType.ANNOTATION,
                    value = MyAnnotation.class))
    class EnableCustomConfiguration {}
}

// file:com.example.app.Application.Java
@SpringBootApplication
@EnableCustom("com.example.app.custom.services")
// @ComponentScan(
//         includeFilters = @ComponentScan.Filter(
//                 type = FilterType.ANNOTATION,
//                 value = MyAnnotation.class)) // <- this would work, but I want to move it to a custom annotation
public class Application {
}

// file:com.example.app.custom.services.MyService
@MyAnnotation
public class MyService {
    public MyService() {
        System.out.println("Look, I'm a bean now!");
    }
}

// file:com.example.annotations.services.WrongService.Java
@MyAnnotation
public class WrongService {
    public WrongService() {
        System.out.println("I'm in the wrong package, I must not be instantiated");
    }
}
5
knittl

Avec l'aide de réponse de Fabio Formosa , les bits manquants remplis de cette réponse , et un peu d'inspiration de l'annotation @EntityScan, J'ai finalement réussi à obtenir ceci à travail. Un exemple de travail compilable peut être trouvé à https://github.com/knittl/stackoverflow/tree/spring-enable-annotation-working .

En un mot: en s'appuyant sur la réponse de Fabio, il est important de configurer correctement une instance ClassPathScanningCandidateComponentProvider avec des filtres d'inclusion, puis de l'exécuter sur toutes les classes de base fournies. @AliasFor(annotation = Import.class, …) ne semble pas nécessaire et peut être aliasé sur un autre attribut, par exemple basePackages de la même annotation.

La mise en œuvre minimale est la suivante:

@Configuration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(EnableCustom.EnableCustomConfiguration.class)
public @interface EnableCustom {
    @AliasFor(attribute = "basePackages")
    String[] value() default {};

    @AliasFor(attribute = "value")
    String[] basePackages() default {};

    class EnableCustomConfiguration implements ImportBeanDefinitionRegistrar, EnvironmentAware {
        private static final BeanNameGenerator BEAN_NAME_GENERATOR = AnnotationBeanNameGenerator.INSTANCE;
        private Environment environment;

        @Override
        public void setEnvironment(final Environment environment) {
            this.environment = environment;
        }

        @Override
        public void registerBeanDefinitions(
                final AnnotationMetadata metadata,
                final BeanDefinitionRegistry registry) {
            final AnnotationAttributes annotationAttributes = new AnnotationAttributes(
                    metadata.getAnnotationAttributes(EnableCustom.class.getCanonicalName()));

            final ClassPathScanningCandidateComponentProvider provider
                    = new ClassPathScanningCandidateComponentProvider(false, environment);
            provider.addIncludeFilter(new AnnotationTypeFilter(MyAnnotation.class, true));

            final Set<String> basePackages
                    = getBasePackages((StandardAnnotationMetadata) metadata, annotationAttributes);

            for (final String basePackage : basePackages) {
                for (final BeanDefinition beanDefinition : provider.findCandidateComponents(basePackage)) {
                    final String beanClassName = BEAN_NAME_GENERATOR.generateBeanName(beanDefinition, registry);
                    if (!registry.containsBeanDefinition(beanClassName)) {
                        registry.registerBeanDefinition(beanClassName, beanDefinition);
                    }
                }
            }
        }

        private static Set<String> getBasePackages(
                final StandardAnnotationMetadata metadata,
                final AnnotationAttributes attributes) {
            final String[] basePackages = attributes.getStringArray("basePackages");
            final Set<String> packagesToScan = new LinkedHashSet<>(Arrays.asList(basePackages));

            if (packagesToScan.isEmpty()) {
                // If value attribute is not set, fallback to the package of the annotated class
                return Collections.singleton(metadata.getIntrospectedClass().getPackage().getName());
            }

            return packagesToScan;
        }
    }
}
1
knittl