1

Since according to the docs @Component registers beans for the Spring container I'm trying to create a simple example of dependency injection using the following code:

package pl.playground;

//...

@SpringBootApplication
public class PlaygroundApplication {

    @Autowired
    private static Building building;

    public static void main(String[] args) {
        building.setBuildingSize(12L);
        System.out.println(building.monthlyHeatingCost());
    }
}
package pl.playground.facade;

//...

@Component
public class Building {

    private HeatingService service;
    private Long buildingSize;

    @Autowired
    public Building(HeatingService service) {
        this.service = service;
    }

    public Double monthlyHeatingCost() {
        return service.getMonthlyHeatingCost(buildingSize);
    }

    // getters & setters...
}
package pl.playground.service;

public interface HeatingService {
    Double getMonthlyHeatingCost(Long size);
}
package pl.playground.service;

//...

@Component
public class HeatingServiceImpl implements HeatingService {

    private final Double CUBIC_PRICE = 2.3;

    public HeatingServiceImpl() {}

    @Override
    public Double getMonthlyHeatingCost(Long size) {
        return size * CUBIC_PRICE;
    }   
}

It builds and runs, but there is a NullPointerException at building.setBuildingSize(12L);. However the one below works without any issues:

//PlaygroundApplication.java
package pl.playground;

//...

@SpringBootApplication
public class PlaygroundApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(Config.class);

        Building building = context.getBean(Building.class);

        building.setBuildingSize(12L);
        System.out.println(building.monthlyHeatingCost());
    }
}
package pl.playground.config;

//...

@Configuration
public class Config {

    @Bean
    public Building building(HeatingService service) {
        return new Building(service);
    }

    @Bean
    public HeatingServiceImpl heatingServiceImpl() {
        return new HeatingServiceImpl();
    }
}

The rest is the same as before.

  • Why is @Component not creating Beans?

  • It is working the way I think it should when used inside a @Controller of a web app, does that make a difference? How does exactly @Bean and @Component differ?

  • What am I failing to understand?

EDIT

Consider the following scenario:

package pl.playground;

//...

@SpringBootApplication
public class ExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}
package pl.playground.controller;

//...

@Controller
public class Controller {

    private Facade facade;

    @Autowired
    public Controller(Facade facade) {
        this.facade = facade;
    }

    @GetMapping("/")
    public String getIndexPage(Model model) {
        return "index";
    }
}
package pl.playground.facade;

//...

@Component
public class Facade {

    private PostsService postService;
    private UserService userService;
    private TagService tagService;

    @Autowired
    public Facade(PostsService retrieve, UserService user, TagService tag) {
        this.postService = retrieve;
        this.userService = user;
        this.tagService = tag;
    }

    //...
}

I don't need @Configuration here for it to work. That's my concern.

bart-kosmala
  • 931
  • 1
  • 11
  • 20
  • Well in the working version, you actually create the context containing the beans, in the other you don't. And the beans will only be available in components, not static members. – daniu Jul 24 '20 at 11:03
  • @daniu Understood, but I cannot create the context without the config class, however It works implicitly when using `@Component` (or the relatives like `@Service`) inside a `@Controller`. – bart-kosmala Jul 24 '20 at 11:05
  • 1
    @bart-kosmala, Please try to ask one question per post. I know your questions are related and but it is good practice to have one focused question per post. It also causes issues for future reference when there are multiple answers and only one is accepted while each answer is correct as they address different questions in the post. One of the `flag` criteria you see below post is related to that and there is risk of being flagged – Kavithakaran Kanapathippillai Jul 25 '20 at 10:36

3 Answers3

2

The problem with your code is that you are trying to @Autowire on a static field. You simply cannot do that. Look here: Can you use @Autowired with static fields?

It fails to work because the PlaygroundApplication class is not being created and managed by spring. The injection works only inside instances managed by spring. You can treat class annotated with @SpringBootApplication as configuration classes. Spring creates instances of those classes and injection works inside them but only on instance fields.

The second example shows the correct way to access spring beans from main method of the application.

Cezary Butler
  • 807
  • 7
  • 21
  • Thanks, that's making sense. I also tested creating a `App` class within the `main` to test it without the static context and it still didn't work. That App wasn't a `@Bean` however. So the thing is that a `@Controller` is defined as a Bean and created implicitly by Spring Boot and thus I can use DI inside of it without defining Beans? (I'm refering the **EDIT** of the question) Or is it the static `run` method of the `SpringApplication` doing it? – bart-kosmala Jul 24 '20 at 11:26
  • 1
    @bart-kosmala `run()` method is just starting the process. But you're right about how you described `@Component` annotation. It's not the best idea to put beans into static fields, since, from the static code, you can't easily tell when they will be initialized. Spring is no magic, it scans classpath, then creates beans, and performs injection afterwards. – Cezary Butler Jul 24 '20 at 11:36
  • That makes sense, thank you it's been very informative. I thought that the `@SpringBootConfiguration` itself did imply the `@ComponentScan`, but it seems that it is done by the `run()` method that actually returns a `Context` derived class which I can use to omit the explicit `@Configuration`. – bart-kosmala Jul 25 '20 at 09:56
  • 1
    @bart-kosmala `@SpringBootConfiguration` implies component scan. But one way or another you have to tell spring boot to load. In the second example, you do that explicitly by using `SpringApplication`. Another way is to use https://docs.spring.io/spring-boot/docs/2.1.9.RELEASE/reference/html/howto-traditional-deployment.html#howto-create-a-deployable-war-file but it is for war packaged applications. I'm not sure if there is an implicit way to start a spring-boot application. – Cezary Butler Jul 25 '20 at 14:17
1

Well. I used your original question and is working without any issues. @cezary-butler pointed out in the comments you can autowire into PlaygroundApplication but you can get hold of it easily in the static main method using context.getBean(Building.class)

    @SpringBootApplication
    public class PlaygroundApplication {

      public static void main(String[] args) {
        ConfigurableApplicationContext context = 
              SpringApplication.run(PlaygroundApplication.class);
        Building building = context.getBean(Building.class);
        building.setBuildingSize(12L);
        System.out.println(building.monthlyHeatingCost());
      }
    }
  • I understand that I don't need them in the `@Bean` scenario, I'm just wondering why the first example doesn't work without `@Configuration`. – bart-kosmala Jul 24 '20 at 11:08
  • @EditedAnswer Oh I see, so does the line `SpringApplication.run(ExampleApplication.class, args);` define the class implicitly as a `@Bean`? – bart-kosmala Jul 24 '20 at 11:29
  • 1
    I have added the sample repo where it works with your original question. So `Building` does get created. `SpringApplication.run(ExampleApplication.class, args)` will by default make the that package of `ExampleApplication` and sub packages to be scanned. So one of the thing is it configures the `@ComponentScan` for that package and sub package – Kavithakaran Kanapathippillai Jul 24 '20 at 11:33
  • 1
    @KavithakaranKanapathippillai You can inject into instances of classes annotated with `@SpringBootApplication` since they are also a `@Configuration`. However, you can't inject into static fields. https://stackoverflow.com/questions/31782819/injecting-object-into-spring-configuration – Cezary Butler Jul 25 '20 at 14:25
0

TLDR;

A Spring context needs to be created before any bean can be injected. In the first scenario, just the fact of having a @SpringBootApplication decorator does not ensure a context in the scope of the class it decorates.

  • SpringApplication.run(ExampleApplication.class, args); instantiates a context (and e.g. a web server among other things)
  • var context = new AnnotationConfigApplicationContext(Config.class); instantiates a scoped context

Thus the first example had null inside of Building as there was no context with the bean to inject.

bart-kosmala
  • 931
  • 1
  • 11
  • 20