STAGE 23 OF 37

Stage 23: observe bean initialization and shutdown

Learning path / Stage 23 of 37

Before you start: use JDK 21+ and Maven 3.9+. Extract this stage’s project and open a terminal in its pom.xml folder. Complete the setup below, including a database connection where required, then follow the commands in this lesson’s Run this example section. Console lessons finish after printing their result; web lessons keep running until Ctrl+C. Follow the lesson’s run section for profile and database settings.

Follow one managed object from its constructor to initialization, use and cleanup.

Download the complete Stage 23 project · Open the project guide

Before you start

Use JDK 21 or newer and Maven 3.9 or newer. Verify with java -version and mvn -version. Extract this project into its own folder; do not merge its files with another stage.

This lesson reuses the product catalog and Todo responsibilities from Stages 8–14. Only the feature described below is new. Read the ordered explanation before opening the complete files.

Constructor → Dependency injection → PostConstruct → Business method → Context close / PreDestroy

What we change and why

  1. Construct before initializing

    The constructor prints its message first. @PostConstruct runs after construction and dependency injection. Do not expect field-injected dependencies to be ready inside a constructor.

  2. Call business behaviour

    The lesson calls read only after the context is refreshed, so initialization has already completed.

  3. Close the context normally

    Application uses try-with-resources. Closing the context invokes @PreDestroy on the singleton. A forced process kill cannot guarantee cleanup callbacks.

  4. Compare a prototype

    Resolving Draft creates and initializes a prototype. Spring does not track its full destruction lifecycle, so closing this context does not call Draft.destroy. A prototype holding resources needs explicit cleanup by its owner.

Run this example

Open a terminal in the extracted folder containing pom.xml. Run one command at a time.

Terminal · Windows, macOS and Linux
mvn test
mvn spring-boot:run

This is a console lesson. It does not open a web port. Application.main starts a Spring context, calls the demonstration and closes the context. Read the terminal, not a browser. Spring Boot’s Maven plugin is used as a convenient launcher; these examples show Spring Framework directly.

Expected result

Expected output · ignore framework startup messages
Catalog constructor
Catalog initialized
Catalog used
Draft initialized
Catalog destroyed

Object identifiers and elapsed times can vary. Compare the relationship between values rather than copying an identifier.

Understand each complete file

Create the following files at exactly these paths, or use the ZIP. Source files include their imports. The test checks the behaviour described in the lesson.

pom.xml
pom.xml · complete file
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.1.1</version><relativePath/></parent>
  <groupId>com.edupoly</groupId><artifactId>spring-stage-23-lifecycle</artifactId><version>1.0.0</version>
  <properties><java.version>21</java.version></properties>
  <dependencies>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency>
    <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
  </dependencies>
  <build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
src/main/java/com/edupoly/lesson/Catalog.java
src/main/java/com/edupoly/lesson/Catalog.java · complete file
package com.edupoly.lesson;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
@Component public class Catalog {
    private boolean ready;
    public Catalog() { System.out.println("Catalog constructor"); }
    @PostConstruct public void initialize() { ready = true; System.out.println("Catalog initialized"); }
    public boolean ready() { return ready; }
    public void read() { System.out.println("Catalog used"); }
    @PreDestroy public void destroy() { ready = false; System.out.println("Catalog destroyed"); }
}
src/main/java/com/edupoly/lesson/Draft.java
src/main/java/com/edupoly/lesson/Draft.java · complete file
package com.edupoly.lesson;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component @Scope("prototype") public class Draft {
    public boolean destroyed;
    @PostConstruct public void initialize() { System.out.println("Draft initialized"); }
    @PreDestroy public void destroy() { destroyed = true; System.out.println("Draft destroyed"); }
}
src/main/java/com/edupoly/lesson/Lesson.java
src/main/java/com/edupoly/lesson/Lesson.java · complete file
package com.edupoly.lesson;

import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component public class Lesson {
    private final Catalog catalog; private final ApplicationContext context;
    public Lesson(Catalog catalog, ApplicationContext context) { this.catalog = catalog; this.context = context; }
    public void run() { catalog.read(); context.getBean(Draft.class); }
}
src/main/java/com/edupoly/lesson/Application.java
src/main/java/com/edupoly/lesson/Application.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Application {
    public static void main(String[] args) throws Exception {
        try (var context = new AnnotationConfigApplicationContext(AppConfig.class)) {
            context.getBean(Lesson.class).run();
        }
    }
}
src/main/java/com/edupoly/lesson/AppConfig.java
src/main/java/com/edupoly/lesson/AppConfig.java · complete file
package com.edupoly.lesson;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan(basePackages = "com.edupoly.lesson")
public class AppConfig { }
src/test/java/com/edupoly/lesson/LessonTest.java
src/test/java/com/edupoly/lesson/LessonTest.java · complete file
package com.edupoly.lesson;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

class LessonTest {
    @Test void lifecycleInitializesAndDestroysSingletonButNotPrototype() {
        var context = new AnnotationConfigApplicationContext(AppConfig.class);
        Catalog catalog = context.getBean(Catalog.class); Draft draft = context.getBean(Draft.class);
        assertTrue(catalog.ready()); context.close();
        assertFalse(catalog.ready()); assertFalse(draft.destroyed);
    }
}

Try a failure, then correct it

Replace the managed catalog lookup with new Catalog().read(). You see construction and use but no container-driven initialization for that unmanaged object. Restore the managed lookup.

Words you can now explain

WordMeaning in this example
@PostConstructInitialization callback after dependencies are supplied.
@PreDestroyCleanup callback when this managed singleton’s context closes.
LifecycleThe sequence of construction, initialization, use and destruction.

Practice assignment

Add a second prototype lookup. Predict how many initialization messages appear and whether any Draft destroyed message appears.

Read the official documentation →

Concepts used in this stage

Use the right column for a quick reminder, or open a concept above for its full explanation.

Java 21 · Spring Boot 4.1.1 · Edupoly