Understanding SpringBoot Extension Points: ApplicationContextInitializer vs ApplicationListener

This article explains the two core SpringBoot extension points—ApplicationContextInitializer for pre‑startup configuration and ApplicationListener for post‑event handling—detailing their purpose, execution timing, source code, registration methods, ordering control, common use cases, and a side‑by‑side comparison to help developers master SpringBoot's flexible extension mechanism.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
Understanding SpringBoot Extension Points: ApplicationContextInitializer vs ApplicationListener

SpringBoot Extension Points

SpringBoot reserves hook interfaces at key stages (startup, container initialization, event publishing) that allow developers to inject custom logic without modifying the framework source. The two fundamental extension points are ApplicationContextInitializer and ApplicationListener .

ApplicationContextInitializer – Pre‑Initialization Hook

Interface source

@FunctionalInterface
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
    /**
     * Execute custom configuration before the application context is initialized.
     */
    void initialize(C applicationContext);
}

Key points

Generic type C must extend ConfigurableApplicationContext (e.g., AnnotationConfigServletWebServerApplicationContext).

The initialize() method receives the context and can modify it.

Execution timing: called in prepareContext() after the environment is prepared but before any bean scanning or loading.

SpringBoot automatically loads built‑in initializers and supports user‑defined ones via component scanning, manual registration, or configuration files.

Execution timing details

Create SpringApplication instance (built‑in initializers already loaded).

Publish ApplicationStartingEvent.

Publish ApplicationEnvironmentPreparedEvent (environment ready).

Print banner.

Create ApplicationContext.

Invoke initialize() on all ApplicationContextInitializer instances (context not yet initialized, beans not loaded).

Refresh the context (bean scanning, loading, initialization).

Start the web container (for web applications).

Publish ApplicationReadyEvent.

Because the initializer runs before bean creation, no bean instances can be retrieved inside initialize().

Custom implementations (three common ways)

1️⃣ Implement the interface and annotate with @Component

import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.stereotype.Component;

@Component
public class MyCustomInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment env = applicationContext.getEnvironment();
        env.getSystemProperties().put("custom.key", "custom.value");
        env.getSystemProperties().put("spring.profiles.active", "dev");
        System.out.println("Custom ApplicationContextInitializer executed: pre‑initialization configuration completed");
        System.out.println("Added environment variable: custom.key = " + env.getProperty("custom.key"));
    }
}

2️⃣ Add initializer manually via SpringApplication

@SpringBootApplication
public class SpringBootExtensionApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(SpringBootExtensionApplication.class);
        app.addInitializers(new MyManualInitializer());
        app.run(args);
    }

    static class MyManualInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext ctx) {
            String[] basePackages = ctx.getEnvironment().getProperty("spring.component-scan.base-packages", String[].class);
            if (basePackages == null) {
                basePackages = new String[]{"com.example.demo"};
            }
            System.out.println("Manual initializer executed: bean scan packages = " + String.join(",", basePackages));
        }
    }
}

3️⃣ Load via application.properties

# application.properties
context.initializer.classes=com.example.demo.initializer.MyConfigInitializer,com.example.demo.initializer.MyAnotherInitializer
public class MyConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        ConfigurableEnvironment env = ctx.getEnvironment();
        Map<String, Object> configMap = new HashMap<>();
        configMap.put("spring.datasource.url", "jdbc:mysql://localhost:3306/test");
        configMap.put("spring.datasource.username", "root");
        configMap.put("spring.datasource.password", "123456");
        env.getPropertySources().addFirst(new MapPropertySource("customConfig", configMap));
        System.out.println("Config file initializer 1 executed: added custom configuration source");
    }
}

public class MyAnotherInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        ctx.registerShutdownHook();
        System.out.println("Config file initializer 2 executed: registered shutdown hook");
    }
}

Controlling execution order

@Component
@Order(1)
public class FirstInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        System.out.println("FirstInitializer executed (order 1)");
    }
}

@Component
@Order(2)
public class SecondInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        System.out.println("SecondInitializer executed (order 2)");
    }
}

Common use cases (four scenarios)

Scenario 1 – Dynamically set environment variables and profiles

@Component
public class EnvConfigInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        ConfigurableEnvironment env = ctx.getEnvironment();
        String envVar = System.getenv("SPRING_ENV");
        if (envVar == null || envVar.isEmpty()) {
            env.setActiveProfiles("prod");
        } else {
            env.setActiveProfiles(envVar);
        }
        env.getSystemProperties().put("app.version", "1.0.0");
        System.out.println("Current active profiles: " + String.join(",", env.getActiveProfiles()));
    }
}

Scenario 2 – Add custom configuration source (e.g., Nacos, Apollo)

public class ConfigCenterInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        ConfigurableEnvironment env = ctx.getEnvironment();
        Map<String, Object> configMap = new HashMap<>();
        configMap.put("spring.datasource.url", "jdbc:mysql://localhost:3306/test");
        configMap.put("spring.datasource.username", "root");
        configMap.put("spring.datasource.password", "123456");
        env.getPropertySources().addFirst(new MapPropertySource("nacosConfig", configMap));
        System.out.println("Loaded configuration from config center: datasource URL = " + env.getProperty("spring.datasource.url"));
    }
}

Scenario 3 – Modify bean scan packages

@Component
public class ScanPathInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        if (ctx instanceof AnnotationConfigApplicationContext) {
            AnnotationConfigApplicationContext ac = (AnnotationConfigApplicationContext) ctx;
            ac.scan("com.example.demo.module1", "com.example.demo.module2");
            System.out.println("Dynamically added bean scan paths: com.example.demo.module1, com.example.demo.module2");
        }
    }
}

Scenario 4 – Register context shutdown hook

@Component
public class ShutdownHookInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext ctx) {
        ctx.registerShutdownHook();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.out.println("Application shutting down, performing resource cleanup...");
        }));
        System.out.println("Shutdown hook registration completed");
    }
}

ApplicationListener – Event‑Driven Hook

Interface source

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    /**
     * Logic executed when the event is published.
     */
    void onApplicationEvent(E event);
}

Key points

Generic type E must extend ApplicationEvent (built‑in or custom events).

The onApplicationEvent() method receives the event instance, allowing access to data carried by the event.

Execution timing depends on when the specific event is published.

SpringBoot automatically loads built‑in listeners and supports custom listeners via component scanning, @EventListener, manual addition, or configuration files.

Execution timing of core events ApplicationStartingEvent – earliest stage; no environment or context available. ApplicationEnvironmentPreparedEvent – environment ready; listeners can read Environment but not the context. ApplicationContextInitializedEvent – context initialized but beans not loaded. ContextRefreshedEvent – all beans created; listeners can retrieve any bean. ServletWebServerInitializedEvent – web container ready (web apps only). ApplicationStartedEvent – startup completed; listeners can run post‑startup tasks. ApplicationReadyEvent – application ready to receive requests; final initialization tasks can be performed.

Custom implementations (four common ways)

1️⃣ Implement the interface and annotate with @Component

import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;

@Component
public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) event.getApplicationContext();
        String[] beanNames = ctx.getBeanDefinitionNames();
        System.out.println("ContextRefreshedEvent triggered: " + beanNames.length + " beans loaded");
        initCache(ctx);
    }

    private void initCache(ConfigurableApplicationContext ctx) {
        CacheService cacheService = ctx.getBean(CacheService.class);
        cacheService.initDictCache();
        System.out.println("Cache initialization completed");
    }
}

2️⃣ Use @EventListener annotation

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.boot.context.event.ApplicationStartingEvent;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.context.ApplicationEvent;

@Component
public class ApplicationReadyListener {
    @EventListener(ApplicationReadyEvent.class)
    public void handleApplicationReady(ApplicationReadyEvent event) {
        String appName = event.getApplicationContext().getEnvironment().getProperty("spring.application.name");
        System.out.println("ApplicationReadyEvent triggered: application " + appName + " is ready to receive requests!");
        sendStartNotice(appName, event);
    }

    private void sendStartNotice(String appName, ApplicationReadyEvent event) {
        System.out.println("Sending startup success notice: app " + appName + " started on port " +
                event.getApplicationContext().getEnvironment().getProperty("server.port"));
    }

    @EventListener({ApplicationStartingEvent.class, ApplicationFailedEvent.class})
    public void handleMultiEvent(ApplicationEvent event) {
        if (event instanceof ApplicationStartingEvent) {
            System.out.println("Application start beginning...");
        } else if (event instanceof ApplicationFailedEvent) {
            System.out.println("Application start failed, handling exception...");
        }
    }
}

3️⃣ Add listener manually via SpringApplication

@SpringBootApplication
public class SpringBootExtensionApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(SpringBootExtensionApplication.class);
        app.addListeners(new MyManualListener());
        app.run(args);
    }

    static class MyManualListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
        @Override
        public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
            ConfigurableEnvironment env = event.getEnvironment();
            env.getPropertySources().addFirst(new MapPropertySource("manualConfig",
                    Collections.singletonMap("server.port", "8081")));
            System.out.println("Manual listener executed: server port changed to 8081");
        }
    }
}

4️⃣ Load via configuration file

# application.properties
context.listener.classes=com.example.demo.listener.MyConfigListener,com.example.demo.listener.MyShutdownListener
public class MyConfigListener implements ApplicationListener<ApplicationFailedEvent> {
    @Override
    public void onApplicationEvent(ApplicationFailedEvent event) {
        Throwable ex = event.getException();
        System.out.println("Application failed: " + ex.getMessage());
    }
}

public class MyShutdownListener implements ApplicationListener<ContextClosedEvent> {
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println("Application shutting down, performing resource cleanup...");
    }
}

Controlling listener execution order

@Component
public class FirstListener implements ApplicationListener<ApplicationReadyEvent>, Ordered {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        System.out.println("FirstListener executed (order 1)");
    }
    @Override
    public int getOrder() { return 1; }
}

@Component
@Order(2)
public class SecondListener implements ApplicationListener<ApplicationReadyEvent> {
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
        System.out.println("SecondListener executed (order 2)");
    }
}

Common use cases (five scenarios)

Scenario 1 – Execute initialization tasks after startup

@Component
public class InitTaskListener {
    @EventListener(ApplicationReadyEvent.class)
    public void handleInitTask(ApplicationReadyEvent event) {
        loadCache();
        initDict();
        startScheduledTask();
        System.out.println("Application startup completed, initialization tasks executed");
    }
    private void loadCache() { /* simulate cache loading */ }
    private void initDict() { /* simulate dictionary initialization */ }
    private void startScheduledTask() { /* simulate scheduled task start */ }
}

Scenario 2 – Modify environment after it is prepared

@Component
public class EnvModifyListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent> {
    @Override
    public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
        ConfigurableEnvironment env = event.getEnvironment();
        if ("dev".equals(System.getenv("ENV"))) {
            env.setActiveProfiles("dev");
        }
        if (env.getProperty("server.port") == null) {
            env.getPropertySources().addFirst(new MapPropertySource("defaultPort",
                    Collections.singletonMap("server.port", "8080")));
        }
    }
}

Scenario 3 – Handle startup failure

@Component
public class FailListener implements ApplicationListener<ApplicationFailedEvent> {
    @Override
    public void onApplicationEvent(ApplicationFailedEvent event) {
        Throwable ex = event.getException();
        System.err.println("Application startup failed, stack trace:");
        ex.printStackTrace();
        sendAlarmNotice(ex.getMessage());
    }
    private void sendAlarmNotice(String message) {
        System.out.println("Sending alarm notice: startup failure reason – " + message);
    }
}

Scenario 4 – Clean up resources on shutdown

@Component
public class ShutdownListener implements ApplicationListener<ContextClosedEvent> {
    @Autowired
    private DataSource dataSource;
    @Autowired
    private CacheService cacheService;

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        System.out.println("Application shutting down, starting resource cleanup...");
        try {
            if (dataSource != null && !dataSource.isClosed()) {
                dataSource.close();
                System.out.println("Database connection closed successfully");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        cacheService.clearCache();
        System.out.println("Cache cleared successfully");
    }
}

Scenario 5 – Decouple business logic with custom events

// 1. Custom event definition
public class UserRegisterEvent extends ApplicationEvent {
    private final User user;
    public UserRegisterEvent(Object source, User user) {
        super(source);
        this.user = user;
    }
    public User getUser() { return user; }
}

// 2. Listener that sends a notification after registration
@Component
public class RegisterNoticeListener {
    @EventListener(UserRegisterEvent.class)
    public void handleRegisterNotice(UserRegisterEvent event) {
        User user = event.getUser();
        sendNotice(user);
        System.out.println("User " + user.getUsername() + " registered successfully, notification sent");
    }
    private void sendNotice(User user) { /* simulate sending notice */ }
}

// 3. Service that publishes the event
@Service
public class UserService implements ApplicationEventPublisherAware {
    private ApplicationEventPublisher publisher;
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }
    public void register(User user) {
        saveUser(user);
        publisher.publishEvent(new UserRegisterEvent(this, user));
    }
    private void saveUser(User user) { /* simulate user persistence */ }
}

Comparison summary

Core role : Initializer – pre‑initialization of the application context; Listener – post‑event handling.

Execution timing : Initializer runs after environment preparation and before bean loading; Listener runs whenever the associated event is published, covering the whole startup lifecycle.

Accessible resources : Initializer can access Environment but cannot retrieve beans; Listener can access any resources carried by the event, including beans.

Typical scenarios : Initializer – dynamic environment activation, adding configuration sources, modifying bean scan paths, registering shutdown hooks; Listener – initialization tasks, exception handling, resource cleanup, business decoupling via custom events.

Loading methods : Both support @Component, manual registration via SpringApplication, and configuration‑file registration ( context.initializer.classes / context.listener.classes).

Order control : Both can use @Order or implement Ordered; configuration‑file order follows the declared order.

Final summary

ApplicationContextInitializer performs pre‑configuration before the context is created; it cannot access beans and is ideal for preparing the environment. ApplicationListener reacts to events after they are published; it can access beans and other resources and is suited for tasks such as initialization, error handling, resource cleanup, and business decoupling. Together they provide a powerful, non‑intrusive extension mechanism for SpringBoot applications.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaconfigurationSpringBootevent handlingextension pointsApplicationContextInitializerApplicationListener
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.