Alexander D.
Alexander D.

Reputation: 91

SpringBoot not recognizing multiple @ComponentScan meta-annotations

I noticed a problem with Spring's @ComponentScan annotation when used as a meta-annotation.

In the following example project structure, both the FirstHandler and SecondService classes should be scanned as components and registered as beans:

org/example/
|_ ExampleContext.java
|___ api/
| |___ ExampleCommand.java
|___ application/
  |___ FirstHandler.java
  |___ SecondService.java
// --- ExampleContext.java ---
@ContextConfiguration
public class ExampleContext { }

// --- api/ExampleCommand.java ---
public class ExampleCommand extends Command {
  // -snip-
}

// --- application/FirstHandler.java ---

public class FirstHandler implements CommandHandler<ExampleCommand> {
  // -snip-
}

// --- application/SecondService.java ---

@CommandService
public class SecondService {
  @CommandMethod(ExampleCommand.class)
  public void handle(ExampleCommand command) {
    // -snip-
  }
}

The Command and related classes are custom, and not relevant to the question at hand. For the purpose of this question, they function as markers and reside in a module which does not depend on Spring, ergo can not be meta-annotated themselves.
The custom annotation ContextConfiguration is supposed to scan all classes either implementing CommandHandler<C> or annotated with CommandService:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScans({
  // Scan for CommandHandler implementations
  @ComponentScan(includeFilters = {
          @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = {
                  CommandHandler.class,
          })
  }),
  // Scan for @CommandService annotated classes
  @ComponentScan(includeFilters = {
          @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = {
                  CommandService.class,
          })
  })
})
public @interface ContextConfiguration { }

But what actually happens is that only the first @ComponentScan annotation is used, and the second annotation is simply ignored. By changing the order of annotations or removing one, I can change which one is ignored/active, but only one of the classes is scanned by Spring.

Is this a known issue? Are there any solutions/workarounds?

Thank you and have a nice day,
Alex.

Upvotes: 2

Views: 301

Answers (1)

Alexander D.
Alexander D.

Reputation: 91

I found a workaround, which basically re-implements what the repeated @ComponentScan annotations should be doing. It was inspired by this answer, with a slightly simplified implementation.

To use multiple different component-scans, a nested configuration implementing ImportBeanDefinitionRegistrar can be imported on the annotation.

Example:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@Import(ContextConfiguration.ScanContextComponents.class)
public @interface ContextConfiguration {

    class ScanContextComponents implements ImportBeanDefinitionRegistrar, EnvironmentAware {
        private Environment environment;

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

        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingMetadata, BeanDefinitionRegistry registry) {
            String basePackage = ClassUtils.getPackageName(importingMetadata.getClassName());
            ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);

            // Add include-filters for different types here
            provider.addIncludeFilter(new AssignableTypeFilter(CommandHandler.class));
            provider.addIncludeFilter(new AnnotationTypeFilter(CommandService.class, true));

            // Register bean-definition candidates
            provider.setEnvironment(environment);
            for (BeanDefinition beanDefinition : provider.findCandidateComponents(basePackage)) {
                String beanName = AnnotationBeanNameGenerator.INSTANCE.generateBeanName(beanDefinition, registry);
                registry.registerBeanDefinition(beanName, beanDefinition);
            }
        }
    }
}

Upvotes: 0

Related Questions