Spring Boot 钩子全集实战(六):SpringApplicationRunListener.contextPrepared() 详解
在上一篇中,我们深入剖析了 ApplicationContextInitializer 这一容器初始化前的核心扩展点,实现了容器安全加固、Bean 定义预处理等高阶能力。今天,我们将继续跟进 Spring Boot 启动生命周期,解析 SpringApplicationRunListener 接口的又一关键方法 :contextPrepared()。
一、什么是 SpringApplicationRunListener.contextPrepared()?
SpringApplicationRunListener.contextPrepared() 是 Spring Boot 启动流程中,衔接 ApplicationContextInitializer 与 ApplicationContext 刷新前的关键回调方法,其触发时机和核心特征如下:
- 触发时机 :
ApplicationContext已创建完成、ApplicationContextInitializer已全部执行完毕,但容器尚未调用refresh()方法; - 核心状态 :容器骨架已搭建,Bean 定义尚未加载,环境(
Environment)已完全就绪; - 执行顺序 :晚于
ApplicationContextInitializer.initialize(),早于SpringApplicationRunListener.contextLoaded()和容器refresh(); - 核心能力 :可对
ApplicationContext进行最终定制、添加容器级监听器、提前绑定资源、拦截 Bean 加载前置流程。
✅ 核心价值 :作为容器刷新前的 "最后一道关卡",它弥补了
ApplicationContextInitializer与容器加载之间的扩展空白,可实现容器行为的最终校准、监听器动态注册等场景。
二、场景:容器启动权限校验(防止非授权环境 / 用户启动应用)
业务痛点
- 生产环境应用包可能被误拷贝到测试环境以外的非授权服务器(如员工本地机器、第三方服务器)启动,导致敏感配置泄露;
- 部分核心应用(如支付系统、用户中心)仅允许指定运维用户启动,普通用户启动可能引发操作风险;
- 传统权限校验多在 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)不在授权白名单内,禁止启动
生产价值
- 校验时机早(容器加载 Bean 前),避免非授权启动后清理资源的额外开销,提升安全校验效率;
- 双重校验(IP + 用户),形成完整的启动权限管控体系,有效防止敏感应用被误启动或恶意启动;
- 支持环境差异化校验(仅生产环境严格校验),不影响开发 / 测试效率,兼顾安全性与易用性;
- 白名单可扩展为从配置中心动态拉取,无需修改代码即可更新授权列表,提升维护灵活性。
三、总结
SpringApplicationRunListener.contextPrepared() 是 Spring Boot 启动流程中 容器刷新前的最终定制入口 ,它承接 ApplicationContextInitializer 的执行结果,为容器加载 Bean 定义做好最后的准备。其与 ApplicationContextInitializer 配合,形成了 "容器创建 → 初始化 → 最终定制 → 加载 Bean" 的完整扩展链路,是构建高可用、高灵活度企业级应用的重要支撑。
📌 关注我,每天 5 分钟,带你从 Java 小白变身编程高手!
👉 点赞 + 关注 + 转发,让更多小伙伴一起进步!
👉 私信 "SpringBoot 钩子源码" 获取完整源码!
