Boot4升级PostConstruct不跑了

Spring Boot 4:javax.annotation.PostConstruct 为什么不跑了

升级到 Spring Boot 4 之后,有一类故障特别阴:项目能编译、能启动,Bean 也创建了,但你以为写在 @PostConstruct 里的初始化(建连接、预热缓存、注册回调)从头到尾没执行 。日志里不一定有报错;方法上的注解还在;依赖树里甚至还能看到 javax.annotation 相关包。于是排查会先怀疑条件装配、再怀疑启动顺序,最后才发现:注解的包名还停在 javax.*。

这不是偶发配置问题。Spring Framework 7.0 Release Notes 写得很清楚:Removed support for javax.annotation and javax.inject annotations ,原文点名的有 @javax.annotation.Resource、@javax.annotation.PostConstruct、@javax.inject.Inject 等(@PreDestroy 没有被点名,但同属 javax.annotation 包:维护者在 spring-framework#36201 里说明 7.0 是有意移除 JSR-250 支持,并把 javax 命名空间下的 @PostConstruct 和 @PreDestroy 一并算在内;Boot 4.0.8 的最小工程实测它也不会执行)。这条在 Release Notes 的「Upgrading From Spring Framework 6.2 → Removed APIs」下。必须迁到 jakarta.annotation / jakarta.inject 的等价注解。Boot 4 基于 Framework 7 / Jakarta EE 11(见 Boot 4 Migration Guide),这条线会直接打到你的业务代码。

和「启动直接炸」的破坏性变更相比,这种静默失效更伤:回归测试若只断言 HTTP 200,可能覆盖不到「预热是否执行」;等流量上来才发现缓存全是冷的。把包名迁移列为升级必做项,别拖成「有空再扫一眼」。

这类问题还有一层组织成本:值班同学按「启动无 ERROR」签字放行,开发同学按「单元测试全绿」合并,两边都没看错仪表盘,却一起漏掉了「注解语义已经换代」。把 javax.annotation 检索结果是否为零,写进升级 Definition of Done,比事后追日志便宜得多。

一、为什么叫「静默」:编译过 ≠ 生命周期还在

Framework 7 之前,团队若仍使用 javax.annotation.PostConstruct,只要 classpath 上还有对应 API jar,编译器和 IDE 都可能「看起来正常」,而且 Framework 6.2 运行时确实还认它(6.2.19 源码里 CommonAnnotationBeanPostProcessor 注释写着 Tolerate legacy JSR-250 annotations in javax.annotation package,同一个工程在 Boot 3.5.16 上 javax 回调会执行)。Jakarta 迁移浪潮里,传递依赖把旧 API 带进来并不罕见:某个旧库、某个 BOM 对齐不完整,都足以把它带进来。

问题在于:框架侧已经不再把这些注解当作生命周期回调来识别。

Spring 文档:@PostConstruct / @PreDestroy 说明:CommonAnnotationBeanPostProcessor 认的是 jakarta.annotation.PostConstruct / PreDestroy 。JDK 11 之后 javax.annotation 本就不在 JDK 里;Jakarta EE 9+ 把这组注解放在 jakarta.annotation。需要时可显式依赖 jakarta.annotation-api。

由此得到三个硬结论:

  1. 源码继续写 import javax.annotation.PostConstruct 时,若旧 API 仍在编译 classpath,可以编译通过;
  2. 运行时 Spring 7 不会 按该注解去调用初始化方法(Release Notes 没有逐字这样写,这条来自实测:Boot 4.0.0(Framework 7.0.1)、4.0.8 和 4.1.1(Framework 7.0.9)上,javax 版 @PostConstruct、@PreDestroy 都没执行;同一个工程在 Boot 3.5.16(Framework 6.2.19)上会执行);
  3. Release Notes / 文档描述的是「不再支持 / 需迁移」,没有承诺「发现 javax 注解就 fail-fast 抛异常」。

所以排障时别假设「没报错就等于 init 跑过了」。「javax.PostConstruct 还在 classpath,编译能过,初始化却不跑」与 Framework 7「移除支持」的方向一致;至于运行时会不会有告警,Release Notes 没提;Boot 4.0.8(Framework 7.0.9)的最小工程实测,INFO 和 TRACE 日志里都没有相关告警,别指望靠日志发现。

也别把责任全推给「忘了加处理器」:在带 jakarta.annotation-api 的典型 Boot 应用里,处理器是注册了的(Framework 7.0.9 的 AnnotationConfigUtils 只有在 classpath 上能找到 jakarta.annotation.PostConstruct 时才注册它,spring-boot-starter 会带这个包),只是不再认 javax 那套注解。换包名才是对症。

先分清依据。官方文档明文的有三条:Framework 7.0 Release Notes(移除 javax.annotation / javax.inject 注解支持,须迁到 jakarta 等价注解)、Spring 参考文档(CommonAnnotationBeanPostProcessor 认 jakarta 的 PostConstruct / PreDestroy)、Boot 4 Migration Guide(基于 Jakarta EE 11,必须使用 Spring Framework 7.x)。「javax 版回调不执行」和「日志里没有告警」这两点文档没有明写,来自 Boot 4.0.8 / Framework 7.0.9 的最小工程实测,下文写「实测」的都是这一类。「启动必打某条 warn」「某版本自动重写 import」这类说法,这些来源里都没有,别当成事实。

二、最小对照:只换 import,语义才回来

错误(升级后不应再依赖):

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

import javax.annotation.PostConstruct;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmer {

    private boolean ready;

    @PostConstruct
    public void warm() {
        ready = true;
        // 预热逻辑......
    }

    public boolean isReady() {
        return ready;
    }
}

在 Boot 4 / Framework 7 上,这个 warm() 不会被容器调用 (Boot 4.0.8 / Framework 7.0.9 实测)。ready 一直是 false,直到某个请求打到依赖它的路径才暴露。更糟的是,若预热失败本该让进程拒绝就绪,现在连失败的机会都没有,因为方法没跑。

正确写法:

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

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;

@Component
public class CacheWarmer {

    private boolean ready;

    @PostConstruct
    public void warm() {
        ready = true;
    }

    @PreDestroy
    public void shutdown() {
        ready = false;
    }

    public boolean isReady() {
        return ready;
    }
}

同批要改的不只是 PostConstruct:

旧(不再支持) 新(迁到)
javax.annotation.PostConstruct jakarta.annotation.PostConstruct
javax.annotation.PreDestroy jakarta.annotation.PreDestroy
javax.annotation.Resource jakarta.annotation.Resource
javax.inject.Inject 等 jakarta.inject.Inject 等

Release Notes 把 javax.annotation 与 javax.inject 写在同一条「Removed support」里,漏迁 Inject 会留下另一类装配问题:看起来像「依赖注入忽然不生效」,根因仍是包名。实测里 javax.inject.Inject 和 javax.annotation.Resource 标注的字段都是 null,启动不报错。

若模块是手写依赖、没有 Boot 的依赖管理托底,确认存在 jakarta.annotation-api(spring-boot-starter 会传递带入,版本由 Boot 的 BOM 管,4.0.8 实测是 3.0.0);用 @Inject 的话还要有 jakarta.inject-api(版本同样由 BOM 管,实测 2.0.1,但 spring-boot-starter 不会带它,没加会编译不过),避免注解类解析到奇怪的旧包。多模块工程要在每个 仍编译业务 Bean 的模块里扫一遍,只改 application 模块不够。

三、升级清单:grep → 换包 → 验证副作用

建议按固定四步做,别「启动成功就合并」:

  1. 全仓检索 javax.annotation 与 javax.inject(含 src/test、示例、生成代码目录)。IDE 全局替换前先看 diff,避免误伤字符串常量或第三方源码副本。
  2. 替换为 jakarta.annotation.* / jakarta.inject.* 对应注解;别只改一处业务类。
  3. 核对依赖 :Boot 4 已在 Framework 7 基线上;清理仍把编译导向 javax 的多余依赖,必要时显式加入 jakarta.annotation-api 和(用到 @Inject 时)jakarta.inject-api。用依赖树确认编译时解析到的 PostConstruct 类来自 jakarta 坐标。
  4. 验证 init 真的跑了 :启动日志中的明确标记、健康检查依赖的预热结果、或单测里断言「构造后副作用可见」。别把「能编译 / 能启动」当成生命周期回归通过。

单测示例(用来证明回调被调用,按你项目的测试栈微调):

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

import jakarta.annotation.PostConstruct;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import static org.junit.jupiter.api.Assertions.assertTrue;

class PostConstructSmokeTest {

    static class Probe {
        boolean called;

        @PostConstruct
        void init() {
            called = true;
        }
    }

    @Configuration
    static class Cfg {
        @Bean
        Probe probe() {
            return new Probe();
        }
    }

    @Test
    void postConstructRunsWithJakarta() {
        try (var ctx = new AnnotationConfigApplicationContext(Cfg.class)) {
            assertTrue(ctx.getBean(Probe.class).called);
        }
    }
}

上面这个 Probe 自己用的就是 jakarta 注解,所以它只能证明容器认 jakarta 的 @PostConstruct,抓不到有人把业务 Bean 改回 javax。要抓回归,得让测试加载真正的业务 Bean,例如 try (var ctx = new AnnotationConfigApplicationContext(CacheWarmer.class)) { assertTrue(ctx.getBean(CacheWarmer.class).isReady()); }:Boot 4.0.8 上实测,CacheWarmer 用 jakarta 注解时通过,换成 javax 版会断言失败。前提是测试跑的是真正的 Spring 容器,别只 new CacheWarmer():纯单元 new 不会触发 CommonAnnotationBeanPostProcessor,测不到迁移是否成功。

对「预热型」Bean,还可以在就绪探针或管理端点里暴露 isReady(),让运维与自动化回归能看见状态,不必只靠人眼扫启动日志。

灰度时建议至少抽一条「依赖 PostConstruct 副作用」的路径做人工或自动验证:例如预热后的本地缓存命中、启动后向下游注册的回调是否存在、定时任务是否已挂上。若这些路径在预发环境就失败,别用「先上线再观察」赌生产。静默跳过的特点,就是观察窗口里往往没有显眼的 ERROR。

多仓库/多模块时,把 grep 做成 CI 门禁也值得:匹配 import javax\.annotation\. 或 import javax\.inject\. 直接失败。门禁拦回归,替代不了 Code Review,但能防止「修过一次又被示例代码带回来」。

四、别的升级坑一起排,主线先保住

Boot 4 迁移指南里还有测试相关变化,和这里的主线无关,但同一波升级常一起撞上,只作旁证:

  • @SpringBootTest 不再自动 配置 MockMvc,需要时加 @AutoConfigureMockMvc;
  • @MockBean / @SpyBean 已移除,改用 @MockitoBean / @MockitoSpyBean。

它们不会解释「PostConstruct 不跑」,但能解释「测试怎么也起不来 / mock 注解报错」。主因仍优先查 javax → jakarta 生命周期注解。HTTP 客户端从 RestTemplate 迁走是另一条线,可参考此前整理的迁移笔记:Spring7 RestTemplate 弃用迁移,这里不展开。

实践里建议把「注解包名」和「测试注解更名」拆成两个 checklist 项:前者保生产初始化语义,后者保 CI 能跑。混在一个巨型 PR 里时,失败日志会互相掩盖。

若升级同时还在迁 HTTP 客户端、改 Jackson 包名(Boot 4 迁移指南:首选 Jackson 3,com.fasterxml.jackson 变成 tools.jackson,jackson-annotations 例外)、动测试注解,优先顺序建议是:先保证 Bean 生命周期与注入注解正确(否则应用语义已歪),再修测试装配,最后处理客户端与序列化。生命周期错了,后面的「接口测通」可能只是测到了未初始化的空壳行为。

五、常见误判

「编译过了就是生命周期 OK」:错。javax API 仍可能在编译路径上。

「Spring 一定会 warn / 抛错提醒我」:没有依据。官方表述是移除支持并要求迁移,未承诺 fail-fast。

「只有 PostConstruct 要改」 :漏了。PreDestroy、Resource、javax.inject.* 同批。

「我改成实现 InitializingBean 就不用管包名」:那是另一条生命周期路径,这里不展开。即便你改接口,仓里残留的 javax 注解仍可能坑到别人。升级窗口内仍建议把注解包名扫干净。

「这是 Boot 独有 bug」:根因在 Framework 7 的注解支持范围;Boot 4 只是把你带到这条基线上。

「我们没用 PostConstruct,只写了 @Resource / @Inject,所以无关」 :Release Notes 把整组 javax.annotation 与 javax.inject 一并移除支持。只要包名还在 javax,就在同一条迁移线上。

六、排障口诀与小结

遇到「升级后初始化状态不对」时,先问三句:

  1. 注解的 package 是 javax 还是 jakarta?
  2. 运行时到底有没有进入方法?有没有可观测副作用,还是只有注解挂在源码上?
  3. 依赖树是否仍让编译器解析到 javax.annotation.PostConstruct,造成「假正常」?

小结: Spring Framework 7 移除了对 javax.annotation / javax.inject 的支持;Boot 4 踩在这条基线上。javax.annotation.PostConstruct 可能继续让你编译通过,但初始化回调不会执行(Boot 4.0.8 / Framework 7.0.9 实测,日志里也没有相关告警),文档也没有承诺必有异常提醒。把 PostConstruct / PreDestroy / Resource / Inject 迁到 jakarta.*,用 grep 扫仓,再用副作用或容器级测试证明 init 真的跑过。这比在启动日志里盲搜 warn 更可靠。升级 PR 里给生命周期迁移单独留一条验证项,能少很多半夜排障。

落地顺序:搜 javax → 换 jakarta → 跑容器级断言 → 再谈其他 Boot 4 测试注解变更。 顺序反了,很容易在 MockMvc / MockitoBean 的噪音里漏掉真正的生产语义问题。

相关推荐
Code_Solitude1 小时前
C语言:关于二维数组的作业总结
java·c语言·前端
Wang's Blog1 小时前
Java 项目实战: 外卖平台优化-Nginx六种负载均衡策略对比与选型
java·nginx·负载均衡
Wang's Blog1 小时前
Java 项目实战: 外卖平台优化-YApi接口管理平台与文档导入导出
java·开发语言·yapi
专业程序开发源1 小时前
django新闻推荐系统70655-计算机课程设计、毕业设计
java·javascript·spring boot·后端·python·django·课程设计
vx_Biye_Design2 小时前
springboot一站式旅游管理平台81037-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·课程设计·express·旅游
写后端的胖头鱼2 小时前
时间复杂度 & 空间复杂度
java·数据结构·算法·时间复杂度·空间复杂度
嵌入式学习菌2 小时前
ESP32 ModbusTCP 分片缓存
java·后端·spring
vx_Biye_Design2 小时前
springboot宠物寄养服务预约与监管系统82684-计算机课程设计、毕业设计
java·vue.js·spring boot·elasticsearch·课程设计·express·宠物
程序员小杰@2 小时前
Spring Boot 常用注解分类速记
java·spring boot·后端