Inversion of Control and the ApplicationContext

Let Spring create and wire your objects, and understand the ApplicationContext that holds them for the life of the application.

SyntaxConfigurableApplicationContext ctx = SpringApplication.run(App.class, args); T bean = ctx.getBean(T.class);

In plain Java, a class that needs a repository creates one with new, and so does every class that needs it. Inversion of control turns that round: your classes declare what they need, and a container builds the objects, works out the order, and hands each one its collaborators. Spring calls the container the ApplicationContext and the objects it manages beans.

What the context does

When SpringApplication.run starts, Spring Boot creates a context, reads bean definitions from your @Component classes, your @Bean methods and the auto-configuration that the starters bring in, and then refreshes it. Refreshing means creating every singleton bean, injecting dependencies, running lifecycle callbacks and, in a web application, starting the embedded server. The method returns the running context, and it stays alive until the JVM shuts down.

ConfigurableApplicationContext context = SpringApplication.run(ShopApplication.class, args);
OrderService orders = context.getBean(OrderService.class);

You rarely call getBean yourself. Asking the context for objects by hand is the service locator pattern, and it hides dependencies just as new did. Let Spring inject them through constructors instead, and keep getBean for tests, diagnostics and the odd bootstrap script.

Why it pays off

  • One place to swap implementations. A test, a profile or a property can replace the real payment client with a fake without touching the classes that use it.
  • Startup fails fast. A missing dependency or an ambiguous one stops the context from refreshing, so you find out at boot rather than on the first request in production.
  • Cross-cutting features work. Transactions, caching, security and retries are applied by wrapping beans in proxies. That only happens to objects the container created. An object you build with new gets none of it, which is a common reason @Transactional "does nothing".

Mistakes to avoid

Do not store the context in a static field so that non-Spring code can reach beans. It couples everything to the container, breaks when tests start several contexts, and is usually a sign that the calling class should itself be a bean. Do not create a second context inside your application either; one Spring Boot application has one context (plus a child context in a few specialised setups such as the management port).

Startup cost is real: every singleton is built before the application accepts traffic. That is the right default for servers, because errors surface early. spring.main.lazy-initialization=true defers creation until first use and can speed up local restarts, but it moves failures to the first request, so keep it out of production.

Example

Example · java

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;

@SpringBootApplication
public class ShopApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(ShopApplication.class, args);
        // Only for demonstration: application code receives beans by injection.
        OrderService orders = context.getBean(OrderService.class);
        orders.place("ord-1", new BigDecimal("19.99"));
        System.out.println(orders.all());
    }
}

record Order(String id, BigDecimal total) {}

@Repository
class OrderRepository {
    private final Map<String, Order> store = new ConcurrentHashMap<>();

    void save(Order order) {
        store.put(order.id(), order);
    }

    List<Order> findAll() {
        return List.copyOf(store.values());
    }
}

@Service
class OrderService {
    private final OrderRepository repository;

    // Spring sees the constructor parameter and passes in the repository bean.
    OrderService(OrderRepository repository) {
        this.repository = repository;
    }

    Order place(String id, BigDecimal total) {
        Order order = new Order(id, total);
        repository.save(order);
        return order;
    }

    List<Order> all() {
        return repository.findAll();
    }
}

When to use it

  • Replace a real payment gateway with an in-memory fake in tests without changing the services that use it.
  • Find out at startup, not on the first request, that a repository bean is missing from the context.
  • Explain why @Transactional is ignored on an object that was created with new instead of injected.

More examples

A context without Spring Boot

The container is an ordinary object you can create, use and close. Spring Boot does the same thing for you, then adds auto-configuration, external configuration and an embedded server on top. Seeing it without Boot makes it clear that nothing magical happens: definitions go in, objects come out, and closing the context tears them down.

Example · java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

public class PlainContextDemo {

    public static void main(String[] args) {
        // try-with-resources closes the context, which runs destroy callbacks.
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            Greeter greeter = context.getBean(Greeter.class);
            System.out.println(greeter.greet("Ada"));
        }
    }
}

@Configuration
class AppConfig {
    @Bean
    Greeter greeter() {
        return new Greeter("Hello");
    }
}

class Greeter {
    private final String salutation;

    Greeter(String salutation) {
        this.salutation = salutation;
    }

    String greet(String name) {
        return salutation + ", " + name;
    }
}

Checking what a context contains in a test

ApplicationContextRunner starts a small context in milliseconds, with only the beans you list. It is the quickest way to prove wiring works, for example that a bean is created only when a property is set, without paying for a full @SpringBootTest.

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.stereotype.Service;

class InvoiceServiceContextTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
            .withBean(TaxCalculator.class)
            .withBean(InvoiceService.class);

    @Test
    void wiresInvoiceServiceWithItsCalculator() {
        runner.run(context -> {
            assertThat(context).hasSingleBean(InvoiceService.class);
            assertThat(context.getBean(InvoiceService.class).totalWithTax(100)).isEqualTo(120);
        });
    }
}

class TaxCalculator {
    int tax(int net) {
        return net / 5;
    }
}

@Service
class InvoiceService {
    private final TaxCalculator calculator;

    InvoiceService(TaxCalculator calculator) {
        this.calculator = calculator;
    }

    int totalWithTax(int net) {
        return net + calculator.tax(net);
    }
}

Listing your own beans at startup

A bean can ask for the ApplicationContext itself like any other dependency. This dev-only runner prints the beans from your own packages, which answers "did Spring pick up my class?" in seconds. In a deployed service, the Actuator beans endpoint gives the same answer without custom code.

Example · java
import java.util.Arrays;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

@Component
@Profile("dev")
public class BeanReport implements ApplicationRunner {

    private final ApplicationContext context;

    public BeanReport(ApplicationContext context) {
        this.context = context;
    }

    @Override
    public void run(ApplicationArguments args) {
        Arrays.stream(context.getBeanDefinitionNames())
                .filter(name -> context.getType(name) != null)
                .filter(name -> context.getType(name).getPackageName().startsWith("com.example"))
                .sorted()
                .forEach(name -> System.out.println(name + " -> " + context.getType(name).getSimpleName()));
    }
}

Discussion

  • Be the first to comment on this lesson.