How to get list of Interfaces from @ComponentScan packages
Asked Answered
D

3

10

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.

Detoxify answered 21/4, 2017 at 9:6 Comment(12)
did you try to create beanFactoryPostProccessor? create it - for each bean, see if it's created with your annotation, add class to the set of classes inside factory if it is. you can then access this list later from that factory. This has a caveat that it's a singleton.Bushmaster
@Sarief I cannot. To create the beans I need to find the interfaces. I don't know how many interfaces were defined and where the interfaces are placed. The beans creating is not problem. Problem is how to find all the interfaces from classpathDetoxify
sorry, misunderstood your questionBushmaster
AnnotationAttributes class, from quick look, should contains pairs annotation <--> class. as for uses, check : AnnotationRepositoryConfigurationSourceBushmaster
@Sarief It's from Spring data. I don't have spring data now. I hope to avoid this. I have a workaround to define one super interface and just find all available interfaces extending the base one. I hope to avoid the approach. Don't want to introduce restrictions.Detoxify
nonono, I meant to use what they use. If you read into it, you can copy the code. More basically, the class that does it in spring data is: RepositoryComponentProvider, which is used by AnnotationRepositoryConfigurationSource . You can look at it and make your own. why your own? because it's behaviour specific ONLY to spring data and you have to customize it == you have to make it. ClassPathScanningCandidateComponentProvider would do what you need, but you need to setup filters, which is why you have to look at RepositoryComponentProviderBushmaster
@Sarief I will check anyway. Thank youDetoxify
you might also want to check AnnotatedTypeScanner, also from spring dataBushmaster
Solved similar problem with a hack. Instead of defining such classes/interfaces at any place, we place them in specific directory as groovy based classes and just pick up everything from that place and register it to spring context. This has limited the search for us since we only scan for files with .groovy extension as we know they are the ones to be injected.Lilongwe
@Lilongwe Imagine your are going to write a library like SpringData. You cannot place interfaces and name them. Another developer who uses the library can place the interfaces in any package and add the package to classpath. I need to detect the interfaces and create Proxy for them.Detoxify
Can you use org.springframework.core.io.support.PathMatchingResourcePatternResolver.getResources(String) with org.springframework.core.io.support.ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX with your top level domain e.g. "classpath*:/com/foo/**/*.class". This will return you list of class names, that you can use to load and filterMagnanimity
Have you been able to solve your question?Hole
H
12

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

Hole answered 27/4, 2017 at 7:37 Comment(3)
Not yet. Will try later. Thank you! That's a really great answer!Detoxify
@Nicolas Labrot, Solution 1 worked like a charm !!. Anyone trying to use this piece of code, please read and understand what it does instead of blindly copy pasting.Nonconformist
Just what I was looking for. Thank you!Frederigo
P
0

I your case I would use a config similar to this in your BeanLocation.xml and separate the proyect by subfolders like mine, I found that useful:

folders -> java/ar/edu/unq/tip/marchionnelattenero

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
  http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
  http://www.springframework.org/schema/context
  http://www.springframework.org/schema/context/spring-context-3.0.xsd
  http://www.springframework.org/schema/tx
  http://www.springframework.org/schema/tx/spring-tx.xsd
  http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
  http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">


    <tx:annotation-driven transaction-manager="persistence.transactionManager" proxy-target-class="true"/>

    <!-- Database Configuration -->

    <!-- Auto scan the components -->
    <context:component-scan base-package="ar.*"/>

</beans>

As you can see, I tell to auto scan all component in folders and subfolders begining from /ar

You can check my public git project here -> git project

Check it, and if some new question is related, or maybe I did not understand your question well, let me know

Phytogeography answered 25/4, 2017 at 15:1 Comment(1)
I don't have any bean location configs. Neither XML nor classes. Imagine I am creating a library. The library is loaded e.g. by maven dependency. I have no idea how developer who uses the lib configure his beans. I just need to access to the configuration from the library.Detoxify
W
0

We do this all the time without incident.

Below is the code for the service bean that will be using the List.

@Service
public class SomeService {

@Autowired
List<MyInterface> myInterfaceInstances;

//class stuff

}

Next we have the implementations of the interface.

@Component
public class SomeImpl implements MyInterface {

//class stuff

}

and another one just for good measure...

@Component
public class SomeOtherImpl implements MyInterface {

//class stuff

}
Wells answered 25/4, 2017 at 19:40 Comment(2)
There are no classes - just interfaces. The idea is to declare interfaces only and generate Proxy classes from the interfaces on fly.Detoxify
@Detoxify I follow. You would have to use @Bean methods that return the proxies. I will try it out today when I get into the office and edit my answer.Wells

© 2022 - 2024 — McMap. All rights reserved.