2

I've got a very simple spring boot test application.

It has just a Dog class and the @SpringBootApplication annotated one. I create two beans of Dog and everything runs as expected.


public class Dog {
    
    public String name;
    
    public Dog() {
        this("noname");
    }

    public Dog(String name) {
        this.name = name;
    }

    public String toString() {
        return name;
    }
}

@SpringBootApplication
public class DemoApplication {

    @Autowired
    private List<Dog> dogs;
    
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
    
    @Bean
    CommandLineRunner runner1() {
        return args -> {
            for (Dog d : dogs) {
                System.out.println(d);
            }
        };
    }

    
    @Bean
    Dog laika() {
        return new Dog("laika");
    }

    @Bean
    Dog lassie() {
        return new Dog("lassie");
    }
}

Outputs:

laika
lassie

However now I add a @Component annotation to Dog class, expecting that now I'll got three beans of type Dog and that's what it seems to happen if I print all beans with another CommandLineRunner like this one:

    @Bean
      public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
        return args -> {
          System.out.println("Let's inspect the beans provided by Spring Boot:");
          String[] beanNames = ctx.getBeanDefinitionNames();
          Arrays.sort(beanNames);
          for (String beanName : beanNames) {
            System.out.println(beanName);
          }
        };
    }

Outputs:

Let's inspect the beans provided by Spring Boot:
applicationAvailability
applicationTaskExecutor
commandLineRunner
demoApplication
dog
laika
lassie
lifecycleProcessor
...

Yet, when I use my first CommandLineRunner for printing out the contents of my Dog list, the only output that I get is:

noname

It seems like @Component beans have made the beans declared by @Bean dissapear for collection injection. I observe the same behaviour with any beans, for example if I declare more CommandLineRunners in an independent @Component class, everyone is ran, but when I @Autowire'd them in a list, only the ones declared with @Component are injected.

Nevertheless, I can still use the other Dog beans. For example, if I annotate Laika bean with @Primary, it's the one that will be injected as a method argument, but nothing changes regarding the @Autowire'd collection.

Héctor
  • 31
  • 3
  • 1
    This is a weird edge case that IMO isn't worth diving into, but that demonstrates why it's probably a bad idea to inject a list of beans in a `@Configuration` class (that is also responsible for their initialization). Instead, if you really want the list of `Dog` beans, specify a `List` parameter in your `runner1` `@Bean` method and Spring will take care of injecting them all. – Sotirios Delimanolis Dec 08 '20 at 17:37
  • 1
    If you're really really really interested, drop a breakpoint in [here](https://github.com/spring-projects/spring-framework/blob/master/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java#L1530-L1575). That's where the difference happens. Essentially, when `dogs` is being populated, Spring will consider only beans declared outside your `@Configuration` class (`isSelfReference` in the link), but only if there are any. If there are not, it will use the `@Bean` method ones as well. – Sotirios Delimanolis Dec 08 '20 at 22:14

1 Answers1

-1

Your boot application is in an interesting way, but anyway, this pcs of code

    @Bean
CommandLineRunner runner1() {
    return args -> {
        for (Dog d : dogs) {
            System.out.println(d);
        }
    };
}

when it is invoking, the other 2 dogs are not yet initialized, so in the dogs list you only have the @component annotated one, which is "noname"

SebastianX
  • 142
  • 1
  • 5
  • 1
    That's strange because if I print all the beans into ApplicationContext in that same CommandLineRunner I can see the other 2 dogs initialized. Besides, why if I comment out @Component annotation in Dog class they are initilized at that point? – Héctor Dec 08 '20 at 16:46
  • That is not strange, when you have ctx, everything is done, but inside CommandLineRunner runner1() as I said, at that moment, the ctx is not ready yet, it is still tring to create rest beans, to confirm this, if you call the method runner1() after the application started, you will see 3 dogs, – SebastianX Dec 08 '20 at 16:57
  • But why the difference between their two examples? – Sotirios Delimanolis Dec 08 '20 at 16:59
  • it's all about timing, when Spring application is ready you will have all 3 beans, when the application is preparing, like in the method runner1(), you will only have 1 bean, not the other 2 beans, because they are not created yet, when the runner1() method finish, Spring will continue with @Bean annotated methods of laika() and lassie(). – SebastianX Dec 08 '20 at 20:30
  • In the 2nd case , method @Bean public CommandLineRunner commandLineRunner(ApplicationContext ctx) instead will only be invoked after Spring is ready, because you have parameter ApplicationContext ctx as input, Spring will await until it created the ApplicationContext before calling this method, but in upper runner1(), the method is inside the SpringApplication and will be invoked during Spring preparing context, that's why you don't have completed bean list – SebastianX Dec 08 '20 at 20:33
  • Forget the `commandLineRunner` method. Their `runner1` method is the same across both examples, but prints different elements in the `@Autowired` `dogs` list. – Sotirios Delimanolis Dec 08 '20 at 21:41
  • As @SotiriosDelimanolis says, passing ApplicationContext doesn't change the output. So it cannot be that the reason. – Héctor Dec 08 '20 at 22:11