Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get list of Interfaces from @ComponentScan packages

I would like to implement something similar to Spring Data.

Developer can define some interfaces, add a custom annotation to the interfaces to mark them, (my code will create Proxy instances for the interfaces) and use them by @Autowire to necessary services.

During spring initializing I need to get list of all the interfaces (properly annotated)< create dynamic Proxy for the interfaces and inject them where they are necessary.

Proxy creation, created beans injecting is fine. Now the problem:

How to find the list of all the interfaces?

They could be placed in any package (or even in a separate jar) and have any name. Scanning all the classes existing on the classpath requires too much time.

I found the question but it requires base package to start.

Tried a Reflections based solution but again it requires base package or in case of starting from root requires really a lot of time to scan all classes available.

Reflections reflections = new Reflections("...");
Set<Class<?>> annotated = reflections.getTypesAnnotatedWith(<annotation>);

So I need a full list of base packages Spring scans to find my Interfaces in the packages (must be much much faster).

The info is definitely available in SpringContext. I tried to debug and see how basePackages[] is initialized but there are a lot of private classes/methods used to initialize and I just don't see how to access the basePackages properly from ApplicationContext.

like image 412
StanislavL Avatar asked Apr 21 '17 09:04

StanislavL


People also ask

What is @component and @ComponentScan?

@Component and @ComponentScan are for different purposes. @Component indicates that a class might be a candidate for creating a bean. It's like putting a hand up. @ComponentScan is searching packages for Components.

Can we use @component for interface?

Annotating an interface with @Component is common for Spring classes, particularly for some Spring stereotype annotations : package org.

How do I get Spring to scan all packages?

With Spring, we use the @ComponentScan annotation along with the @Configuration annotation to specify the packages that we want to be scanned. @ComponentScan without arguments tells Spring to scan the current package and all of its sub-packages.

Can we use @ComponentScan without @configuration?

@Configuration is meta annotated with @Component , which marks it eligible for classpath scanning. AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig. class); @Bean doesn't need @ComponentScan as all these beans are created explicitly when spring encounters this annotation.


1 Answers

Solution 1: Spring way

The simplest answer is to follow how spring sub projects (boot,data...) implements this type of requirement. They usually define a custom composed annotation which enable the feature and define a set of packages to scan.

For example given this annotation :

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({MyInterfaceScanRegistrar.class})
public @interface MyInterfaceScan {

  String[] value() default {};
}

Where value defines the packages to scan and @Import enables the MyInterfaceScan detection.

Then create the ImportBeanDefinitionRegistrar. This class will be able to create bean definition

Interface to be implemented by types that register additional bean definitions when processing @Configuration classes. Useful when operating at the bean definition level (as opposed to @Bean method/instance level) is desired or necessary.

public class MyInterfaceScanRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware {
  private Environment environment;

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

  @Override
  public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // Get the MyInterfaceScan annotation attributes
    Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(MyInterfaceScan.class.getCanonicalName());

    if (annotationAttributes != null) {
      String[] basePackages = (String[]) annotationAttributes.get("value");

      if (basePackages.length == 0){
        // If value attribute is not set, fallback to the package of the annotated class
        basePackages = new String[]{((StandardAnnotationMetadata) metadata).getIntrospectedClass().getPackage().getName()};
      }

      // using these packages, scan for interface annotated with MyCustomBean
      ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false, environment){
        // Override isCandidateComponent to only scan for interface
        @Override
        protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
          AnnotationMetadata metadata = beanDefinition.getMetadata();
          return metadata.isIndependent() && metadata.isInterface();
        }
      };
      provider.addIncludeFilter(new AnnotationTypeFilter(MyCustomBean.class));

      // Scan all packages
      for (String basePackage : basePackages) {
        for (BeanDefinition beanDefinition : provider.findCandidateComponents(basePackage)) {
          // Do the stuff about the bean definition
          // For example, redefine it as a bean factory with custom atribute... 
          // then register it
          registry.registerBeanDefinition(generateAName() , beanDefinition);
          System.out.println(beanDefinition);
        }
      }
    }
  }
}

This is the core of the logic. The bean definition can be manipulated and redefined as a bean factory with attributes or redefined using a generated class from an interface.

MyCustomBean is a simple annotation:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomBean {

}

Which could annotate an interface:

@MyCustomBean
public interface Class1 {

}

Solution 2: extract component scan

The code which would extract packages define in @ComponentScan will be more complicated.

You should create a BeanDefinitionRegistryPostProcessor and mimic the ConfigurationClassPostProcessor:

  • Iterate over the bean registry for bean definitions with a declared class having the ComponentScan attribute eg (extracted from ConfigurationClassPostProcessor.):

    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
      List<BeanDefinitionHolder> configCandidates = new ArrayList<BeanDefinitionHolder>();
      String[] candidateNames = registry.getBeanDefinitionNames();
      for (String beanName : candidateNames) {
        if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
          // Extract component scan
        }
      }
    }
    
  • Extract these attributes as Spring do

    Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
            sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
    
  • Then scan the packages and register the bean definition like the first solution

like image 169
Nicolas Labrot Avatar answered Sep 27 '22 16:09

Nicolas Labrot