An ApplicationContext creates and connects registered bean definitions and manages the lifecycle of container-owned objects.
Spring ApplicationContext: construct and close an object graph
This lesson uses the downloadable source kit: Java 21, Spring Boot 4.0.8 and its managed Spring Framework 7 dependencies. The version is pinned for repeatable builds.
Register before refreshing
The report fixture registers an exporter and a report service, then refreshes the context. Refresh is a lifecycle transition: definitions become usable instances and missing dependencies can fail startup. Calling getBean before completing that transition is not a way to test business behavior.
A Java interface separates the report contract from its exporter implementation. The context assembles that graph; the report still performs an ordinary Java method call. Avoid retrieving collaborators from a global context inside the report, because the constructor would then hide what the object actually requires.
Close the owner
The complete ContainerContracts program opens a context in try-with-resources. Its singleton ClosingChannel is destroyed when the context closes, and the displayed closed flag confirms that lifecycle boundary. A stopped test must not keep container-owned pools and channels alive.
A context is not a database transaction or a request scope. Reusing a singleton report gives shared object identity within that definition and context, but says nothing about the safety of mutable fields. Ownership rules still belong to the application.
Checked source
package in.aitrove.learning;
import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.PayloadApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.aop.framework.ProxyFactory;
import org.aopalliance.intercept.MethodInterceptor;
public class ContainerContracts {
public interface Exporter { String format(); }
public static class CsvExporter implements Exporter { public String format() { return "csv"; } }
public static class ReceiptReport {
private final Exporter exporter;
public ReceiptReport(Exporter exporter) { this.exporter = exporter; }
public String render() { return exporter.format() + ":receipt"; }
}
public static class ImportBuffer { private final int capacity = 32; public int capacity() { return capacity; } }
public static class ClosingChannel implements DisposableBean {
public boolean closed; public void destroy() { closed = true; }
}
public interface Counter { void outer(); void inner(); }
public static class CounterTarget implements Counter {
public void outer() { inner(); }
public void inner() {}
}
public static void main(String[] args) {
ClosingChannel channel;
try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext()) {
context.registerBean("csv", Exporter.class, CsvExporter::new);
context.registerBean(ReceiptReport.class);
context.registerBean(ImportBuffer.class, ImportBuffer::new,
definition -> definition.setScope(BeanDefinition.SCOPE_PROTOTYPE));
context.registerBean(ClosingChannel.class); context.refresh();
channel = context.getBean(ClosingChannel.class);
System.out.println(context.getBean(ReceiptReport.class).render());
ObjectProvider<ImportBuffer> buffers = context.getBeanProvider(ImportBuffer.class);
System.out.println("different=" + (buffers.getObject() != buffers.getObject()));
AtomicInteger messages = new AtomicInteger();
context.addApplicationListener((ApplicationListener<PayloadApplicationEvent<String>>) event -> messages.incrementAndGet());
context.publishEvent("receipt-created");
System.out.println("events=" + messages.get());
}
System.out.println("closed=" + channel.closed);
AtomicInteger advised = new AtomicInteger();
ProxyFactory factory = new ProxyFactory(new CounterTarget());
factory.addAdvice((MethodInterceptor) invocation -> { advised.incrementAndGet(); return invocation.proceed(); });
Counter proxy = (Counter) factory.getProxy(); proxy.outer();
System.out.println("advised=" + advised.get());
}
}Test the boundary
Run mvn test in the source-kit directory. CoreBoundaryTest checks the behavior described here. Java excerpts belong to the named source-kit classes; they are not independent source files unless the complete class is shown.
Costs and boundaries
Startup cost depends on the number of definitions, dependencies and initialization work. This four-bean fixture does not measure a production application’s startup time. Do not build a new context per report request.
Common Mistakes
- Register definitions before refresh.
- Close a context created by a test.
- Do not mistake container reuse for thread safety.
Read next
Constructor injection, Bean scopes, Java interfaces and replaceable behavior.
