昨天晚上十一点多吧,我在公司楼下抽烟(别学哈),我们组小李突然在群里吼:哥,SpringBoot启动完我得先把缓存预热、把一堆字典拉下来,不然早高峰一来接口就跟“冷启动”一样抖三抖……我当时第一反应就是:你这是典型的“启动时自动执行一段代码”,SpringBoot里能玩的花样其实不少,选不对还真容易踩坑。
我就按我平时线上爱用的几种方式给他捋了一遍,顺便把容易翻车的点也夹带私货说一下,免得你们也半夜被call。
先说最常见的俩:CommandLineRunner 和 ApplicationRunner。这俩我用得最多,因为它俩的语义很“Boot”,就是容器起来了,参数也准备好了,你给我跑一段。区别嘛……怎么说呢,一个拿的是原始 String[] args,一个拿的是解析后的 ApplicationArguments,就这点差别。代码长这样(我随手写的,别纠结命名):
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(10) // 数字越小越早执行
publicclassWarmupByCmdRunnerimplementsCommandLineRunner{
@Override
publicvoidrun(String... args){
System.out.println("[WarmupByCmdRunner] args=" + String.join(",", args));
// 这里放预热逻辑:拉字典、建本地缓存、提前连一下下游
}
}
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
@Component
@Order(20)
publicclassWarmupByAppRunnerimplementsApplicationRunner{
@Override
publicvoidrun(ApplicationArguments args){
if (args.containsOption("skipWarmup")) {
System.out.println("[WarmupByAppRunner] skip warmup");
return;
}
System.out.println("[WarmupByAppRunner] optionNames=" + args.getOptionNames());
// 业务预热
}
}
对了,@Order 这玩意儿很关键,小李之前写了俩 Runner,结果执行顺序飘来飘去,他还以为是多线程……其实就是没排序。还有个坑:Runner里你别整那种特别慢的活儿(比如全量扫库十分钟),不然启动卡住,k8s readiness 过不去,你自己把自己“假死”了。
然后是 @PostConstruct。这个很多人喜欢,因为看起来最省事:Bean一创建就执行。它的触发点更早一点,属于“这个Bean初始化完成就干活”。适合做一些轻量的本地初始化,比如检查配置、准备内存结构。像这样:
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Component;
@Component
publicclassLocalInitByPostConstruct{
@PostConstruct
publicvoidinit(){
System.out.println("[PostConstruct] build local map...");
// 轻量初始化,比如构建一些只读Map、校验配置
}
}
但我得插一句哈:@PostConstruct 里别指望事务、AOP 那些一定按你想的来(尤其是你在这里调用自己类里的 @Transactional 方法,那基本等于没写),因为它发生得比较早,而且你容易绕过代理,懂的都懂,不懂也别硬背,记住“这里适合轻,别做重”。
再一个是 InitializingBean,跟 @PostConstruct 很像,只是写法从注解变成接口,老项目里挺常见:
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
@Component
publicclassInitByInitializingBeanimplementsInitializingBean{
@Override
publicvoidafterPropertiesSet(){
System.out.println("[InitializingBean] properties set, do init...");
}
}
你要问我更推荐哪个?我一般更偏向 @PostConstruct,少侵入一点;但如果你做的是框架型组件、想明确表达“我就是初始化钩子”,接口也行。
接着聊事件监听,这块就比较“讲究时机”了。很多启动后要做的事,其实更适合在 应用真正可对外提供服务之后 再做,比如你想等 WebServer 起好、端口已经能接请求了,再去异步预热一部分东西。那我会用 ApplicationReadyEvent 或者 ApplicationStartedEvent 之类的事件。
我常用的是 ApplicationReadyEvent,因为它更靠后:Runner跑完后它才会来。写法可以用 ApplicationListener:
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
@Component
publicclassWarmupOnReadyEventimplementsApplicationListener<ApplicationReadyEvent> {
@Override
publicvoidonApplicationEvent(ApplicationReadyEvent event){
System.out.println("[ApplicationReadyEvent] app is ready, start warmup...");
// 这里适合做:打点、通知、异步预热、延迟加载
}
}
或者你嫌接口丑,就用 @EventListener:
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
publicclassWarmupOnReadyEvent2{
@EventListener(ApplicationReadyEvent.class)
publicvoidonReady() {
System.out.println("[@EventListener] ready event received");
}
}
这里也有坑:你监听 ContextRefreshedEvent 的话,在某些场景可能触发多次(比如父子容器),新同学特别容易“预热跑两遍”,然后缓存构建重复、日志爆炸。你要真想用刷新事件,至少加个原子标记防重:
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicBoolean;
@Component
publicclassOnceOnContextRefresh{
privatefinal AtomicBoolean once = new AtomicBoolean(false);
@EventListener(ContextRefreshedEvent.class)
publicvoidonRefresh() {
if (!once.compareAndSet(false, true)) return;
System.out.println("[ContextRefreshedEvent] only once");
}
}
还有个偏“底层”的钩子,叫 SmartInitializingSingleton。这玩意儿我一般用在:你希望所有单例Bean都初始化完成之后再做点事,比如你要扫一遍所有实现了某接口的Bean,做注册表、做路由表。它比你自己在某个Bean里乱搞更稳:
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
publicclassRegistryBySmartInitSingletonimplementsSmartInitializingSingleton{
privatefinal ApplicationContext context;
publicRegistryBySmartInitSingleton(ApplicationContext context){
this.context = context;
}
@Override
publicvoidafterSingletonsInstantiated(){
Map<String, Runnable> tasks = context.getBeansOfType(Runnable.class);
System.out.println("[SmartInitializingSingleton] runnable beans=" + tasks.keySet());
// 做注册、建索引表之类的
}
}
说到这儿,小李插了一句:那我到底选哪个啊,我都想用……我说你别全家桶啊兄弟,按“你要的时机”选就行—— 你要 最常规、最可控:Runner(还能排序) 你要 某个Bean自己轻量初始化:@PostConstruct / InitializingBean你要 等应用真正ready再做:ApplicationReadyEvent你要 等所有单例就位后做注册扫描:SmartInitializingSingleton
最后再塞一个我线上很爱用的小技巧:很多启动任务在测试、或某些环境根本不想跑,你别靠注释代码,太原始了,搞个开关最香:
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
@Component
@ConditionalOnProperty(prefix = "app.warmup", name = "enabled", havingValue = "true", matchIfMissing = true)
publicclassToggleWarmupRunnerimplementsCommandLineRunner{
@Override
publicvoidrun(String... args){
System.out.println("[ToggleWarmupRunner] warmup enabled, go!");
}
}
这样你在 application-test.yml 里配个 app.warmup.enabled=false,就安静了,不然单测一跑也跟着预热,跑得你怀疑人生。
哎我说着说着又想起上周那个事故,也是启动时预热把下游打挂了……算了那段先不展开了,等下我还得去回个消息,生产那边又在催日志权限,烦死了。