代码自动化迁移——用 AI 把老代码升到新版本框架
代码自动化迁移——用 AI 把老代码升到新版本框架
适读人群:维护老项目的工程师、Tech Lead | 阅读时长:约14分钟 | 核心价值:AI驱动代码迁移的真实工程实践,含失败教训
去年接了一个活:把一个运行了 6 年的项目从 Spring Boot 2.7 迁移到 Spring Boot 3.2,同时把 Java 版本从 8 升到 17。
项目规模:约 8 万行业务代码,200 多个接口,数据库用 MySQL + Hibernate。
同事的预估:3 个人全力干,2 个月。
我当时想着用 AI 辅助试试,结果花了 1 个人 5 周时间完成了核心迁移。这里面有成功的经验,也有几个让我踩了坑的地方,都值得记录。
迁移的真实难点
Spring Boot 2 → 3 的迁移,官方有 Migration Guide,看起来挺详细。但真正动手之后发现,难点不在于"知道哪些 API 变了",而在于:
- 改动量太大,人工处理会出遗漏:我这个项目里有大约 340 处需要改动
- 改完要验证:改完了不知道有没有漏,不知道改错了没有
- 有些改动有业务逻辑依赖:不是机械地替换,要理解上下文才能改对
AI 在第 1 点上能发挥最大价值,第 2 点需要配套的测试策略,第 3 点 AI 有时候会犯错。
我的迁移工作流
第一步:生成迁移 Checklist
不要一上来就改代码。先用 AI 生成一份针对你这个项目的迁移 Checklist。
给 AI 的输入:
pom.xml/build.gradle- 主要的配置文件(
application.yml) - 一个有代表性的 Controller + Service + Repository 示例
让 AI 输出:针对这个项目的具体改动清单,不是通用文档,是针对你用到的依赖和代码模式的具体列表。
AI 给我生成的清单(节选):
Spring Boot 2.7 -> 3.2 迁移清单(针对本项目)
【高优先级 - 必须改,会编译报错】
1. javax.* -> jakarta.* 包名迁移
- 发现约 127 处 javax.servlet.*
- 发现约 43 处 javax.persistence.*
- 发现约 18 处 javax.validation.*
2. Spring Security 配置 API 变更
- WebSecurityConfigurerAdapter 已移除,需改为 SecurityFilterChain Bean
- 发现 3 个 Security 配置类继承了 WebSecurityConfigurerAdapter
3. Hibernate 6 升级
- org.hibernate.query.Query.getResultList() 返回类型变化
- 原生 HQL 查询里用到 from entity 的写法可能需要调整
【中优先级 - 不改功能可用但有告警或隐患】
4. Spring MVC 中 RequestMapping 处理变化
- 末尾斜杠不再自动匹配,/api/users/ 和 /api/users 不再等效
- 发现项目里有客户端直接使用带尾斜杠的 URL
5. 配置项重命名
- spring.datasource.initialization-mode -> spring.sql.init.mode
- spring.mvc.pathmatch.use-suffix-pattern 已移除这个清单是可执行的起点,比通用文档有用得多。
第二步:分类批处理
把改动分成三类:
A 类:机械替换(安全自动化)
javax. → jakarta. 是最典型的机械替换,用脚本处理:
import os
import re
REPLACEMENTS = [
("javax.servlet.", "jakarta.servlet."),
("javax.persistence.", "jakarta.persistence."),
("javax.validation.", "jakarta.validation."),
("javax.transaction.", "jakarta.transaction."),
("javax.annotation.", "jakarta.annotation."),
]
def migrate_file(file_path: str) -> tuple[bool, list[str]]:
"""
返回 (changed: bool, changes: list[str])
"""
with open(file_path, "r", encoding="utf-8") as f:
original = f.read()
modified = original
changes = []
for old, new in REPLACEMENTS:
count = modified.count(old)
if count > 0:
modified = modified.replace(old, new)
changes.append(f" {old} -> {new} ({count}处)")
if modified != original:
with open(file_path, "w", encoding="utf-8") as f:
f.write(modified)
return True, changes
return False, []
def migrate_directory(root_dir: str):
total_files = 0
changed_files = 0
total_changes = 0
for dirpath, _, filenames in os.walk(root_dir):
for filename in filenames:
if not filename.endswith(".java"):
continue
file_path = os.path.join(dirpath, filename)
total_files += 1
changed, changes = migrate_file(file_path)
if changed:
changed_files += 1
total_changes += len(changes)
print(f"[CHANGED] {file_path}")
for c in changes:
print(c)
print(f"\n总计: {total_files} 个文件, {changed_files} 个修改, {total_changes} 处替换")
if __name__ == "__main__":
migrate_directory("src/main/java")这个脚本处理了我项目里 188 处包名替换,10 分钟跑完,0 出错。
B 类:有规律但需要上下文的改动
Security 配置迁移是典型的 B 类。旧的写法:
// Spring Boot 2 写法
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().disable()
.httpBasic();
}
}我把每个 Security 配置类(一共 3 个)分别给 AI,让它迁移:
// Spring Boot 3 写法(AI 生成)
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}AI 生成的迁移基本正确,但我需要逐一 Review,因为有时候它会把一些自定义的 Customizer 细节处理得不对。
C 类:需要理解业务含义的改动
这类改动 AI 帮不上太多,或者说帮了你还要仔细验证。
我项目里有一个自定义的 HQL 查询:
// 原始代码
Query query = session.createQuery(
"FROM Order o WHERE o.userId = :userId AND o.status IN (:statuses)"
);
query.setParameter("userId", userId);
query.setParameterList("statuses", statuses);
List<Order> results = query.list();在 Hibernate 6 里,setParameterList 的行为有变化,而且 query.list() 被标记为废弃,需要改成 getResultList()。AI 可以告诉你改法,但它不知道你的 Order 实体有没有其他关联关系会受影响,这个还是要自己确认。
第三步:验证方案(这是最关键的)
改完代码,怎么知道改对了?
我的验证方案分三层:
层1:编译验证
这是最基础的,但很多人到这里就停了,觉得编译通过就好了。不够。
层2:接口快照测试
在迁移之前,用脚本把所有接口的测试请求和响应录下来:
import requests
import json
from pathlib import Path
BASE_URL_OLD = "http://localhost:8080" # 迁移前的实例
SNAPSHOT_DIR = Path("migration_snapshots")
SNAPSHOT_DIR.mkdir(exist_ok=True)
# 预定义的测试场景(覆盖核心业务路径)
TEST_CASES = [
{"name": "query_orders_by_user", "method": "GET",
"path": "/api/orders?userId=test001&statuses=PAID"},
{"name": "query_orders_no_status", "method": "GET",
"path": "/api/orders?userId=test001"},
# ... 更多场景
]
def capture_snapshot(test_case: dict):
resp = requests.request(
method=test_case["method"],
url=f"{BASE_URL_OLD}{test_case['path']}",
headers={"Authorization": "Bearer test-token"}
)
snapshot = {
"status_code": resp.status_code,
"body": resp.json() if resp.content else None,
"headers": dict(resp.headers)
}
file = SNAPSHOT_DIR / f"{test_case['name']}.json"
file.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2))
def compare_with_snapshot(test_case: dict):
BASE_URL_NEW = "http://localhost:8081" # 迁移后的实例
resp = requests.request(
method=test_case["method"],
url=f"{BASE_URL_NEW}{test_case['path']}",
headers={"Authorization": "Bearer test-token"}
)
snapshot_file = SNAPSHOT_DIR / f"{test_case['name']}.json"
old_snapshot = json.loads(snapshot_file.read_text())
new_response = {
"status_code": resp.status_code,
"body": resp.json() if resp.content else None
}
if old_snapshot["status_code"] != new_response["status_code"]:
print(f"[FAIL] {test_case['name']}: 状态码变化 "
f"{old_snapshot['status_code']} -> {new_response['status_code']}")
return False
# 响应体比较(忽略时间戳等动态字段)
# ... 具体实现根据你的响应结构来
return True这个快照测试能发现 "改完能跑,但返回结果变了" 的问题,这类问题非常隐蔽。
层3:人工确认节点
我设定了几个必须人工 Review 的节点,不能全自动通过:
- 所有 Security 配置改动
- 涉及事务管理的改动
- 数据库查询语句的改动
这些地方 AI 出错的概率最高,或者说出错的代价最高。
失败的经验:我在哪里翻车了
有两处改动 AI 给了我错误的方案,上线后才发现:
问题1:Spring MVC 尾斜杠匹配
AI 知道 Spring Boot 3 默认不再匹配尾斜杠,给我的建议是加全局配置:
// AI 建议的方案
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true); // 恢复旧行为
}
}但 setUseTrailingSlashMatch 在 Spring Boot 3.2 里已经被废弃并移除了,AI 给了一个 3.0 时期可用但现在不能用的方案。最后我是在 Nginx 层统一去掉了尾斜杠。
问题2:Hibernate 的 @GeneratedValue 策略
AI 在迁移一个实体类时,把 @GeneratedValue(strategy = GenerationType.AUTO) 保持了不变,但没有告诉我 Hibernate 6 里 AUTO 策略在 MySQL 下的行为从使用 hibernate_sequence 表改成了使用 sequences,如果数据库里没有建对应的 sequence,第一次插入会报错。
这个问题在测试环境没发现,因为测试环境每次都重建数据库,sequence 会自动创建。生产上用现有数据库,sequence 不存在,第一次插入数据直接报错。
教训:迁移后必须在和生产环境一致的数据库版本上跑完整测试,不能只用开发环境。
最终结果
5 周,1 个人,完成了核心迁移。
比原始预估(3人×2个月)节省了大量时间,其中 AI 辅助在机械替换和 B 类改动上贡献最大,大概帮我省了 40% 的时间。
但这 40% 不是"省下来玩"——省下来的时间基本都花在验证、测试、处理 AI 犯错导致的坑上。
AI 让迁移变快了,但没让迁移变容易。你还是需要懂这些框架,才能判断 AI 给的方案对不对。
