资讯动态

Spring Boot老项目升级实战:从JDK8到JDK17的完整迁移指南

发布时间:2026/9/7 13:12:37 来源:尧图企业网站定制
开发三年以上的后端同学手头几乎都会有几个“老项目”。它们可能基于 Spring Boot 2、Java 8甚至更早的 SSM 体系。表面上系统还在正常跑实际已经积累了大量技术债依赖版本老旧、JDK 无法升级、安全漏洞无法修复、新同事不敢随便动代码。遇到一次线上故障排查依赖冲突就能花掉半天。“真神复活”这个标题并不是在聊某个游戏角色而是想聊一件后端开发里更现实的事如何让那些被时代“判了缓刑”的老项目重新在新版本技术栈里活过来。本文会用一套完整、可复制的 Spring Boot 老项目升级实战案例覆盖核心原理、迁移步骤、常见报错与工程化建议帮你在不推翻重写的前提下让老项目重新具备继续演进的底气。如果你正准备升级手里的遗留系统或者只是想知道 Spring Boot 3、JDK 17 到底和老项目有哪些差异这篇文章都值得收藏慢慢看。1. “真神复活”到底在说什么——技术债与遗留系统重构1.1 老项目为什么会“死”说一个很常见的演进路径。团队早期为了快速上线选择了当时最顺手的技术组合JDK 8 Spring Boot 2.xMyBatis / JPA 直连数据库前端页面由后端模板渲染或者单一 Vue 工程前后端分离大量公共类写在commons模块里内部互相依赖上线之后业务需求一个接一个这个“技术神像”就开始慢慢出现裂缝依赖锁定某个老版本组件只有旧版才兼容升级一个包就会连带升级一串包。安全漏洞Spring Boot 2.0 早期版本存在大量已知 CVE但团队因为兼容性问题一直不敢升级。JDK 版本停滞想用 Java 17 的新特性但项目还跑在 JDK 8 上。新人不愿接手代码能跑但没人能说清楚整体链路改造风险高。这就是典型的技术债。技术债不是一次写坏代码产生的而是每一次“先上线、后优化”的选择积累出来的。等债越积越多系统不会立刻崩溃但会表现为“改不动、升不了、不敢碰”。1.2 “复活”的本质是什么“复活”不等于推翻重写而是用可控成本把项目迁移到一条更健康的技术主线上。正规的做法通常有两种方案思路优点风险渐进式升级在保留业务功能的前提下升级依赖、JDK、框架版本风险可控、可回滚、逐步验证周期较长需要多次回归测试绞杀者模式用新框架逐步替换老模块老模块逐步下线最终架构更干净需要双轨并行工程量较大大多数中小团队更适合渐进式升级。核心是先把运行时环境、基础依赖、编译基线升级到新版本再逐步替换掉不合理的编码方式和设计而不是一次性把所有代码全部重写。1.3 本文实战案例范围为了让教程足够具体本文会以一套典型的“老项目”为例技术栈基线升级前 - JDK 8 - Spring Boot 2.3.x - Spring Security 5.x - MyBatis 3.x - Maven 3.6升级目标技术栈基线升级后 - JDK 17长期支持版本 - Spring Boot 3.x - Spring Security 6.x - MyBatis 适配版本 - Maven 3.8你不需要保证项目结构完全一致核心思路、迁移步骤、报错排查方法都是通用的。版本号需要根据你手里的实际项目调整这一点后面会反复强调。2. 环境准备与版本说明2.1 运行环境在开始升级前先整理本地开发环境。本文示例环境如下操作系统Windows 10 / macOS / Linux 均可JDK升级到 JDK 17构建工具Maven 3.8IDEIntelliJ IDEA 或 Eclipse数据库MySQL 8.x也可以用你项目正在使用的数据库版本管理Git这里要特别注意一个点JDK 17 和老项目之间不是只换一个版本号那么简单。Spring Boot 3 强制要求 JDK 17 起步而 JDK 8 到 JDK 17 之间有不少内置 API 变化比如javax.*中部分 API 被移除或模块化处理SecurityManager相关逻辑变化反射机制加强封装限制setAccessible行为变化某些第三方底层库依赖内部 JDK API可能导致启动失败2.2 示例项目结构为了更贴近真实情况模拟一个后端服务legacy-demo ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/example/legacy │ │ │ ├── LegacyApplication.java │ │ │ ├── config │ │ │ │ ├── SecurityConfig.java │ │ │ │ └── MybatisConfig.java │ │ │ ├── controller │ │ │ │ └── UserController.java │ │ │ ├── entity │ │ │ │ └── User.java │ │ │ ├── mapper │ │ │ │ └── UserMapper.java │ │ │ └── service │ │ │ └── UserService.java │ │ └── resources │ │ ├── application.yml │ │ └── mapper │ │ └── UserMapper.xml │ └── test │ └── java这个结构很常见迁移重点在于改造pom.xml、启动类、配置类以及受 API 变化影响的代码。2.3 升级前先做版本盘点不要直接动手改代码。升级前先花 30 分钟做一次依赖盘点把pom.xml里所有依赖列出来逐个确认是否有 Spring Boot 3 对应版本是否还维护中是否与 JDK 17 兼容是否存在已知 CVE 漏洞如果是内部自研组件或者不维护的公共库需要先评估替换方案。建议用 Maven 命令快速导出依赖树mvn dependency:tree dependency-tree.txt导出后重点看spring-boot-starter-*、数据库驱动、连接池、Json 库、Redis 客户端这几类关键依赖。如果项目很大建议优先用一个独立分支做升级实验不要直接在主干上操作。3. 升级前必须搞懂的核心差异Spring Boot 2 升级到 Spring Boot 3不只是大版本号 1。最核心的变化是Jakarta EE 9 带来的命名空间迁移以及Spring Security 6 配置方式重写。这两点是绝大多数排错问题的根源。3.1 javax → jakarta 命名空间迁移Java EE 移交 Eclipse 基金会后所有规范包名从javax.*改成了jakarta.*。这意味着老代码里的import javax.persistence.Entity; import javax.validation.Valid; import javax.servlet.http.HttpServletRequest;需要改成import jakarta.persistence.Entity; import jakarta.validation.Valid; import jakarta.servlet.http.HttpServletRequest;Spring Boot 3 的自动配置、内嵌 Tomcat、Spring MVC 底层全部使用jakarta.*。如果代码中还有javax.servlet、javax.validation这类导入编译期就可能直接报找不到类。常见受影响的位置实体类上的 JPA 注解参数校验注解如NotNull、ValidHttpServletRequest、HttpServletResponse过滤器、拦截器、监听器WebSocket 相关 API升级时最稳妥的做法是在 IDE 中全局搜索import javax.逐个确认是否属于 Jakarta 迁移范围。3.2 Spring Boot 2 到 3 的依赖坐标变化Spring Boot 3 中部分依赖坐标和版本管理发生了变化。比如springfoxSwagger 2基本停止维护需要换成springdoc-openapijavax.inject改为jakarta.injectjavax.annotation改为jakarta.annotationMyBatis 官方提供了适配 Spring Boot 3 的新版本mybatis-spring-boot-starter下面是一个老版本依赖坐标的示例dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.1.4/version /dependency这个版本在 Spring Boot 3 下可能无法直接使用需要升级到支持 Spring Boot 3 的版本例如 3.x 系列具体版本以官方发布为准。推荐在升级时优先查看 Spring Boot 官方 Upgrade 文档和每个第三方库的 Release Notes不要使用过旧版本强行兼容。3.3 Spring Security 配置方式变化Spring Security 5 老写法中常见的是继承WebSecurityConfigurerAdapterConfiguration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/public/**).permitAll() .anyRequest().authenticated() .and() .formLogin(); } }Spring Security 6 中WebSecurityConfigurerAdapter被移除推荐使用基于SecurityFilterChain的组件式写法Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth - auth .requestMatchers(/public/**).permitAll() .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); } }差异点总结Spring Security 5Spring Security 6WebSecurityConfigurerAdapterSecurityFilterChainBeanauthorizeRequests()authorizeHttpRequests()antMatchers()requestMatchers()and()链式写法Lambda 风格配置如果老项目大量依赖WebSecurityConfigurerAdapter升级时需要一个一个改。功能上尽量保持一致不要顺手把权限规则也改了否则容易引入安全问题。3.4 配置文件与属性项变更Spring Boot 3 中部分配置属性被重命名或删除。例如spring.redis.*→spring.data.redis.*spring.datasource.*基本保持但部分类型推断策略调整server.servlet.session.timeout仍可用但底层实现有变化升级后项目启动时如果配置项失效Spring Boot 通常会输出警告提示例如The following configuration properties are invalid: spring.redis.host不要忽略这些警告把所有失效配置项逐个修正。4. 完整实战让老项目在新环境里“复活”下面开始一个最小可运行的迁移实战。为了便于理解我们会把完整过程拆成 6 步。4.1 备份与创建迁移分支无论项目大小一定要先备份。推荐用 Git 创建独立分支这样随时可以回滚。git checkout -b feature/upgrade-springboot3如果是老项目建议同时打一个 taggit tag legacy-springboot2-before-upgrade这里要提醒涉及生产环境或团队协作项目时一定要先走评审和测试流程不要直接在主干上操作。4.2 改造 pom.xml升级第一步是修改父工程或 parent 配置。老项目可能是parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.3.12.RELEASE/version relativePath/ /parent改为parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent注意这里的版本号是本文撰写时的示例你在实际项目中要以官方当前稳定版本为准。随后把java.version改为 17properties java.version17/java.version maven.compiler.source17/maven.compiler.source maven.compiler.target17/maven.compiler.target /properties如果项目中使用了自定义的第三方依赖版本也需要同步调整。例如 MyBatis Starter、数据库驱动、Redis 客户端等。一个经过调整的依赖参考dependencies !-- Spring Boot Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Spring Security -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency !-- MyBatis Starter需选用适配 Spring Boot 3 的版本 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- 参数校验 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency !-- 测试 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency /dependencies这里可以看到mysql-connector-java坐标在老版本是dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.23/version /dependency新版本驱动坐标改为了com.mysql:mysql-connector-j由 Spring Boot 的依赖管理统一控制版本。4.3 修改启动类和业务代码启动类本身变化不大// 文件路径src/main/java/com/example/legacy/LegacyApplication.java package com.example.legacy; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication MapperScan(com.example.legacy.mapper) public class LegacyApplication { public static void main(String[] args) { SpringApplication.run(LegacyApplication.class, args); } }重点是实体类、DTO、Controller 中涉及javax.*的导入。下面用参数校验举例。升级前// 文件路径src/main/java/com/example/legacy/controller/UserController.java package com.example.legacy.controller; import com.example.legacy.entity.User; import com.example.legacy.service.UserService; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; RestController RequestMapping(/user) public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService userService; } PostMapping public User createUser(RequestBody Valid User user) { return userService.createUser(user); } }升级后把javax.validation.Valid改为import jakarta.validation.Valid;如果其他地方还用到javax.servlet.http.HttpServletRequest也需要一并改为import jakarta.servlet.http.HttpServletRequest;字符串、集合、时间处理等工具类通常不受影响但要注意部分反射工具和字节码增强库可能对 JDK 17 的模块化机制更敏感。遇到这种情况优先升级工具库版本而不是用 JVM 参数强行绕过。4.4 升级 Spring Security 配置继续用前面提到的SecurityConfig举例。改造前// 文件路径src/main/java/com/example/legacy/config/SecurityConfig.java package com.example.legacy.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers(/public/**).permitAll() .antMatchers(/user/**).authenticated() .anyRequest().permitAll() .and() .formLogin(); } }改造后// 文件路径src/main/java/com/example/legacy/config/SecurityConfig.java package com.example.legacy.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; Configuration EnableWebSecurity public class SecurityConfig { Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth .requestMatchers(/public/**).permitAll() .requestMatchers(/user/**).authenticated() .anyRequest().permitAll() ) .formLogin(Customizer.withDefaults()); return http.build(); } }重点解释几个变化authorizeRequests()改为authorizeHttpRequests()底层使用新的AuthorizationManager机制。antMatchers()改为requestMatchers()从 Spring Security 5.8 开始推荐写法。配置类不再继承父类改为暴露返回SecurityFilterChain的 Bean。.csrf().disable()改为.csrf(csrf - csrf.disable())样式上更符合 Lambda 风格。如果你在项目中还配置了密码加密器建议显式声明PasswordEncoderBeanBean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }4.5 更新配置文件与脚本老项目application.yml里可能有一堆spring.redis开头的配置。升级前spring: redis: host: localhost port: 6379Spring Boot 3 中推荐改为spring: data: redis: host: localhost port: 6379数据库连接配置基本不变但要注意数据库驱动类名。老配置可能是spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/legacy_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root新版驱动坐标变化后driver-class-name仍然可以使用com.mysql.cj.jdbc.Driver这一点在不同版本中保持兼容暂时不用改。另外如果 Maven 的settings.xml或 CI/CD 脚本中写死了 JDK 8 路径记得同步修改。例如 Jenkins 构建脚本里的export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64需要改为 JDK 17 的路径。4.6 运行验证完成以上改造后先执行编译mvn clean compile编译通过后启动项目mvn spring-boot:run或者先打包再运行mvn clean package -DskipTests java -jar target/legacy-demo-0.0.1-SNAPSHOT.jar启动成功后如果配置了 Spring Security访问接口时会出现登录页。测试一下几个关键路径/public/**无需登录直接访问/user/**未登录时返回 401 或跳转登录页数据库读写正常5. 常见报错与排查思路升级过程中你会遇到一批“看起来一模一样”但根源各异的报错。下面整理最典型的几类。5.1 启动失败ClassNotFoundException: javax.servlet.Filter这个报错非常典型根源是代码或者第三方依赖还在引用javax.servlet.*。排查顺序在 IDEA 中全局搜索javax.servlet。看是自有代码还是第三方 Jar 包内部引用。自有代码直接改为jakarta.servlet。第三方库需要升级到适配版本或者用mvn dependency:tree定位来源。如果反复检查代码中没有javax.servlet可以执行mvn dependency:tree -Dincludesjavax.servlet定位到具体依赖后再决定升级还是排除。5.2 编译报错无法将 WebSecurityConfigurerAdapter 解析为类型这就是典型的 Spring Security 6 兼容问题。解决办法是把配置类重写为SecurityFilterChain方式参考第 4.4 节。不要在项目里继续继承已经删除的类。如果某个内部公共包大量使用了WebSecurityConfigurerAdapter需要统一升级公共包而不是单独在业务项目里做兼容。5.3 运行期接口全部返回 401 或 403最可能的原因是 Spring Security 的默认行为和旧版不同尤其是没有正确配置requestMatchers(/public/**).permitAll()。另外注意Spring Security 6 默认会对 CSRF 进行拦截对 POST、PUT、DELETE 请求会校验 CSRF Token。老项目如果依赖csrf().disable()升级后必须明确配置否则测试接口时会遇到 403。可以用下面这个简易配置排查http .csrf(csrf - csrf.disable()) .authorizeHttpRequests(auth - auth.anyRequest().permitAll());如果这样写后接口能正常访问说明问题出在权限规则上再逐步把精确规则加回去。5.4 启动时报错java.lang.reflect.InaccessibleObjectExceptionJDK 17 对反射做了强封装限制部分老库通过反射访问 JDK 内部 API会抛出Unable to make field private final java.util.Map java.util.Collections$EmptyMap.serializable accessible这类报错背后通常是 CGLIB、Byte Buddy、ASM 等字节码工具版本过旧或者数据库驱动、Redis 客户端版本不兼容 JDK 17。正确做法是升级对应依赖版本不要直接加--add-opens绕过去否则只是掩盖问题并可能留下安全隐患。5.5 表格汇总问题现象常见原因解决思路启动失败找不到javax.servlet类代码或依赖仍使用 Java EE 老命名空间全局替换javax.*为jakarta.*升级依赖WebSecurityConfigurerAdapter编译错误Spring Security 6 删除旧基类重写为SecurityFilterChainBean接口 403CSRF 默认开启或权限规则失效明确配置 CSRF 策略和requestMatchersInaccessibleObjectException字节码库与 JDK 17 不兼容升级 CGLIB、Byte Buddy 等依赖配置项告警spring.redis.*invalidSpring Boot 3 重命名配置前缀改为spring.data.redis.*数据库驱动加载异常老驱动坐标不适合 Spring Boot 3改用新坐标并交由 Boot 管理版本6. 工程化最佳实践“升级一次成功”不算本事“升级后能持续演进”才是目标。下面几条工程化建议非常值得落地。6.1 迁移前做静态盘点升级前建议把代码库的依赖、API 使用情况、废弃用法做个静态扫描。可以使用 IDE 自带检查、mvn dependency:tree以及代码仓库的全局搜索。重点盘点所有javax.*导入所有继承WebSecurityConfigurerAdapter的类所有spring.redis等即将失效的配置项所有反射调用外部依赖的场景形成一份清单后按照“依赖升级 → 编译修复 → 运行修复 → 回归测试”的顺序推进。6.2 依赖统一管理老项目最容易出现“同一个包多个版本”的问题。升级后建议统一使用 Spring Boot BOM 管理版本业务模块不显式写版本号。第三方库的版本由properties统一维护。定期执行mvn versions:display-dependency-updates检查版本更新。例如properties mybatis-starter.version3.0.3/mybatis-starter.version /properties这样后续升级时只需改一个地方。6.3 兼容层与灰度回滚如果项目里有大量公共模块无法一次性替换javax.*导入可以考虑短期维护一段兼容层但不要长期依赖。更推荐的是“路由规则”策略新版本服务上线后通过网关或注册中心做灰度先让 10% 流量进入新版本验证稳定后再逐步放大。一旦出现异常立即切回老版本。整个过程中数据库结构尽量保持兼容不建议在升级框架版本的同时执行大规模表结构变更否则一旦回滚数据一致性问题会非常棘手。6.4 日志与监控升级后的前两周要特别关注日志和监控。重点检查启动日志中有没有配置项告警有没有异常堆栈被吞掉慢 SQL 是否变多内存和 GC 表现是否正常建议对比升级前后同一时段的基础指标。JDK 17 默认的 G1 垃圾回收器表现通常不错但每个项目情况不同要以实际监控数据为准。6.5 安全边界老项目升级后不要忘记检查安全配置密码加密方式是否还符合当前标准如BCryptPasswordEncoder。CSRF 策略是否符合业务场景。Session 管理是否合理是否需要引入分布式会话。数据库账号是否遵循最小权限原则代码中的账号密码不能硬编码。升级期间如果发现中间件存在已知 CVE应优先修复而不是等所有功能迁移完再处理。7. 总结与后续建议通过这次升级实战你已经走完了一套老项目“复活”的标准路径理解了老项目为什么会积累技术债以及“渐进式升级”为什么比重写更稳。掌握了 Spring Boot 3 和 JDK 17 带来的核心差异包括javax到jakarta、Spring Security 6 配置重写、配置属性变更。用一份最小可运行的 Maven 工程完成了从pom.xml到启动类、安全配置、配置文件的完整迁移。整理了一份常见报错排查清单并补充了依赖升级、灰度发布、安全收尾等工程化建议。升级过程中最忌讳的是“为了升级而升级”。每一次依赖变更、每一处安全配置调整都应该有明确的理由和回归验证。与其一口气把整个系统翻新一遍不如先让它在新版本技术上稳定运行再逐步优化代码结构和业务设计。接下来你可以按三个方向继续深入继续学习 Spring Security 6 的授权模型了解AuthorizationManager、OAuth2 资源服务器等新能力。掌握 JDK 17 常用新特性比如sealed class、record、switch表达式这些能让代码更简洁。梳理你手里项目的依赖树做一次笔记式的版本基线记录以后每次升级都有据可查。希望这篇文章能帮你的老项目重新“活”过来。如果你在升级过程中遇到其他奇怪的报错也欢迎把报错信息整理出来对照排查。动手实践一次比收藏十篇教程更有效。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价