Spring Boot 3.4正式发布那天,我第一时间在本地把测试项目升了上去。说实话,这几年Spring Boot的版本节奏一直很稳,但从3.3到3.4的这波更新,确实让不少团队群里炸了锅。结构化日志、虚拟线程的进一步支持、RestClient转正、配置属性校验增强,每一个点都踩在日常开发的痛点上。这篇文章我打算从实际使用的角度,把3.4的新特性拆开讲清楚,顺便把搭建、集成WebSocket、Bean注入控制这类高频操作一起串起来。不管你是刚准备写第一个Spring Boot程序的新手,还是正在维护老项目的老人,都能在里边找到可以直接抄的配置和思路。
先说清楚这版本能带来什么:日志从纯文本变成可检索的JSON,排查线上问题不需要再靠肉眼扫文件;虚拟线程在容器环境里更稳定,高并发IO场景能明显感受到吞吐量变化;RestClient从实验状态走向稳定,以后写HTTP调用不用再纠结用哪个客户端。另外,从3.3升到3.4并不是无脑换版本,有些API被标记废弃,有些自动配置的默认行为变了,直接升级可能会遇到数据源初始化失败、优雅停机不生效、配置绑定报错这些坑。
下面我会按照“亮点拆解→配置实战→升级踩坑→问题排查”的顺序来写,全文都是我在测试环境里实际跑过、验证过的内容,涉及的代码块和yml配置可以直接复制去改。
1. Spring Boot 3.4:这次更新到底“王炸”在哪里?
1.1 版本背景:从3.3到3.4,社区关注的重点
Spring Boot 3.4距离3.3发布大概隔了四个月,但它不是单纯修bug的小版本,而是实实在在的“功能蓄水”。从里程碑版本开始,社区讨论最集中的就是三件事:结构化日志默认支持、虚拟线程全兼容、RestClient正式转正。这三件事分别对应了三个不同层面的需求——可观测性、高并发、代码清爽度。
以前我们排查生产问题,最痛苦的是日志格式五花八门。团队里有人用logback的pattern输出时间级别线程,有人用log4j2的JSON格式,日志平台解析时还得写一堆正则。3.4把结构化日志作为一等公民,通过logging.structured.format.file和logging.structured.format.console就能切换到纯JSON输出。这个能力在微服务链路追踪场景下尤其有用,因为traceId、spanId可以直接映射为JSON字段,而不是嵌在一条字符串里。
另外,Spring Boot 3.4对虚拟线程的支持从“能用”变成了“好用”。之前用spring.threads.virtual.enabled=true开启虚拟线程,在高并发下偶尔会遇到JVM内部锁竞争,3.4调整了Tomcat和Jetty对虚拟线程的适配逻辑,同时补充了虚拟线程状态在Actuator中的展示。这一点对做WebFlux或大量外部IO调用的服务来说,收益是实打实的。
1.2 核心特性全景:一张表看清楚有什么变化
我整理了一张特性清单,方便大家对照自己项目的现状:
| 特性 | 3.3及之前 | 3.4变化 | 影响范围 |
|---|---|---|---|
| 结构化日志 | 需要手动配置logback的JSON encoder | 原生logging.structured.format.*支持 | file和console输出 |
| 虚拟线程 | 支持但偶尔有阻塞点 | 增强锁竞争处理,可观测性更好 | Tomcat、Jetty、Reactor |
| RestClient | 处于实验阶段 | 正式稳定,文档和自动配置完善 | 替代RestTemplate |
| 配置属性校验 | @ConfigurationProperties基本可用 | 支持在配置元数据里声明校验规则 | yml/properties绑定 |
| 优雅停机 | 需要手动设置server.shutdown=graceful | 默认调整了graceful period上限 | 长任务处理 |
| 依赖版本 | Spring Framework 6.1.x | Spring Framework 6.2.x | 全部模块 |
这张表里有几个点需要特别解释。server.shutdown=graceful其实在2.3就引入了,但3.4把spring.lifecycle.timeout-per-shutdown-phase的默认行为调整得更加保守,避免有些任务在停机时被强制中断。还有@ConfigurationProperties校验,以前如果字段校验失败,启动时只报一个模糊的错误,3.4会把具体是哪个字段、哪个校验注解、当前值是多少全部列出来。
这些变化看起来零散,但它们都指向同一个方向:减少开发者在基础框架上的心智负担,把更多精力留给业务。
2. 新特性拆解:哪些真正值得马上用?
2.1 结构化日志:让排查问题从“肉眼”升级到“检索”
先说结构化日志,因为它最直观。以前我们logback配置通常是这样的:
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>这种格式人眼看着舒服,可一旦日志量每天几百GB,想从里面筛出一个特定请求的完整链路,就非常痛苦。3.4提供了直接的结构化输出,在application.yml里加两行:
logging: structured: format: console: ecs file: ecs这里ecs是Elastic Common Schema格式,输出是纯JSON,包含@timestamp、log.level、message、service.name等字段。如果你用的是ELK或者Loki,解析成本瞬间下降。除了ecs,还支持logstash格式,适合已经使用Logstash采集的团队。
实际使用中要注意一个细节:logging.structured.format.console=ecs会覆盖原本的pattern,也就是说你自定义的%msg%n不再生效。如果你有少量字段必须放到JSON里,建议用MDC写入,比如:
MDC.put("userId", "1024"); log.info("用户登录成功"); MDC.remove("userId");在ECS格式下,MDC里的键值对会自动展平为JSON顶层字段。踩过坑的朋友应该知道,以前要在logback里把MDC输出到pattern,需要写%X{userId},现在完全不用了。
2.2 虚拟线程与可观测性:高并发场景下的配置要点
虚拟线程是Java 21的正式功能,Spring Boot 3.4对它的适配已经非常成熟。如果你的服务是IO密集型,比如大量请求第三方API、查询数据库,开启虚拟线程往往能明显提升吞吐。
开启方式很简单:
spring: threads: virtual: enabled: true但光开启还不够。Tomcat在处理虚拟线程时,默认连接器的accept-count和max-connections行为与平台线程不同。我实测下来,在开启虚拟线程后,Connector的线程池相关参数(如server.tomcat.threads.max)会被忽略,因为虚拟线程不再受固定池大小限制。这时候更值得关注的是server.tomcat.accept-count,它控制接受队列长度,设太小容易在突发流量时直接拒绝连接。
还有一点,虚拟线程不适合CPU密集型任务。如果你在服务里用虚拟线程跑复杂计算,反而会因频繁切换导致性能下降。判断依据很简单:看线程是被阻塞还是被运行,阻塞越多,虚拟线程收益越大。
在可观测性方面,3.4加强了虚拟线程在Actuator里的展示。访问/actuator/metrics可以看到executor.active.threads等指标,而线程转储也能正确识别虚拟线程的运行状态。排查问题时,不再需要困惑“明明线程数很高,为什么CPU利用率很低”。
2.3 RestClient与RestTemplate:HTTP客户端选择的时代交替
RestTemplate在Spring生态里存在了十几年,但官方文档一直建议逐步替换为RestClient。3.4里RestClient不再是实验特性,这意味着它的API、自动配置和错误处理都稳定了。最明显的变化是,不需要手动创建Builder,直接注入RestClient即可:
@Service public class OrderSyncService { private final RestClient restClient; public OrderSyncService(RestClient.Builder builder) { this.restClient = builder .baseUrl("https://api.example.com") .defaultHeader("X-Requested-By", "order-service") .build(); } public OrderDto fetchOrder(String id) { return restClient.get() .uri("/orders/{id}", id) .retrieve() .body(OrderDto.class); } }和RestTemplate相比,RestClient的接口更流畅,且默认支持响应式风格的方法。在微服务相互调用时,可以少写很多无关代码。
升级时要注意:如果旧项目大量使用RestTemplate,并且自定义了RestTemplateCustomizer,升级到3.4后这部分逻辑不会自动迁移到RestClient。建议新代码优先使用RestClient,旧代码暂时保留RestTemplate,再通过SDK或者网关层做过渡。
2.4 配置属性的刷新与校验:让yml更安全
很多项目配置多到爆炸,yml里一个拼写错误,启动时看不出来,运行时才出现诡异行为。3.4加强了对@ConfigurationProperties的校验支持,可以直接利用JSR-303规范:
@ConfigurationProperties(prefix = "payment") @Validated public class PaymentProperties { @NotBlank private String apiKey; @Min(1) private int connectTimeoutSeconds = 5; @Pattern(regexp = "\\d{10}") private String merchantId; }启动时如果payment.api-key没配置,会直接报错,而不是等请求时才发现空指针。这个特性在配置中心动态刷新场景下也有意义:当配置刷新时,校验规则会重新执行,不符合新值的属性不会生效。
3. 从零到一:Spring Boot 3.4 项目搭建与核心配置实战
3.1 第一个Spring Boot程序:脚手架初始化步骤
老生常谈,但很多新手在第一步就卡住。我建议直接去Spring Initializr生成项目,选Spring Boot 3.4.x,Java版本选21,依赖按需勾选。如果本地命令行方便,也可以这样:
curl https://start.spring.io/starter.tgz \ -d type=maven-project \ -d language=java \ -d bootVersion=3.4.0 \ -d javaVersion=21 \ -d dependencies=web,actuator \ -d packageName=com.example.demo \ -d name=demo \ | tar -xzvf -生成后的项目结构非常简单。入口类长这样:
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }第一个程序不建议一开始就引入MyBatis、Redis这些重依赖。先把spring-boot-starter-web跑起来,写一个Controller,确认端口和日志正常,再逐步加东西。我看到太多新手一上来把依赖堆满,启动报错后根本分不清是哪里的问题。
3.2 Spring Boot集成WebSocket的yml配置:常见坑与推荐写法
WebSocket在Spring Boot里用起来不难,但很多人在yml层面就会踩坑。首先是端点和路径配置:
server: port: 8080 spring: websocket: endpoint: /ws/chat allowed-origins: "http://localhost:3000"注意,spring.websocket在3.4中对应的配置类在不同模块里略有差异。如果用Spring MVC的WebSocket支持,端点配置通常在@Configuration类中,而不是在yml里。yml中主要配置的是消息代理。推荐这样写:
spring: websocket: message-broker: enabled: true application-destination-prefixes: /app simple-broker: destinations: /topic,/queue前面那段配置如果放在全局application.yml里,需要确保依赖包含spring-boot-starter-websocket。否则配置会被Spring Boot忽略,且不会报错,这是最让人头疼的“配置失效”。
接下来写一个最简WebSocket配置类:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic", "/queue"); registry.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/ws/chat") .setAllowedOriginPatterns("*") .withSockJS(); } }setAllowedOriginPatterns("*")在生产环境要精确收敛,不要直接全放行。用SockJS兼容降级时,记得前端也要引入sockjs-client库。
另一个常见坑是:在Spring Boot 3.4中,如果你同时引入了spring-boot-starter-security,WebSocket的握手请求会被Security拦截。需要在SecurityConfig里放行/ws/**,同时注意帧类型,比如STOMP CONNECT帧可能携带Authorization头,要自定义ChannelInterceptor去解析。
3.3 Bean注入控制:三种注入方式的边界
Spring的依赖注入看着简单,实际项目里因为注入方式混乱导致的循环依赖、测试困难问题特别多。这里把三种方式讲清楚。
第一种是字段注入,即直接在成员变量上加@Autowired:
@Autowired private UserService userService;优点是代码少,缺点是没法做Immutable对象,也没法直接单元测试(除非用反射)。3.4仍然支持字段注入,但不推荐。
第二种是Setter注入。适合可选依赖或运行时可以替换的依赖:
private UserService userService; @Autowired public void setUserService(UserService userService) { this.userService = userService; }第三种是构造器注入。这是Spring官方推荐的方式,也是Spring Boot 3.4默认自动配置最友好的方式:
@Service public class OrderController { private final PaymentService paymentService; private final InventoryService inventoryService; public OrderController(PaymentService paymentService, InventoryService inventoryService) { this.paymentService = paymentService; this.inventoryService = inventoryService; } }为什么推荐构造器注入?因为它能强制依赖完整性,避免对象创建一半、依赖为空的情况。同时Spring容器会优先选择构造器完成注入,配合Lombok的@RequiredArgsConstructor可以少写一大段代码。
有人问,那循环依赖怎么办?在Spring Boot 3.4中,默认禁止循环依赖,一旦检测到,启动直接抛异常。这在以前是警告,现在变成了错误。如果你的项目确实存在A依赖B、B依赖A的情况,解决思路不是回到Setter注入,而是拆分职责,把公共依赖提取到C服务中。
4. 老项目升级到3.4:操作步骤与踩坑实录
4.1 升级前的环境检查与依赖调整
升级不是把pom.xml里的版本号改了就行。我建议先做一次环境检查:
- JDK必须是17或21,如果还在用Java 8,劝你放弃直接上3.4,先升到2.7再考虑跨版本。
- Spring Boot版本从2.x升级到3.x,
javax.*包要全部换成jakarta.*。这一步只能靠全局替换,但要注意有些第三方库内部还在用javax,需要同步升级。 - 检查自定义的
@MapperScan、@EnableXxx这类注解,它们内部可能耦合了老版本Spring的类。
推荐用官方提供的spring-boot-migrator工具做辅助分析。它可以扫描项目并识别不兼容的API,但不保证100%覆盖,最终还是要靠编译和启动测试来验证。
4.2 高版本JDK带来的编译与运行差异
很多老项目升级到3.4时,JDK也从11升到17或21。这个过程中除了代码,还要注意构建工具的版本。Maven要3.6.3+,Gradle要7.5+。我遇到过Maven编译器插件默认用JDK 11的--release参数,导致JDK 21环境下编译直接报错。解决办法是显式指定:
<properties> <java.version>21</java.version> <maven.compiler.release>21</maven.compiler.release> </properties>运行阶段,JDK 21默认启用了--enable-preview吗?并没有。如果你用了虚拟线程,需要JDK 21正式版,而不需要preview标志。真正要注意的是某些依赖通过反射访问JDK内部类,比如CGLIB库,会导致InaccessibleObjectException。升级后启动时如果出现这类报错,一般可以通过添加模块开放参数来解决,但更推荐直接升级CGLIB或Spring的代理方式。
4.3 实测升级中遇到的典型问题
我把自己在升级测试项目中遇到的三个真实问题列出来,每个都是能复现的。
第一个问题是数据源初始化顺序变化。老的Spring Boot 3.3项目里,自定义的DataSourceInitializer会在JPA实体管理器创建之前执行。升级到3.4后,如果配置了spring.jpa.hibernate.ddl-auto=validate且数据库结构不匹配,启动时Hibernate会先报错,而不是先执行初始化脚本。解决办法是把初始化脚本放到schema.sql并由Spring SQL init接管:
spring: sql: init: mode: always schema-locations: classpath:schema.sql第二个问题是@ConfigurationProperties绑定日志不再打印“Ignored properties”。以前配置里写了不存在的属性,启动日志会提示哪一项被忽略,方便发现拼写错误。3.4默认把这种提示降级到debug级别,导致很多人觉得配置不生效。如果你依赖这个提示来发现错误,可以设置:
debug: true或者用--debug启动参数。
第三个问题是优雅停机在长任务场景下超时。3.4中spring.lifecycle.timeout-per-shutdown-phase默认值虽然还是30s,但如果你在停机前有超过30s的任务,K8s里会先被Pod删除,导致任务中断。我调整了超时时间:
spring: lifecycle: timeout-per-shutdown-phase: 90s server: shutdown: graceful同时配合K8s的preStop钩子,先把Pod从Service端点摘掉,等待几秒再真正停机,这样能最大程度避免请求在升级时被断开。
5. 常见问题与排查技巧速查表
5.1 启动失败类问题
| 错误现象 | 常见原因 | 解决思路 |
|---|---|---|
Failed to determine a suitable driver class | 数据源配置不完整,或缺驱动 | 检查yml里spring.datasource.url和driver配置,或排除数据源自动配置 |
Error creating bean with name 'xxx' | Bean依赖构造器参数不匹配 | 查对应类的构造器注解,确认@Qualifier是否使用 |
The dependencies of some beans... | 循环依赖 | 调整Bean职责,拆出公共依赖 |
InaccessibleObjectException | 反射访问JDK内部 | 更新代理库或调整--add-opens参数 |
其中Failed to determine a suitable driver class在升级到3.4后更容易出现,因为Spring Boot 3.4对数据源的自动配置做了更严格的类型推断。如果你只在配置里写了spring.datasource.url,但忘了加对应的数据库驱动坐标,启动会直接失败。如果用了嵌入式数据库如H2,也需要显式引入runtime依赖。
5.2 配置不生效类问题
配置不生效是最高频的“伪故障”。遇到过好几次,同事说“我改了yml,服务没变化”,最后发现是YAML缩进有问题。3.4对YAML的解析更严格,一个Tab键就能让整个配置文件读取失败。建议统一用两个空格缩进,并设置IDE的“Detect file indentation”为off。
另外,@ConfigurationProperties和@Value并存时容易出现覆盖混乱。我的经验是:核心配置统一用@ConfigurationProperties,临时变量用@Value,不要把同一个属性在两类注解里都用一遍。3.4引入配置属性校验后,如果某个字段校验失败,启动日志会明确告诉你“Property payment.connect-timeout-seconds is outside the valid range [1, 30]”,信息比之前友好得多。
5.3 测试与打包阶段问题
有一个很容易踩的坑是:升级到3.4之后,spring-boot-maven-plugin默认的repackage goal会在package阶段重新打包,如果你的工程模块结构复杂,且子模块也引入了Boot插件,会导致主jar包里的BOOT-INF/classes出现重复内容。解决办法是只在最外层应用模块配置插件,其余模块仅依赖父POM。
测试环境里另一个典型问题是@SpringBootTest与@ActiveProfiles组合导致上下文缓存失效。每次切换profile都会重新启动容器,测试时间会翻倍。如果只是少量配置差异,建议把不同profile的公共配置放到application.yml,差异配置再放子文件。
还有一个经常被忽略的点:spring-boot-starter-test在3.4里集成了Mockito 5和AssertJ 3.25,如果你在业务代码里用过Mockito老版本的stub写法,可能会遇到编译错误。比如:
when(userService.getUserById(1)).thenReturn(user);这种写法没有问题,但doAnswer配合when在Mockito 5中对泛型推导更严格,可能需要显式类型声明。
版本升级之外的一点个人体会
把Spring Boot 3.4真正引入生产之前,我一直提醒团队:新特性虽香,但升级前必须做一次完整的回归测试,尤其是配置绑定、数据源初始化、HTTP客户端调用这三条主链路。我在测试环境里压过一个并发2000的WebSocket推送场景,虚拟线程开启后,线程数从200降到几十,但GC停顿反而增加了,原因是为每个连接创建的NIO缓冲区变多。后来通过调整JVM堆和直接内存大小解决了问题。这说明版本升级从来不只是换依赖,还包括对运行时参数的重新理解。
最后分享一个小技巧:升级后第一次启动,记得加--debug和--trace参数,把日志级别提到trace,它能暴露很多自定义组件在自动配置阶段被悄悄替换的问题。排查完再恢复正常日志级别。把这些经验沉淀到团队文档里,下次再升级就会轻松很多。