AI 辅助代码迁移——用 AI 把老代码升到新版本框架(进阶篇)
AI 辅助代码迁移——用 AI 把老代码升到新版本框架(进阶篇)
Spring Boot 2 到 3 的迁移,我做过不止一次。第一次手工做,做了三个月,踩了无数坑。第二次用 AI 辅助,同等体量的项目,六周完成。
但我不想讲那种"AI 帮你把 javax 改成 jakarta"的简单案例,那个用搜索替换就能搞定,不需要 AI。
我要讲的是真正麻烦的迁移场景:废弃 API 没有直接替代品、行为语义发生了变化、安全配置体系完全重写。这些场景,没有理解能力的工具帮不了你,但 AI 可以。
Spring Boot 2 -> 3 迁移的复杂场景分类
Spring Boot 3 的迁移比大家想的复杂得多,远不只是包名替换。
场景一:废弃 API 没有直接替代品
典型例子:WebSecurityConfigurerAdapter
Spring Boot 2 的安全配置:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER");
}
}Spring Boot 3 废弃了 WebSecurityConfigurerAdapter,但不是简单地换一个父类。整个安全配置的设计理念变了:从"继承并覆盖"变成了"声明 Bean"。
你不能只是删掉 extends WebSecurityConfigurerAdapter,因为方法签名变了,结构变了,有些功能移到了不同的地方。
这种场景,搜索替换没用,需要理解"为什么要这样改"才能正确迁移。
场景二:行为语义变化
典型例子:Spring Security 默认行为
Spring Boot 2 中,antMatchers 是路径匹配的主要方式。Spring Boot 3 引入了 requestMatchers,默认行为不同:
- Spring Boot 2:
antMatchers("/**")匹配所有请求 - Spring Boot 3:同样的路径,在有多个
DispatcherServlet的情况下行为不同
如果你的配置直接替换了方法名但没有理解语义变化,测试可能过了,但在特定部署环境下会出现权限漏洞。
场景三:自动配置行为变化
Spring Boot 3 的很多自动配置发生了默认值变化:
- Actuator 端点默认暴露方式不同
- Logging 配置格式变化
- HTTP/2 支持的配置方式变化
这类问题不会报编译错误,但会导致运行时行为异常,排查起来很费时间。
AI 辅助迁移的工作流设计
处理复杂迁移场景,AI 能做三件事:
- 分析影响范围:识别代码里所有需要改动的地方,按复杂度分类
- 提供迁移方案:对于复杂场景,给出具体的迁移思路和代码
- 验证迁移正确性:对迁移后的代码做合理性检查
关键是:不要让 AI 直接生成整个迁移后的代码,而是让它分析问题、提供思路,你来写实际的代码。AI 对你的业务逻辑不了解,直接让它写往往会生成看起来正确但语义错误的代码。
实现:AI 辅助迁移工作流
数据结构
@Data
public class MigrationAnalysisResult {
private String filePath;
private List<MigrationIssue> issues;
private MigrationComplexity overallComplexity;
private String summary;
public enum MigrationComplexity {
TRIVIAL, // 简单替换,可以自动化
MODERATE, // 需要理解,但有明确替代品
COMPLEX, // 需要重构,无直接替代品
RISKY // 行为语义变化,需要充分测试
}
}
@Data
public class MigrationIssue {
private String issueId;
private IssueCategory category;
private String description;
private String codeLocation; // 文件名:行号
private String originalCode; // 原始代码片段
private String suggestedFix; // 建议的修复方案
private MigrationComplexity complexity;
private List<String> references; // 参考文档链接
private String riskAssessment; // 风险评估
public enum IssueCategory {
PACKAGE_RENAME, // 包名变更(javax->jakarta)
API_DEPRECATED_WITH_REPLACEMENT, // 废弃但有替代品
API_DEPRECATED_NO_REPLACEMENT, // 废弃无直接替代品
BEHAVIOR_CHANGE, // 行为语义变化
CONFIGURATION_CHANGE, // 配置方式变化
DEPENDENCY_CHANGE // 依赖变化
}
}迁移分析服务
@Service
@Slf4j
public class MigrationAnalysisService {
private final ChatClient chatClient;
private static final String MIGRATION_ANALYSIS_PROMPT = """
你是一个 Spring Boot 框架迁移专家。
请分析以下 Java 代码,识别从 Spring Boot 2.x 迁移到 Spring Boot 3.x 时需要处理的问题。
特别关注:
1. 废弃的 API(尤其是没有直接替代品的)
2. 包名变更(javax.* 到 jakarta.*)
3. 行为语义发生变化的 API
4. Spring Security 相关配置的变化
5. 自动配置行为变化
对于每个问题,评估:
- 问题类型
- 迁移复杂度(TRIVIAL/MODERATE/COMPLEX/RISKY)
- 具体的迁移思路
- 潜在风险
以JSON格式输出:
{
"issues": [
{
"issueId": "唯一ID",
"category": "问题类别",
"description": "问题描述",
"codeLocation": "代码位置(方法名或行)",
"originalCode": "有问题的代码片段",
"suggestedFix": "具体的迁移思路(不是完整代码,是思路)",
"complexity": "TRIVIAL/MODERATE/COMPLEX/RISKY",
"riskAssessment": "风险评估"
}
],
"overallComplexity": "整体复杂度",
"summary": "总体评估"
}
待分析的代码文件({fileName}):
```java
{code}
```
""";
public MigrationAnalysisResult analyzeFile(Path filePath) throws IOException {
String code = Files.readString(filePath);
String fileName = filePath.getFileName().toString();
// 如果文件不包含Spring相关内容,跳过
if (!containsSpringAnnotations(code)) {
return MigrationAnalysisResult.noIssues(filePath.toString());
}
String prompt = MIGRATION_ANALYSIS_PROMPT
.replace("{fileName}", fileName)
.replace("{code}", truncateCode(code));
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
return parseMigrationResult(filePath.toString(), response);
}
// 对 Security 配置文件专项分析(这类文件最复杂)
public SecurityMigrationGuide analyzeSecurityConfig(Path securityConfigPath) throws IOException {
String code = Files.readString(securityConfigPath);
String prompt = """
分析以下 Spring Security 配置类,它基于 Spring Boot 2.x 的 WebSecurityConfigurerAdapter 风格。
请提供迁移到 Spring Boot 3.x 的详细指导:
1. 识别所有需要改变的配置
2. 解释新的配置方式(基于Bean声明而不是继承)
3. 特别说明哪些地方行为可能发生变化
4. 提供完整的迁移后代码(这里可以提供代码,因为Security配置模式较为通用)
5. 列出需要额外测试的场景
原始配置:
```java
%s
```
""".formatted(code);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
SecurityMigrationGuide guide = new SecurityMigrationGuide();
guide.setOriginalFile(securityConfigPath.toString());
guide.setAnalysis(response);
return guide;
}
private boolean containsSpringAnnotations(String code) {
return code.contains("@Component") || code.contains("@Service") ||
code.contains("@Controller") || code.contains("@Configuration") ||
code.contains("@RestController") || code.contains("@Repository") ||
code.contains("springframework");
}
private String truncateCode(String code) {
// 代码文件太长时只分析前后各1000行
String[] lines = code.split("\n");
if (lines.length <= 200) return code;
StringBuilder truncated = new StringBuilder();
for (int i = 0; i < 100; i++) {
truncated.append(lines[i]).append("\n");
}
truncated.append("\n// ... (中间 ").append(lines.length - 200).append(" 行已省略)...\n\n");
for (int i = lines.length - 100; i < lines.length; i++) {
truncated.append(lines[i]).append("\n");
}
return truncated.toString();
}
}迁移执行助手
@Service
@Slf4j
public class MigrationExecutionAssistant {
private final ChatClient chatClient;
private final MigrationAnalysisService analysisService;
// 为TRIVIAL级别的问题生成自动修复补丁
public String generateAutoPatch(MigrationIssue issue) {
if (issue.getComplexity() != MigrationAnalysisResult.MigrationComplexity.TRIVIAL) {
throw new IllegalArgumentException("只有TRIVIAL级别的问题才支持自动修复");
}
String prompt = """
请为以下代码迁移问题生成一个精确的替换补丁。
问题类型:%s
原始代码:
```java
%s
```
迁移建议:%s
请直接输出修复后的代码(不需要解释),要求:
1. 只修改必要的部分
2. 保留原有的注释和格式
3. 修复后的代码必须语法正确
""".formatted(issue.getCategory(), issue.getOriginalCode(), issue.getSuggestedFix());
return chatClient.prompt()
.user(prompt)
.call()
.content();
}
// 为复杂迁移场景生成详细的迁移指南(带步骤)
public MigrationGuide generateComplexMigrationGuide(MigrationIssue issue, String fullFileContent) {
String prompt = """
需要将以下 Spring Boot 2.x 代码迁移到 3.x,这是一个复杂的迁移场景。
问题描述:%s
相关代码上下文:
```java
%s
```
请提供分步骤的迁移指南:
1. 第一步:...(具体操作)
2. 第二步:...
...
对于每一步,说明:
- 为什么要做这个改动(不只是怎么做)
- 可能遇到的问题
- 验证这步正确的方法
同时提供:
- 需要新增的依赖(如果有)
- 需要新增的配置
- 功能等价性验证的测试案例
""".formatted(issue.getDescription(), extractRelevantCode(fullFileContent, issue));
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
MigrationGuide guide = new MigrationGuide();
guide.setIssueId(issue.getIssueId());
guide.setGuideContent(response);
return guide;
}
// 验证迁移后代码的合理性(不是运行,是静态分析)
public MigrationValidationResult validateMigratedCode(String originalCode, String migratedCode) {
String prompt = """
请对比以下代码迁移前后的版本,验证迁移的正确性:
迁移前(Spring Boot 2.x):
```java
%s
```
迁移后(Spring Boot 3.x):
```java
%s
```
请检查:
1. 功能语义是否等价(最重要)
2. 是否有遗漏的迁移点
3. 是否引入了新的潜在问题
4. 边界情况的处理是否一致
以JSON格式返回:
{
"isSemanticEquivalent": true/false,
"equivalenceAnalysis": "语义等价性分析",
"missedMigrationPoints": ["遗漏点1"],
"potentialIssues": ["潜在问题1"],
"overallAssessment": "SAFE/RISKY/NEEDS_REVIEW"
}
""".formatted(originalCode, migratedCode);
String response = chatClient.prompt()
.user(prompt)
.call()
.content();
return parsValidationResult(response);
}
private String extractRelevantCode(String fullCode, MigrationIssue issue) {
// 根据issue中的代码位置,提取相关的代码片段
// 简化实现:如果originalCode不为空,直接返回
if (issue.getOriginalCode() != null && !issue.getOriginalCode().isEmpty()) {
return issue.getOriginalCode();
}
// 否则返回前100行
String[] lines = fullCode.split("\n");
return String.join("\n", Arrays.copyOf(lines, Math.min(100, lines.length)));
}
}完整迁移工作流
@Service
@Slf4j
public class SpringBootMigrationWorkflow {
private final MigrationAnalysisService analysisService;
private final MigrationExecutionAssistant executionAssistant;
public MigrationReport runFullMigration(Path projectRoot) throws IOException {
MigrationReport report = new MigrationReport();
report.setProjectRoot(projectRoot.toString());
report.setStartTime(LocalDateTime.now());
// 1. 扫描所有Java文件
List<Path> javaFiles = scanJavaFiles(projectRoot);
log.info("发现 {} 个Java文件", javaFiles.size());
// 2. 分析每个文件的迁移问题
List<MigrationAnalysisResult> analysisResults = new ArrayList<>();
for (Path file : javaFiles) {
log.info("分析文件: {}", file.getFileName());
MigrationAnalysisResult result = analysisService.analyzeFile(file);
analysisResults.add(result);
}
// 3. 汇总所有问题,按复杂度分类
Map<MigrationAnalysisResult.MigrationComplexity, List<MigrationIssue>> issuesByComplexity
= groupIssuesByComplexity(analysisResults);
report.setIssuesByComplexity(issuesByComplexity);
// 4. TRIVIAL问题:自动生成补丁
List<AutoPatch> autoPatches = new ArrayList<>();
List<MigrationIssue> trivialIssues = issuesByComplexity.getOrDefault(
MigrationAnalysisResult.MigrationComplexity.TRIVIAL, List.of()
);
log.info("TRIVIAL问题 {} 个,自动生成补丁", trivialIssues.size());
for (MigrationIssue issue : trivialIssues) {
try {
String patch = executionAssistant.generateAutoPatch(issue);
autoPatches.add(new AutoPatch(issue, patch));
} catch (Exception e) {
log.warn("自动补丁生成失败: {}", issue.getIssueId());
}
}
report.setAutoPatches(autoPatches);
// 5. 复杂问题:生成详细迁移指南
List<MigrationGuide> guides = new ArrayList<>();
List<MigrationIssue> complexIssues = issuesByComplexity.getOrDefault(
MigrationAnalysisResult.MigrationComplexity.COMPLEX, List.of()
);
complexIssues.addAll(issuesByComplexity.getOrDefault(
MigrationAnalysisResult.MigrationComplexity.RISKY, List.of()
));
log.info("COMPLEX/RISKY问题 {} 个,生成迁移指南", complexIssues.size());
for (MigrationIssue issue : complexIssues) {
MigrationGuide guide = executionAssistant.generateComplexMigrationGuide(
issue,
readFileContent(issue.getCodeLocation())
);
guides.add(guide);
}
report.setMigrationGuides(guides);
// 6. 生成总体迁移计划
String migrationPlan = generateMigrationPlan(issuesByComplexity);
report.setMigrationPlan(migrationPlan);
report.setEndTime(LocalDateTime.now());
return report;
}
private String generateMigrationPlan(
Map<MigrationAnalysisResult.MigrationComplexity, List<MigrationIssue>> issuesByComplexity) {
int trivialCount = issuesByComplexity.getOrDefault(
MigrationAnalysisResult.MigrationComplexity.TRIVIAL, List.of()).size();
int moderateCount = issuesByComplexity.getOrDefault(
MigrationAnalysisResult.MigrationComplexity.MODERATE, List.of()).size();
int complexCount = issuesByComplexity.getOrDefault(
MigrationAnalysisResult.MigrationComplexity.COMPLEX, List.of()).size();
int riskyCount = issuesByComplexity.getOrDefault(
MigrationAnalysisResult.MigrationComplexity.RISKY, List.of()).size();
return """
# 迁移计划
## 问题统计
- TRIVIAL(可自动修复):%d 个,预计耗时 0.5 天
- MODERATE(需手工处理):%d 个,预计耗时 %d 天
- COMPLEX(需重构):%d 个,预计耗时 %d 天
- RISKY(行为变化,需测试):%d 个,预计耗时 %d 天
## 建议执行顺序
1. 先做包名替换(javax -> jakarta),全局搜索替换,快速完成
2. 处理 TRIVIAL 和 MODERATE 问题,应用自动补丁
3. 处理 Security 相关的 COMPLEX 问题(通常是迁移中最复杂的)
4. 处理其他 COMPLEX 问题
5. RISKY 问题需要完整回归测试,建议放到最后
## 注意事项
- 每次只迁移一个模块,确保可以独立测试
- Security 配置迁移后必须做完整的权限测试
- 关注 Actuator 端点暴露配置,默认行为有变化
""".formatted(
trivialCount,
moderateCount, Math.max(1, moderateCount / 10),
complexCount, Math.max(2, complexCount / 3),
riskyCount, Math.max(3, riskyCount / 2)
);
}
}Security 配置迁移的完整示例
这是最复杂的场景,单独拿出来详细说。
Spring Boot 2 的写法:
// 旧写法(Spring Boot 2.x)
@Configuration
@EnableWebSecurity
public class OldSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}AI 生成的迁移指南会告诉你:
Spring Boot 3 的写法变成了:
// 新写法(Spring Boot 3.x)
@Configuration
@EnableWebSecurity
public class NewSecurityConfig {
@Autowired
private UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
// 注意:authorizeRequests 已废弃,改用 authorizeHttpRequests
// 注意:antMatchers 已废弃,改用 requestMatchers
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
// 注意:不再通过覆盖 configure(AuthenticationManagerBuilder) 来配置
// 而是声明 AuthenticationManager Bean
return config.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 如果需要自定义 UserDetailsService 的认证逻辑:
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}
}AI 会特别提示的行为变化:antMatchers 和 requestMatchers 在处理 MVC 路径时有细微差异,如果你的项目有多个 Servlet,需要特别注意。
与之前迁移文章的区别
之前写过一篇"代码自动化迁移"(article-1406),那篇的重点是:如何用脚本自动化处理大量的简单替换(包名、注解变化、API重命名这类有明确一一对应关系的变化)。
这篇的重点是:没有直接对应关系的复杂迁移。当旧 API 被废弃但没有直接替代品、或者替代品的行为语义不同时,需要 AI 辅助你理解"应该怎么重构"。
两篇结合起来是一套完整的迁移方法:
- 先用脚本处理简单替换(前者的方法)
- 再用 AI 辅助处理复杂场景(本篇的方法)
总结
AI 辅助代码迁移的核心价值不在于它能自动改代码,而在于:
快速识别所有需要关注的地方:一个几十万行的项目,手工排查所有潜在迁移点需要很长时间,AI 能快速扫描分类
对复杂场景提供有深度的分析:Security 配置为什么要从"继承"变成"声明 Bean"?这背后的设计理念是什么?AI 能解释,帮你理解而不只是照抄
生成功能等价性的验证思路:迁移完了怎么验证?AI 能帮你识别需要特别关注的测试场景
AI 是你的迁移助手,但最终的判断和实现还是你来做。对于 RISKY 级别的改动,哪怕 AI 给了方案,也一定要仔细审查和充分测试。
