schauk11erd
schauk11erd

Reputation: 634

Inject a merged list of beans within @Configuration

First, let me introduce a few simple classes and interface to be able to describe my problem.

interface Basic { void foo(); }
interface Extended extends Basic { void bar(); }

class BasicService {
  @Inject
  List<Basic> basics;

  void execute() {
    basics.forEach(Basic::foo);
  }
}

class ExtendedService {
  @Inject
  List<Extended> extendeds;

  void execute() {
    extendeds.forEach(Extended::bar);
  }
}

@Configuration
class MyConfiguration {
  // Assume, that Basic1 and Basic2 are implementations of Basic and
  // Extended1 is an implementation of Extended
  @Bean
  public Basic basic1() {
    return new Basic1();
  }
  @Bean
  public Basic basic2() {
    return new Basic2();
  }
  @Bean
  public List<Extended> extendeds() {
    return Arrays.asList(new Extended1("0"), new Extended1("1"), new Extended1("2"));
  }

  @Bean
  public BasicService basicService() {
    return new BasicService();
  }
  @Bean
  public ExtendedService extendedService() {
    return new ExtendedService();
  }
}

I have two services that act on different level of abstraction. My problem is that I'm failing to find a way how I can inject all beans that implement the Basic interface in my BasicService. With the current implementation it only injects all Extended implementations, because I have a factory bean method that has a return type of List in its method signature.

I can not change the bean configuration in a way that all Extended beans have their own factory methods, because in my real code the number of Extended implementations is dynamically computed on runtime...

Is there a way I can configure Spring, so that it merges all beans with Basic and List<Basic> together in 1 big list that I can use in my BasicService?

Upvotes: 0

Views: 81

Answers (2)

Benjamin Maurer
Benjamin Maurer

Reputation: 3753

I'm not sure whether there is a way to solve this implicitly, since Spring tries to find the best match, which the List bean seems to be (have you tried generics, e.g., List<? extends Basic> ?).

But you could do it programmatically, by getting the bean definitions from the ApplicationContext:

public BasicService(@Inject ApplicationContext ctx) {
    Map<String, ? extends Basic> basicMap = ctx.getBeansOfType(Basic.class);
    Map<String, Collection<? extends Basic>> basicCollectionMap = ctx.getBeansofType(ResolvableType.forClassWithGenerics(Collection.class, Basic.class));

    // Now merge
    Collection<? extends Basic> basics = basicMap.values();
    basicCollectionMap.values().stream().map(l -> basics.addAll(l));
}

I just typed this here, so I hope there aren't any obvious errors.

Upvotes: 1

schauk11erd
schauk11erd

Reputation: 634

I solved the problem by adding generic bean definitions for the Extended implementations. This can be done by adding a BeanDefinitionRegistryPostProcessor implementation to the @Configuration class as follows:

@Configuration
class MyConfiguration {
  @Bean
  public static ExtendedBeanFactory extendedBeanFactory() {
    return new ExtendedBeanFactory();
  }
}

class ExtendedBeanFactory implements BeanDefinitionRegistryPostProcessor {
  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    for (int i = 0; i < 3; i++) {
      GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
      beanDefinition.setBeanClass(Extended1.class);
      ConstructorArgumentValues args = new ConstructorArgumentValues();
      args.addGenericArgumentValue(Integer.toString(i));
      beanDefinition.setConstructorArgumentValues(args);
      registry.registerBeanDefinition("extended_" + i, beanDefinition);
    }
  }

  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
    // noop
  }
}

This has the advantage, that consumer classes can simply inject a list of Basic or a list of Extended as the code from the question does.

Upvotes: 0

Related Questions