Spring Boot 钩子全集实战(六):SpringApplicationRunListener.contextPrepared()详解

Spring Boot 钩子全集实战(六):SpringApplicationRunListener.contextPrepared() 详解

在上一篇中,我们深入剖析了 ApplicationContextInitializer 这一容器初始化前的核心扩展点,实现了容器安全加固、Bean 定义预处理等高阶能力。今天,我们将继续跟进 Spring Boot 启动生命周期,解析 SpringApplicationRunListener 接口的又一关键方法contextPrepared()

一、什么是 SpringApplicationRunListener.contextPrepared()

SpringApplicationRunListener.contextPrepared() 是 Spring Boot 启动流程中,衔接 ApplicationContextInitializerApplicationContext 刷新前的关键回调方法,其触发时机和核心特征如下:

  • 触发时机ApplicationContext 已创建完成、ApplicationContextInitializer 已全部执行完毕,但容器尚未调用 refresh() 方法;
  • 核心状态 :容器骨架已搭建,Bean 定义尚未加载,环境(Environment)已完全就绪;
  • 执行顺序 :晚于 ApplicationContextInitializer.initialize(),早于 SpringApplicationRunListener.contextLoaded() 和容器 refresh()
  • 核心能力 :可对 ApplicationContext 进行最终定制、添加容器级监听器、提前绑定资源、拦截 Bean 加载前置流程。

核心价值 :作为容器刷新前的 "最后一道关卡",它弥补了 ApplicationContextInitializer 与容器加载之间的扩展空白,可实现容器行为的最终校准、监听器动态注册等场景。

二、场景:容器启动权限校验(防止非授权环境 / 用户启动应用)

业务痛点

  1. 生产环境应用包可能被误拷贝到测试环境以外的非授权服务器(如员工本地机器、第三方服务器)启动,导致敏感配置泄露;
  2. 部分核心应用(如支付系统、用户中心)仅允许指定运维用户启动,普通用户启动可能引发操作风险;
  3. 传统权限校验多在 Bean 初始化后执行,此时容器已加载部分资源,校验失败后需额外清理,效率低下。

解决方案

利用 contextPrepared() 方法,在容器加载 Bean 前执行「服务器 IP 白名单校验」+「启动用户白名单校验」,校验失败直接终止应用启动,从源头阻断非授权访问。

步骤 1:实现权限校验逻辑(在 contextPrepared() 中)

修改 CustomContextPreparedRunListener,添加权限校验逻辑:

java 复制代码
package com.example.demo.listener;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/** * 自定义 SpringApplicationRunListener,实现容器启动权限校验 */
public class CustomContextPreparedRunListener implements SpringApplicationRunListener {

    // 必须提供的构造方法
    public CustomContextPreparedRunListener(SpringApplication application, String[] args) {
    }

    // 服务器 IP 白名单(生产环境可从配置中心动态拉取)
    private static final Set<String> SERVER_IP_WHITELIST = new HashSet<>(Arrays.asList(
            "192.168.1.100", "192.168.1.101", "172.16.0.50" // 生产授权服务器 IP
    ));

    // 启动用户白名单(生产环境可从配置中心动态拉取)
    private static final Set<String> USER_WHITELIST = new HashSet<>(Arrays.asList(
            "prod_ops", "admin", "payment_admin" // 授权运维用户
    ));

    /**     * 核心方法:contextPrepared 实现启动权限校验     */
    @Override
    public void contextPrepared(ConfigurableApplicationContext context) {
        System.out.println("[ContextPrepared] 开始执行容器启动权限校验...");
        ConfigurableEnvironment environment = context.getEnvironment();
        String currentEnv = environment.getActiveProfiles().length > 0
                ? environment.getActiveProfiles()[0] : "prod";

        // 仅对生产环境执行严格权限校验(开发/测试环境跳过)
        if ("prod".equals(currentEnv)) {
            try {
                // 1. 服务器 IP 白名单校验
                validateServerIp();
                // 2. 启动用户白名单校验
                validateStartupUser();

                System.out.println("[ContextPrepared] 权限校验通过,允许启动应用");
            } catch (SecurityException e) {
                System.err.println("[ContextPrepared] 权限校验失败:" + e.getMessage());
                // 校验失败,直接终止 JVM 进程(避免容器继续加载资源)
                System.exit(1);
            }
        } else {
            System.out.println("[ContextPrepared] 当前为非生产环境(" + currentEnv + "),跳过严格权限校验");
        }
    }

    /**     * 服务器 IP 白名单校验     */
    private void validateServerIp() {
        try {
            // 获取当前服务器本机 IP
            InetAddress localHost = InetAddress.getLocalHost();
            String serverIp = localHost.getHostAddress();
            System.out.println("[ContextPrepared] 当前服务器 IP:" + serverIp);

            if (!SERVER_IP_WHITELIST.contains(serverIp)) {
                throw new SecurityException("当前服务器 IP(" + serverIp + ")不在授权白名单内,禁止启动");
            }
        } catch (UnknownHostException e) {
            throw new SecurityException("获取服务器 IP 失败:" + e.getMessage());
        }
    }

    /**     * 启动用户白名单校验     */
    private void validateStartupUser() {
        // 获取当前启动应用的操作系统用户
        String currentUser = System.getProperty("user.name");
        System.out.println("[ContextPrepared] 当前启动用户:" + currentUser);

        if (!USER_WHITELIST.contains(currentUser)) {
            throw new SecurityException("当前用户(" + currentUser + ")不在授权白名单内,禁止启动");
        }
    }

    // 其他生命周期方法(省略)
    @Override
    public void contextLoaded(ConfigurableApplicationContext context) {}

    @Override
    public void failed(ConfigurableApplicationContext context, Throwable exception) {}
}
步骤 2:注册 RunListener
plaintext 复制代码
org.springframework.boot.SpringApplicationRunListener=\
com.example.demo.listener.CustomContextPreparedRunListener
步骤3: 输出结果

非授权 IP 启动(生产环境):

plaintext 复制代码
[ContextPrepared] 开始执行容器启动权限校验...
[ContextPrepared] 当前服务器 IP:127.0.0.1
[ContextPrepared] 权限校验失败:当前服务器 IP(127.0.0.1)不在授权白名单内,禁止启动
生产价值
  1. 校验时机早(容器加载 Bean 前),避免非授权启动后清理资源的额外开销,提升安全校验效率;
  2. 双重校验(IP + 用户),形成完整的启动权限管控体系,有效防止敏感应用被误启动或恶意启动;
  3. 支持环境差异化校验(仅生产环境严格校验),不影响开发 / 测试效率,兼顾安全性与易用性;
  4. 白名单可扩展为从配置中心动态拉取,无需修改代码即可更新授权列表,提升维护灵活性。

三、总结

SpringApplicationRunListener.contextPrepared() 是 Spring Boot 启动流程中 容器刷新前的最终定制入口 ,它承接 ApplicationContextInitializer 的执行结果,为容器加载 Bean 定义做好最后的准备。其与 ApplicationContextInitializer 配合,形成了 "容器创建 → 初始化 → 最终定制 → 加载 Bean" 的完整扩展链路,是构建高可用、高灵活度企业级应用的重要支撑。

📌 关注我,每天 5 分钟,带你从 Java 小白变身编程高手!

👉 点赞 + 关注 + 转发,让更多小伙伴一起进步!

👉 私信 "SpringBoot 钩子源码" 获取完整源码!

相关推荐
小小仙。1 小时前
IT自学第十八天
java·开发语言·算法
我命由我123451 小时前
Android 开发 - FragmentPagerAdapter、Pair、ClipboardManager、PopupWindow
android·java·java-ee·kotlin·android studio·android-studio·android runtime
扶苏-su2 小时前
Java--打印流
java·开发语言
Kevin-anycode2 小时前
如何将自己的应用上传文件功能对接到群辉的NAS上
java·unix
幽络源小助理2 小时前
SpringBoot+Vue旅游推荐系统源码 | 幽络源
java·开发语言·spring boot
丶小鱼丶2 小时前
Java基础之【排序算法】
java·算法
csdnfanguyinheng2 小时前
生产级的考试系统
java·springboot·考试
浮尘笔记2 小时前
Go语言上下文:context.Context类型详解
开发语言·后端·golang
康小庄2 小时前
通过NGINX实现将小程序HTTPS请求转为内部HTTP请求
java·spring boot·nginx·spring·http·小程序