Spring Framework & the IoC Container

The ApplicationContext in Action

18 min Lesson 7 of 13

The ApplicationContext in Action

You have read about the IoC container and bean definitions in the abstract. Now it is time to write real code: boot an ApplicationContext, pull beans out of it, and understand which concrete implementation to choose in each situation. This lesson focuses on the two things developers do every day with the container — selecting the right ApplicationContext class and retrieving beans correctly.

The Three Core ApplicationContext Implementations

Spring ships with several ApplicationContext implementations. You will encounter three of them constantly:

  • AnnotationConfigApplicationContext — the standard choice for standalone Java applications, integration tests, and any project that configures Spring with @Configuration classes and/or component scanning. It does not depend on a servlet container or a classpath resource.
  • ClassPathXmlApplicationContext — loads XML bean definitions from the classpath. You will mainly see this in legacy codebases. Understanding it helps you read older projects and migrate them.
  • GenericWebApplicationContext / AnnotationConfigWebApplicationContext — used inside a Servlet container (Tomcat, Jetty). Spring MVC's DispatcherServlet creates one of these for you automatically; you rarely construct it by hand.
Spring Boot note: When you use Spring Boot, a SpringApplication.run() call boots an AnnotationConfigServletWebServerApplicationContext (or the reactive variant) behind the scenes. You do not call the constructor yourself, but you can still cast the returned ConfigurableApplicationContext to inspect it. Everything covered here applies directly.

Booting the Context: AnnotationConfigApplicationContext

The simplest way to start a Spring container is to pass your @Configuration class to the constructor:

import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { // Boot the container — registers beans, runs @PostConstruct, resolves dependencies try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) { OrderService orders = ctx.getBean(OrderService.class); orders.placeOrder("item-42", 3); } // ctx.close() triggers @PreDestroy and destroys singleton beans } }

The constructor accepts one or more @Configuration classes, or you can pass a base package name string to enable component scanning:

// Scan everything under com.example AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext("com.example");

Use try-with-resources whenever you control the lifecycle. ApplicationContext extends Closeable, so the try block calls close() for you, triggering graceful shutdown of all singleton beans.

Retrieving Beans: Three Overloads of getBean()

The BeanFactory interface (which ApplicationContext extends) exposes several overloads. Knowing when each is appropriate avoids hard-to-spot bugs:

// 1. By type only — preferred in almost every case OrderService svc = ctx.getBean(OrderService.class); // 2. By name only — returns Object; requires an unsafe cast OrderService svc2 = (OrderService) ctx.getBean("orderService"); // 3. By name AND type — the safe alternative to (2) OrderService svc3 = ctx.getBean("orderService", OrderService.class);
Prefer retrieval by type. It is the most concise, type-safe form and the one Spring itself uses internally. Retrieval by name is useful only when you have multiple beans of the same type registered under different names, or when you are writing framework-level code that does not know the type at compile time.

What Happens When getBean() Is Called

For a singleton bean (the default scope), Spring returns the same fully-initialised instance every time. There is no performance cost to calling getBean() repeatedly — it is a simple map lookup after the first initialisation:

OrderService a = ctx.getBean(OrderService.class); OrderService b = ctx.getBean(OrderService.class); System.out.println(a == b); // true — same instance

For a prototype bean, Spring creates a fresh instance on every call to getBean(). That instance is never cached and its lifecycle is not managed after creation — you are responsible for calling any cleanup logic yourself.

Do not call getBean() inside your own beans. Pulling beans directly out of the context from within application code is called the Service Locator anti-pattern. It couples your class to the Spring API, makes the dependency invisible, and breaks testability. Always declare dependencies as constructor parameters and let Spring inject them. The only legitimate places to call getBean() are application entry points (like main()), framework bootstrap code, and tests.

Listing and Inspecting Beans

ApplicationContext provides several introspection methods that are valuable in debugging and tooling:

// All bean names registered in the context String[] names = ctx.getBeanDefinitionNames(); System.out.println("Total beans: " + names.length); // Check whether a bean of a given type exists boolean hasRepo = ctx.getBeanNamesForType(UserRepository.class).length > 0; // Get the autowire candidate when you know the type UserRepository repo = ctx.getBean(UserRepository.class); // Is this name a singleton? System.out.println(ctx.isSingleton("userRepository")); // true System.out.println(ctx.isPrototype("userRepository")); // false

You can also check whether a bean has been initialised yet or query its type without triggering initialisation using ctx.getType("beanName"). These are powerful tools during debugging — put them in a temporary CommandLineRunner in a Spring Boot app to inspect the live context.

A Realistic Worked Example

Here is a compact but realistic setup — a command-line tool that processes orders — demonstrating how booting, retrieval, and close interact:

// AppConfig.java import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class AppConfig { @Bean public InventoryRepository inventoryRepository() { return new InMemoryInventoryRepository(); } @Bean public OrderService orderService(InventoryRepository repo) { return new OrderService(repo); // constructor injection } } // OrderService.java public class OrderService { private final InventoryRepository repo; public OrderService(InventoryRepository repo) { this.repo = repo; } public void placeOrder(String sku, int qty) { if (repo.reserve(sku, qty)) { System.out.println("Order placed: " + qty + "x " + sku); } else { System.out.println("Insufficient stock for: " + sku); } } } // Main.java import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class Main { public static void main(String[] args) { try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) { OrderService orders = ctx.getBean(OrderService.class); orders.placeOrder("SKU-100", 2); orders.placeOrder("SKU-999", 500); } } }

Notice that main() calls getBean() exactly once — just to get the root service. Everything else flows through constructor injection. This is the correct pattern.

Using ClassPathXmlApplicationContext (Legacy Reading)

If you inherit a pre-Spring-3 codebase or need to consume a legacy XML config file, the boot code looks almost identical:

import org.springframework.context.support.ClassPathXmlApplicationContext; try (var ctx = new ClassPathXmlApplicationContext("applicationContext.xml")) { DataSource ds = ctx.getBean("dataSource", DataSource.class); // ... }

The XML file lives on the classpath (e.g., src/main/resources/applicationContext.xml). The API for retrieving beans is identical — only the bootstrap line changes. When modernising such a project, your first step is usually replacing this line with AnnotationConfigApplicationContext after migrating beans to @Configuration classes.

Summary

The ApplicationContext is the container you interact with every day, even if Spring Boot usually hides it behind SpringApplication.run(). Know that AnnotationConfigApplicationContext is your default for non-Boot, annotation-driven apps; that you retrieve beans primarily by type; and that direct getBean() calls belong only at the outermost entry point. The next lessons build on this foundation by examining naming, qualifiers, and how to handle multiple beans of the same type.