把存量 Scala 2 代码库迁到 Scala 3,不是「换个编译器」那么简单——宏不兼容、隐式改 given、枚举是新关键字、依赖要升级。本文给出可执行的迁移路线:先评估风险与收益,再搭交叉编译基线,用 scalafix 自动迁移,最后渐进式逐模块迁移、同步现代化、回归验证。重点讲清「哪些会自动、哪些要手动、哪些是坑」。
前置:/scala3-modern-features/(Scala 3 新特性)、/scala-metaprogramming/(宏的迁移)、/scala-build-tooling/(sbt/Mill 构建)、/scala-typelevel-programming/(类型级编程差异)。
目录
- 1. 迁移的收益与成本评估
- 2. 兼容性分析:宏、隐式、反射与跨版本依赖
- 3. 交叉编译:一个代码库两个编译器
- 4. 迁移工具链:scalafix 与自动迁移
- 5. 渐进式迁移:逐模块、双编译器并行
- 6. 语法现代化:缩进、枚举与给定实例
- 7. 依赖升级与插件适配
- 8. 宏与元编程的迁移
- 9. 回归测试与性能验证
- 10. 速查表与一句话记忆
- 延伸阅读
1. 迁移的收益与成本评估
迁移前先想清楚「为什么迁」,避免为迁而迁:
迁移收益:
□ 长期维护:Scala 2 进入维护模式,新特性只在 Scala 3
□ 编译器改进:更快的编译、更好的错误信息
□ 现代化语法:缩进语法、enum、given/using 更简洁
□ 生态重心:主流库逐渐要求 Scala 3
迁移成本:
□ 宏/元编程:Scala 2 宏(黑盒/白盒)与 Scala 3 宏 API 不兼容
□ 隐式 → given:大量隐式参数/隐式转换要迁移
□ 依赖:第三方库需要 Scala 3 版本(部分滞后)
□ 反射/工具链:某些框架依赖 Scala 2 编译器细节
评估信号:
□ 代码库大小、宏数量、依赖的 Scala 3 支持度
□ 团队对 Scala 3 的熟悉度
□ 收益集中在「长期」,成本集中在「前期」
工程要点:迁移决策是**「收益长期、成本前期」**的权衡。评估清单:宏数量(最大变量)、第三方依赖的 Scala 3 支持、团队准备度。收益集中在维护性与未来生态,成本集中在前期一次性改造。
2. 兼容性分析:宏、隐式、反射与跨版本依赖
迁移前做「兼容性扫描」,识别必须手动改的部分:
四大风险区:
□ 宏(Macro):Scala 2 宏(def macro)→ Scala 3 完全不同
□ 隐式(Implicit):implicit 参数/隐式转换 → given/using
□ 反射(Reflection):scala.reflect API → Scala 3 减少依赖
□ 依赖:Java/Scala 2 库 vs Scala 3 交叉版本
自动迁移能覆盖:
□ 语法变化(多数)
□ 标准库小改动
□ 隐式的大多数(scalafix 迁移规则)
手动迁移必须:
□ 宏实现(重写)
□ 反射代码(重新设计或改 scala 3 方式)
□ 特殊类型级技巧(TypeTag/Manifest → TypeTest 等)
扫描工具:
□ 用 Scala 3 编译器跑「迁移模式」(-source:3.0-migration)
□ scalafix 迁移规则报告无法自动处理的点
□ 静态扫描隐式/宏/反射的分布
工程要点:兼容性分析的产出是**「自动能改 vs 必须手动的清单」**——语法和多数隐式可自动,宏和反射必须手动。先扫描出「手动工作量」,再决定「要不要迁、迁的节奏」,而不是直接开迁。
3. 交叉编译:一个代码库两个编译器
让代码同时兼容 Scala 2.13 与 Scala 3,是渐进迁移的桥头堡:
交叉编译(Cross Compilation):
□ 一套源码,发布 Scala 2.13 与 Scala 3 两个版本
□ 用「差异层」处理语法/库差异:
source.binary 或 %% 版本矩阵
□ sbt:crossScalaVersions := Seq("2.13.x", "3.x.y")
□ Mill:支持 cross 配置
差异处理技巧:
□ 公共代码:写两版都兼容的子集
□ 差异代码:分开在 scala-2.13/ 与 scala-3/ 目录(sbt srcMain)
□ 库依赖:%%% 自动选对应 Scala 版本
收益:
□ 不必「一刀切」迁:库/模块逐步切到 Scala 3
□ 新代码可直接用 Scala 3,旧代码保持 2.13
□ 生态兼容:给使用方的依赖保持双版本
// build.sbt 交叉编译
crossScalaVersions := Seq("2.13.12", "3.3.1")
scalaVersion := "3.3.1"
libraryDependencies += "org.typelevel" %% "cats-effect" % "3.5.4"
// %%% 自动选择对应 Scala 版本
工程要点:交叉编译让迁移**「可并行」**——一套代码两个版本,模块逐步切换。源码目录分层处理差异(公共子集 + 双版本目录),依赖用 %%% 自动匹配。这是「不用停业重建」的渐进迁移地基。
4. 迁移工具链:scalafix 与自动迁移
Scala 官方迁移工具链以 scalafix 为核心:
迁移工具链:
□ scalafix:语义化代码改写工具(规则系统)
□ Scala 3 迁移规则:scala-3-migration / ExplicitResultTypes
□ sbt 插件:scalafix + scalafmt 协同
□ 官方迁移指南:scala 3-migration-guide
自动迁移能做的:
□ 隐式 → given/using(多数)
□ 语法调整:auto-application、后缀操作符、下划线用法
□ 类型推断补全(ExplicitResultTypes 规则)
□ 关键字冲突:enum/extension/given 作为标识符加反引号
迁移流程:
1. 编译基线:先让 Scala 2.13 全绿(迁移前的测试基线)
2. 跑 scalafix 迁移规则 → 生成 diff
3. review diff(自动改的也要人审)
4. 切到 Scala 3 编译器跑迁移模式 → 修残余
# sbt 里跑 scalafix 迁移规则
sbt "scalafixAll dependency:scala3-migration"
sbt "scalafixAll ExplicitResultTypes"
工程要点:scalafix 迁移是**「自动为主、人审兜底」**——跑规则生成 diff,但 diff 必须 review(自动改写也有语义边界)。配合「迁移模式编译器 + ExplicitResultTypes」处理残余,把迁移从「全手写」变成「自动 + 审查」。
5. 渐进式迁移:逐模块、双编译器并行
不要「一把梭」,用逐模块渐进迁移控制风险:
渐进式迁移策略:
1. 先迁「叶子模块」:依赖少、无宏、无复杂隐式
→ 风险最小,建立信心
2. 后迁「根模块」:被大量依赖(最后切,等依赖都就绪)
3. 中间模块:依赖被迁移的叶子,逐个跟上
双编译器并行:
□ 库/模块层:交叉编译双版本(见第 3 节)
□ 应用层:切 Scala 3 编译,2.13 分支保留回滚
每步门禁:
□ 该模块测试全绿
□ 依赖它的模块不受影响
□ 性能/行为无明显回归
迁移顺序示例:
core(无宏)→ domain → repository → service → api
最后迁「含宏的模块」与「被全部依赖的根」
工程要点:渐进式迁移的准则是**「先叶子后根」**——从依赖最少的模块开始,双编译器并行兜底,每步过「测试全绿 + 依赖无伤」的门禁。迁移不是「切换日」,是「逐步替换」。
6. 语法现代化:缩进、枚举与给定实例
迁移过程中顺手做语法现代化(可选,但要权衡):
Scala 3 现代化语法:
□ 缩进语法:去掉大括号(可读性,团队要适应)
□ enum:替代 sealed trait + case object(更简洁)
□ given/using:替代 implicit(更显式)
□ 可选参数的类型标注、通配符 _
现代化 vs 迁移分开:
□ 迁移 = 语义兼容的自动改写(先做,无风险)
□ 现代化 = 风格重构(后做,有行为风险)
现代化建议:
□ 新代码直接用 Scala 3 风格
□ 旧代码迁移后「不强制现代化」(保持可用,降低风险)
□ enum/given 重构走独立 PR + 测试
enum 示例(迁移后的现代化可选项):
enum Direction:
case North, South, East, West
// vs Scala 2:sealed trait + case object(仍可用)
工程要点:「迁移」与「现代化」要分开——迁移是语义兼容的自动改写,现代化是风格重构。先迁移(保证可用),现代化在迁移后「新代码新风格、旧代码可缓」。别把两个高风险动作绑在一起。
7. 依赖升级与插件适配
第三方依赖是迁移的硬约束,提前确认支持度:
依赖检查:
□ 库是否有 Scala 3 版本(%sbt 查询 / 官方支持矩阵)
□ 大库基本已支持(Cats/ZIO/Akka 等),小众库可能滞后
□ Java 库:Java 依赖通常直接可用(Scala 3 兼容 JVM 字节码)
编译器插件:
□ Kind Projector:Scala 2 类型 lambda 插件 → Scala 3 用原生
□ better-monadic-for:Scala 2 → Scala 3 不再需要(for 已优化)
□ 其他插件:需找 Scala 3 版本或找替代
处理滞后依赖:
□ 把「依赖滞后」的模块留到交叉编译阶段(2.13 继续)
□ 用 Java interop / 本地适配桥接
□ 或等待/参与库的迁移
工程要点:依赖适配的准则是**「先查支持度,滞后的模块缓迁」**——大库大多已支持 Scala 3,小众库可能滞后,那就让该模块留在交叉编译的 2.13 侧。编译器插件(Kind Projector 等)大多被 Scala 3 原生语法取代,检查并清理。
8. 宏与元编程的迁移
宏是迁移里最硬的一块,Scala 2 与 Scala 3 的宏 API 完全不同:
Scala 2 宏(def macro):黑盒/白盒
→ 基于 scala.reflect.macros(编译器内部 API)
Scala 3 宏:
→ 基于 quotes.reflect(显式 Quotes 上下文)
→ inline 与宏分工:inline 做常量折叠,宏做复杂生成
迁移路径:
□ 简单「编译期生成」→ 用 inline + 内联表达式
□ 类型级/元编程 → 用 Match Types(很多 Scala 2 宏可替代)
□ 复杂代码生成宏 → 用 quotes.reflect 重写
依赖宏的库:
□ 用宏的库(如一些 DSL/derive 库)→ 需等其 Scala 3 版本
□ 自己的宏 → 属「必须手动」的重写项
Migration 模式:
□ 编译器 -source:3.0-migration 能给出宏相关的迁移提示
□ 先让「无宏模块」迁移,宏模块单独攻关
// Scala 3 简单宏形态(示意)
import scala.quotes.*
inline def debug(inline expr: Any): String =
${ debugImpl('expr) }
def debugImpl(expr: Expr[Any])(using Quotes): Expr[String] =
'{ ${ Expr(expr.show) } }
工程要点:宏迁移的准则是**「能 inline/Match Types 替代就先替代,复杂的重写 quotes.reflect」**——许多 Scala 2 宏的用途(常量折叠、类型级计算)在 Scala 3 有更干净的方案。宏模块是「手动清单」里的重头,放迁移后期单独攻关。
9. 回归测试与性能验证
迁移必须证明「行为没变」,回归验证是门禁:
回归测试:
□ 迁移前建立测试基线(2.13 全绿)
□ 迁移后同一套测试跑 Scala 3
□ 重点:隐式解析差异、枚举/ADT 行为、边界类型
性能验证:
□ 编译时间对比(Scala 3 编译更快,但首次会慢)
□ 运行时性能:核心路径 benchmark(不应有回归)
□ 启动时间/内存:对比迁移前后
兼容回归:
□ 交叉编译的 2.13 版本仍可用(库的消费者)
□ 对外 API 行为一致(字节码/Scala 2 消费者)
发布策略:
□ 库:发布双版本(2.13 + 3),消费者各自验证
□ 应用:灰度发布,观察线上指标
工程要点:迁移的门禁是**「同一套测试双编译 + 性能对比」**——迁移前全绿是基线,迁移后测试与 benchmark 无回归才算过。库发布双版本,应用灰度验证,用「可回滚」保证迁移过程的安全。
10. 速查表与一句话记忆
| 问题 | 一句话答案 |
|---|---|
| 要不要迁 | 收益长期、成本前期,评估宏与依赖 |
| 最大风险区 | 宏、隐式、反射、跨版本依赖 |
| 怎么搭桥 | 交叉编译(一套代码双版本) |
| 自动迁移 | scalafix 规则 + 迁移模式编译器 |
| 怎么渐进 | 先叶子后根,逐模块双编译器并行 |
| 现代化何时做 | 迁移后,新代码新风格 |
| 依赖怎么办 | 查支持度,滞后的留 2.13 侧 |
| 宏怎么迁 | inline/Match Types 替代 + quotes.reflect 重写 |
| 怎么验证 | 同一套测试 + 性能对比 + 可回滚 |
一句话记忆:Scala 2→3 迁移 = 评估清单(宏/依赖是最大变量)+ 交叉编译桥(一套代码双版本)+ scalafix 自动迁移(人审兜底)+ 渐进式先叶子后根(双编译器并行)+ 宏单独攻关(inline/Match Types 替代)+ 同套测试回归(性能对比 + 可回滚)——把「换编译器」变成「逐步替换的安全工程」。
延伸阅读
- /scala3-modern-features/ — Scala 3 新特性详解
- /scala-metaprogramming/ — Scala 3 元编程与宏
- /scala-typelevel-programming/ — 类型级编程的差异
- /scala-build-tooling/ — sbt/Mill 交叉编译配置
- /scala-testing-practice/ — 迁移回归测试
- Java 企业级专题 — JVM 生态与迁移
- Go 语言专题 — 语言版本迁移对照
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。