ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot3断代升级实战:从零搭建到部署的完整指南

SpringBoot3断代升级实战:从零搭建到部署的完整指南

前两周帮某电商团队做技术迁移评审,对方项目经理一上来就问:能不能在 SpringBoot2.7 基础上直接改版本号升到 3?

我当时就告诉他:这可不是改一行版本号的事。编译直接红一片,javax消失,Tomcat行为变了,Spring Security的配置链全部重写——SpringBoot3 不是一个增量版本,而是一次基于 JDK17+Jakarta EE 9+ 的断代升级。

这篇博文就围绕 SpringBoot3 的快速搭建与工程详解展开,我以自己最近从零起的一个用户服务项目为例,把初始化、目录结构、配置、数据访问、打包部署的完整链路拆开讲清楚。你会遇到哪些坑、为什么会有这些坑、怎么用最少的时间趟过去——这才是本文想给你的东西。

1. 为什么我说 SpringBoot3 是一次断代升级

除非你完全没写过 Java,否则第一次打开 SpringBoot3 项目时,最先撞见的一定是编译错误。import javax.persistence没了,javax.validation也找不到。这背后是整套 Java EE 规范改名成了 Jakarta EE,所有原来挂在javax.*命名空间下的标准 API 全部切到了jakarta.*。SpringBoot3 基于 Spring Framework 6 构建,而后者只认 Jakarta EE 9+,这就是所谓的“断代”。

除了命名空间,几个关键变化会直接影响你写代码的方式:

  • 强制 JDK17 起步。JDK8 的时代彻底翻篇,SpringBoot3 的所有官方文档和测试环境都按 JDK17 走。
  • javax.servlet变成jakarta.servlet。这意味着你的Filter、Servlet、WebListener等相关类全部要改 import。
  • Spring Security的配置写法发生重大变化。WebSecurityConfigurerAdapter被移除,全面转向SecurityFilterChainBean 注册的方式。
  • 可观测性和 GraalVM 原生镜像的支持大幅增强。SpringBoot3 把原生编译放到了第一梯队,虽然实战中大多数人还是用 JVM 方式跑。

版本对应关系上,我整理了个简表方便你对照排查:

组件版本要求说明
JDK17 及以上21 也完全兼容
Spring Boot3.0 ~ 3.43.2 是我当前主力版本
Spring Framework6.0+内嵌于 Boot 依赖
Jakarta EE9 以上所有标准 API 换名
Maven3.6.3+推荐 3.9.x
MyBatis Starter3.0.x 起对应 Boot32.x 版本直接冲突

一开始会觉得这些限制很烦,但换个角度看,JDK17 本身的性能表现和稳定性确实比 8 强不少。尤其是 ZGC 这种超低暂停时间的垃圾回收器,在生产环境的响应稳定性上能明显感觉出来。所以我的建议是:新建项目直接上 Boot3,老项目如果有精力,值得做一次系统性的迁移升级,而不是继续在 2.x 上原地踏步。

2. 初始化项目的正确姿势:从版本下拉框开始避坑

很多人喜欢自己手写 pom.xml 去引依赖,我强烈建议不要这么干,至少 SpringBoot 初期不要这么干。直接用 Spring Initializr 生成骨架,后续所有需要调整的地方都可以基于官方结构来改。原因很简单:SpringBoot 的依赖版本号大量依赖父级 BOM 管理,自己徒手拼依赖组合,版本搞错一个就可能连环炸。

初始化前需要先想清楚几个事,它们决定了这个项目后续能不能舒服地活着:

  • 构建工具选 Maven 还是 Gradle。我的建议是 Maven,教程最多、公司内网镜像最全、排查编译问题最方便。Gradle 虽然构建更快,但团队里如果有人不熟,协作成本会立刻上来。
  • 打包方式选 jar 而不是 war。SpringBoot 内嵌 Tomcat,用 jar 跑是最省心的,不管你是部署到云服务器还是容器里,一个 java -jar 全搞定。
  • Java 版本选 17。选 21 问题也不大,但绝大多数云厂商的 CI 镜像对 JDK17 的缓存更完善,构建速度有优势。
  • 依赖勾选不要贪多。基础包只需要 Spring Web、Validation、Lombok、MySQL Driver、Redis 这几个,剩下的按需添加。依赖越多,启动时的自动配置越多,出错时排查面越大。

我这次的项目依赖清单如下:

依赖作用
Spring Web提供 MVC 能力和内嵌 Tomcat
Validation参数校验,源于 Jakarta Bean Validation
Lombok简化实体类样板代码
MySQL DriverJDBC 连接 MySQL
Spring Data RedisRedis 客户端与操作模板
Configuration Processor自定义配置项快速提示并校验

勾选完成后直接 Generate 下载 zip,解压后用 IDE 打开。注意刚打开时 Maven 会疯狂下载依赖,这些依赖来自 Maven Central,在国内网络环境下非常慢。我的实操经验是:第一时间在.m2/settings.xml里把阿里云镜像配好,或者直接用云厂商的公共镜像服务,十分钟能解决的问题不要耗一个小时。

配置好镜像之后刷新整个 Maven 项目,确定spring-boot-starter-parent把版本号锁在你选择的 Boot3 版本上,接下来就可以开始改项目结构了。

3. 工程目录结构拆解:启动类为什么要放在最外层

Initializr 生成的项目结构长这样:

user-service/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/com/example/userservice/ │ │ │ ├── UserServiceApplication.java │ │ │ ├── controller/ │ │ │ ├── service/ │ │ │ ├── mapper/ │ │ │ └── entity/ │ │ └── resources/ │ │ ├── application.yml │ │ ├── application-dev.yml │ │ └── application-prod.yml │ └── test/java/com/example/userservice/

很多人不解为什么UserServiceApplication.java一定要放在com.example.userservice这一层,也就是所有子包的外面。这不只是约定习惯,而是 Spring 的组件扫描机制决定的。@SpringBootApplication注解自带了@ComponentScan,默认扫描它所在包及所有子包。如果启动类放到controller包里,那它就只能扫到controller下的组件,你写在service里的所有 Bean 一个都认不出来,启动时直接出现诡异的自动装配失败。

再说回主类本身。@SpringBootApplication是一个组合注解,等价于以下三个注解的叠加:

  • @SpringBootConfiguration:标记当前类为配置类,允许在类里声明额外的 Bean。
  • @EnableAutoConfiguration:开启自动配置。SpringBoot 会根据你 classpath 下存在的依赖和配置项,自动装配相关的 Starter。
  • @ComponentScan:扫描包下的@Component、@Service、@Repository、@Controller等托管 Bean。

理解这一点,你就能明白为什么自动配置这么“魔法”。比如你引入了spring-boot-starter-web,@EnableAutoConfiguration就会发现 classpath 上有 Spring MVC,自动帮你装配DispatcherServlet、Tomcat等全套 Web 组件。如果你不需要某些自动装配,可以通过exclude属性或者配置文件里的spring.autoconfigure.exclude来关闭,这个技巧在做二次开发或定制启动逻辑时经常用得上。

我还建议在resources目录之外,把测试目录也用起来。SpringBoot3 的测试底座已经切到 JUnit5 + AssertJ,spring-boot-starter-test里集成了大量好用的测试工具。工程一开始就把测试基础设施铺好,后面写集成测试会非常顺手。

4. 写第一个接口:从一个商品模块看 Controller 到数据库全链路

工程骨架搭好之后,别急着写一堆花里胡哨的配置,先用一个最小的商品模块把整条链路跑通。这个模块的逻辑很简单:客户端请求商品列表和详情,服务端从数据库读取数据并返回 JSON。麻雀虽小,但 Controller → Service → Mapper → MySQL 的完整链路走一遍,项目就算真正立起来了。

商品实体类:

package com.example.userservice.entity; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data public class Product { private Long id; private String name; private BigDecimal price; private Integer stock; private LocalDateTime createdAt; }

Lombok 的@Data帮我们把getter/setter/toString/equals/hashCode全部生成,实体类保持整洁,这个习惯在 Boot3 时代同样是推荐的。

接着写 Mapper。Boot3 项目里我用的还是 MyBatis,注意必须引入mybatis-spring-boot-starter的 3.0.x 版本版本,2.x 系列是 Boot2 的专属,混用会直接拉起冲突。

package com.example.userservice.mapper; import com.example.userservice.entity.Product; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; @Mapper public interface ProductMapper { @Select("SELECT * FROM product WHERE status = 1 ORDER BY id DESC") List<Product> selectAll(); @Select("SELECT * FROM product WHERE id = #{id} AND status = 1") Product selectById(@Param("id") Long id); }

这里用注解 SQL 是为了快速跑通链路,实际业务查询复杂时还是要用 XML 文件方式维护,尤其涉及多表联查和动态 SQL 的场景,XML 的可维护性远高于注解。

Service 层主要处理业务逻辑和异常:

package com.example.userservice.service; import com.example.userservice.entity.Product; import com.example.userservice.mapper.ProductMapper; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.List; @Service @RequiredArgsConstructor public class ProductService { private final ProductMapper productMapper; public List<Product> listProducts() { return productMapper.selectAll(); } public Product getProductById(Long id) { Product product = productMapper.selectById(id); if (product == null) { throw new RuntimeException("商品不存在"); } return product; } }

@RequiredArgsConstructor配合final字段完成构造器注入,这也是 Spring 官方推荐的注入方式,比@Autowired字段注入更利于测试和不可变设计。

Controller 层负责接收请求、参数校验、返回统一响应结构:

package com.example.userservice.controller; import com.example.userservice.entity.Product; import com.example.userservice.service.ProductService; import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/api/products") @RequiredArgsConstructor public class ProductController { private final ProductService productService; @GetMapping public List<Product> list() { return productService.listProducts(); } @GetMapping("/{id}") public Product detail(@PathVariable @Min(value = 1, message = "商品ID必须大于0") Long id) { return productService.getProductById(id); } }

在 Boot3 中,参数校验注解从javax.validation.constraints.Min换成了jakarta.validation.constraints.Min,如果从老项目拷贝代码,这里是一个必踩的坑。

此外还要配置@RestControllerAdvice来做全局异常处理。真实项目中,我不想让用户直接看到RuntimeException堆栈,而是统一返回{ "code": 500, "message": "商品不存在" }这类格式。全局异常处理除了封装响应格式,还能顺手把服务端错误日志打出来,方便排查。

跑通这个模块之后,不要急着加业务。先拿 Postman 或 IDEA 自带的 HTTP Client 敲一下GET /api/products,确认能返回 JSON 数据,再开始叠功能。链路越短,定位问题越容易。

5. 配置文件拆分的正确思路与 environment 切换技巧

SpringBoot 的配置体系,是每个项目从“能跑”走向“能上线”必须迈过的一道坎。早期我也见过很多人上来就只写一个application.yml,把数据源、Redis、日志、业务开关全部塞在里面,到最后自己都分不清哪个配置是哪个环境用的。

正确的思路,是把配置拆成“公共配置 + 环境配置”两个层面。公共配置写在application.yml里,各环境不同的值放到application-dev.yml、application-prod.yml中。激活环境时,只需要在启动命令里追加--spring.profiles.active=dev或--spring.profiles.active=prod。

这是一个典型的拆分示例:

# application.yml server: port: 8080 spring: application: name: user-service profiles: active: dev data: redis: host: localhost port: 6379 database: 0 mybatis: configuration: map-underscore-to-camel-case: true mapper-locations: classpath:mapper/*.xml
# application-dev.yml spring: datasource: url: jdbc:mysql://localhost:3306/user_service_dev?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver
# application-prod.yml spring: datasource: url: jdbc:mysql://生产地址:3306/user_service_prod?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: prod_user password: ${DB_PASSWORD}

有几个细节必须注意。

数据库密码这类敏感信息,不要明文写死。用环境变量引用,比如${DB_PASSWORD},部署时通过 CI/CD 注入。同样的做法也适用于 Redis 密码、第三方密钥。这属于成本低廉但收益极高的安全习惯。

spring.datasource.driver-class-name在 Boot3 中其实可以省略,因为 Boot 会根据 JDBC URL 自动推断驱动类。但有些老项目的代码习惯还在,写了也没问题,只是没太大必要。

还有一个特别容易被人忽略的点:server.forward-headers-strategy。如果你在项目前面挂了 Nginx 或网关做反向代理,应用自身就无法感知客户端真实的 IP 和协议。配置framework策略,由容器提供转发头支持,是生产环境的常见需求。很多兄弟反馈 Nginx 配好了但拿不到真实 IP,多半就是漏了这个配置。

应用到配置项的方式,我建议优先使用@ConfigurationProperties而不是@Value。前者支持类型安全的属性绑定、自动提示、以及配置项的校验;@Value更适合读取那些一次性使用的零散配置。比如自定义业务配置:

package com.example.userservice.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Data @Component @ConfigurationProperties(prefix = "app.order") public class OrderProperties { private Integer timeoutSeconds = 30; private Boolean autoCancel = true; }

这样在application.yml里以app.order.timeout-seconds的方式写配置,就能自动映射到字段。注意 Spring 的宽松绑定规则,timeout-seconds和timeoutSeconds是等价的,但用@ConfigurationProperties写完一定要引入Configuration Processor依赖,否则 IDE 里没有自动提示,很容易写错字段名。

6. 集成 MySQL 与 Redis:版本与连接方式的双重考验

现实中的接口不可能都从静态内存里取数据,集成数据库与缓存是这个项目真正迈向实用的关键一步。

先说 MySQL 集成。Boot3 项目中引入 MySQL 驱动和 MyBatis Starter 之后,需要在pom.xml确认版本号的兼容性。这里有个经验判断:SpringBoot3 对应的 MyBatis Starter 是org.mybatis.spring.boot:mybatis-spring-boot-starter的版本 3.0.x 起,如果你看到依赖的 version 是 2.x,那就说明它面向的是 SpringBoot2,继续用它编译没问题,但运行时会因为javax命名空间缺失或者类冲突疯狂报错。

数据库连接池方面,Boot3 默认使用 HikariCP。这块不用额外配置,它已经是目前综合性能最好的连接池之一。如果你需要调整最大连接数、最小空闲数、连接超时时间,可以在spring.datasource.hikari下配置。线上流量出现高峰期连接不够时,优先查这个参数,而不是直接加数据库实例。

Reids 配置上,Boot3 引入spring-boot-starter-data-redis后,默认连接池是 Lettuce。单机 Redis 直接用 Spring 自动配置的RedisTemplate即可。下面是我项目里对RedisTemplate的序列化增强配置:

package com.example.userservice.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(new GenericJackson2JsonRedisSerializer()); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer()); return template; } }

为什么非要单独配置这个?因为 SpringBoot 默认的RedisTemplate用的是 JDK 序列化,往 Redis 里存进去的数据是一串带类型信息的二进制乱码。你用命令行客户端看数据时根本读不出来,而且跨语言读取也困难。换成 JSON 序列化之后,可读性和通用性立刻提升。

建议别用默认序列化进生产。我当时第一次用 RedisTemplate 存订单信息后,运维那边用工具看 Redis 里全是\xAC\xED开头的二进制前缀,闹了不少笑话。类之间互相传值时,还要担心默认序列化带来的类型转换异常。

集成测试这个模块时,我习惯写一个简单的健康检查接口,把数据库和 Redis 的连接状态打出来。这样每次启动应用后先调一次健康检查,再去调业务接口,一旦出现网络抖动或密码错误,能立刻定位到是哪个中间件的问题,而不是在业务逻辑里绕来绕去。

7. 写单元测试与集成测试:别让验收人心惊胆战

服务端项目越到后期,回归成本越高。有一次我改了一个别人写的复杂订单状态机,改动只有三行代码,结果一晚上接了六个线上工单。后来我把核心逻辑的测试补齐,情况立刻好转。SpringBoot3 的测试生态对开发者友好,关键是正确使用MockMvc和@SpringBootTest。

单元测试应当聚焦 Service 层,用 Mockito 把 Mapper 层替换掉,重点验证业务状态流转与异常分支。集成测试则用@SpringBootTest拉起完整的 Spring 容器,配合MockMvc来验证 Controller 层对外暴露的 HTTP 接口。

MockMvc 在 Boot3 中的经典用法如下:

package com.example.userservice.controller; import com.example.userservice.entity.Product; import com.example.userservice.service.ProductService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import static org.mockito.BDDMockito.given; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @WebMvcTest(ProductController.class) public class ProductControllerTest { @Autowired private MockMvc mockMvc; @MockBean private ProductService productService; @Autowired private ObjectMapper objectMapper; @Test public void testGetProductDetail() throws Exception { Product product = new Product(); product.setId(1L); product.setName("测试商品"); product.setPrice(new java.math.BigDecimal("99.9")); given(productService.getProductById(1L)).willReturn(product); mockMvc.perform(get("/api/products/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.name").value("测试商品")); } }

@WebMvcTest只加载 Web 层的配置,不会真正触发数据库和 Redis 连接,速度快且稳定。用@MockBean切掉 Service 依赖,直接在隔离场景下验证 Controller 的映射与响应结构。

老项目从 Boot2 升到 Boot3 后,测试代码最大的坑就是JUnit4的@RunWith等注解不可用了,现在必须是JUnit5的写法。IDEA 在升级时一般会提示包名变化,注意把org.junit.Test换成org.junit.jupiter.api.Test,同时处理@Before到@BeforeEach的映射。

写完这些测试之后,服务端启动命令会变成:

mvn test

如果当前环境变量缺失导致测试失败,还可以用 Maven profile 隔离不同场景的配置。这个细节在多人协作的仓库里特别重要——你要保证新同事 checkout 代码之后,执行一条命令就能跑通所有测试,而不是先折腾十分钟配置。

8. 打包与部署:从 fat jar 到优雅启动的全过程

本地开发跑通以后,紧接着要回答的问题是:这玩意儿怎么上服务器跑起来?

SpringBoot3 提供的默认打包方式是 fat jar,也就是把所有依赖和三方类全部打进一个可执行 jar 文件里。执行mvn clean package之后,target目录下会生成两个 jar,一个是以.jar.original结尾的纯业务包,另一个是可直接运行的 fat jar。生产部署用的是后者。

部署时最基础的启动命令是:

java -jar user-service.jar --spring.profiles.active=prod

如果服务器内存有限,建议显式指定堆内存参数:

java -Xms256m -Xmx512m -jar user-service.jar --spring.profiles.active=prod

别小看这个参数。SpringBoot 默认堆上限是物理内存的四分之一,如果在只有 1G 内存的小机器上忘了限制,很容易把机器内存打满,连带其他进程一起遭殃。

除了启动参数,我再强烈建议配上优雅停机配置:

spring: lifecycle: timeout-per-shutdown-phase: 20s server: shutdown: graceful

这个配置生效后,应用接收到终止信号时不会立刻杀死线程,而是先停止接收新请求,给已经进入处理流程的请求预留最多 20 秒时间完成收尾。对于正在处理支付回调、状态流转等关键业务的接口,少了这一步,停机重启时非常容易出现数据不一致。

部署之后要立即验证服务健康状态。如果你引入了spring-boot-starter-actuator,可以直接访问/actuator/health来确认应用状态,SpringBoot3 默认暴露的信息只有status和磁盘状态,足够做最基础的探活。我通常还会把 Redis、数据库连接池的健康指示器也配进去,这样监控系统能直接感知到中间件可用性。

日志方面同样不能忽略。生产环境至少要把日志滚动策略配好,我用的是 Logback,配置里指定了按天滚动并压缩历史文件:

logging: level: root: info com.example.userservice: debug file: name: logs/user-service.log logback: rollingpolicy: max-file-size: 100MB max-history: 7 total-size-cap: 1GB

这个配置能保证日志文件不会无限膨胀。大小触顶之后自动切割,老日志保留七天,超过总量就清理,运维起来省心不少。

9. 实战踩坑记录:一次编译失败引发的完整排查链路

讲完所有流程之后,我把这次搭建过程中印象最深的一个排查经历完整复盘一遍。希望你看完之后能掌握一套通用的排错思路,而不是遇到问题就盲搜报错。

现象是这样的:集成 MyBatis 后执行编译,Maven 直接报错package javax.sql does not exist。

我意识到这不是简单的依赖缺失,而是命名空间冲突。SpringBoot3 已经把数据访问的底层切到 Jakarta 规范,而项目中可能某个依赖仍然按其老版本预期引入javax.sql。

排查第一步,看依赖树:

mvn dependency:tree

输出里果然看到了被传递引入的旧版spring-boot-starter-jdbc相关包,以及某个老版本工具包强制依赖javax.sql。进一步的证据表明,某个平台组提供的内部 SDK 还是基于 SpringBoot2 打的包,里面的javax引用无法在 Boot3 的编译环境下解析。

排查思路明确之后,解决方式有两种:一是要求平台组升级 SDK 基线到 Boot3;二是在我们的工程里用exclusion把传递依赖里的冲突项排除掉。

<dependency> <groupId>com.internal</groupId> <artifactId>legacy-sdk</artifactId> <version>2.1.0</version> <exclusions> <exclusion> <groupId>javax.persistence</groupId> <artifactId>javax.persistence-api</artifactId> </exclusion> <!-- 其他排除项逐个补充 --> </exclusions> </dependency>

调整完再执行mvn clean package,编译通过。紧接着又冒出来一个新的问题:由于内部 SDK 运行时还需要旧版Servlet API,Tomcat 启动阶段直接报NoSuchMethodError。

这说明一个问题:光靠 exclusion 把编译期“混过去”不够,还得保证运行期每个类加载器拿到的 API 版本一致。正解是在pom.xml中把我们这边的标准依赖显式声明在最前面,利用 Maven 的“最近定义优先”原则把版本锁死:

<dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <scope>provided</scope> </dependency>

加上这条之后,Tomcat 正常启动,接口可以访问了。

这个排查过程给我最大的启发是:SpringBoot3 迁移排错要遵循“三层定位法”。第一层先查 JDK 版本是否 17+;第二层查 SpringBoot 版本与所有第三方 Stater 的兼容性;第三层查依赖树里的命名空间冲突。把这三个问题查透,绝大多数启动失败问题都能得到解决。

如果还想再快一点,建议任何引入的依赖,先到官方仓库确认它的最新版本是否标注了针对 SpringBoot3 的适配,再决定要不要引。不要迷信“最新就是最好”,适配性远比版本号大小重要。

10. 最后再说两句

项目从初始化到部署全链路走完之后,我给你几个沉淀下来的实操建议。

创建新项目一定走 Spring Initializr,构建方式选 Maven,Java 版本用 17,依赖按需添加,不要一开始就把全家桶全勾上。配置文件坚持“公共 + 环境”分离,敏感信息一律走环境变量。MyBatis Starter 版本选 3.x,Redis 的序列化配置一定要改。本地开发完接上测试,用@WebMvcTest写完核心接口的 MockMvc 验证。部署时别忘堆内存参数和优雅停机,再把健康检查和日志滚动配上,上线就能睡个安稳觉。

这些内容来自我实际项目的真实经验,很多细节都是文档上找不到的,但对少走弯路非常有帮助。如果你正在从 Boot2 迁移到 Boot3,或者刚打算用 Boot3 开新项目,照着这套流程走一遍,应该能省掉大量排错时间。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进