资讯动态

Scala 2.13到3.x迁移实战:构建配置、依赖与代码改造全指南

发布时间:2026/10/15 9:04:12 来源:尧图企业网站定制
简介Scala-升级版.docx 是一份面向 Scala 入门者及 Spark 开发者的升级版学习笔记立足于 Spark 1.6 依赖 Scala 2.10、Spark 2.0 以后升级到 Scala 2.11 的实际背景系统讲解多范式特性、类型推测、高阶函数、模式匹配、特质与 Java 互操作等六大特征并强调学习 Scala 对理解和开发 Spark 应用的重要性。文档从零梳理安装与开发环境搭建覆盖 Windows/Linux 下 JDK 1.8、Scala 2.11.8 的安装与环境变量配置其中 Windows 下可通过 msi 安装并配置 SCALA_HOMELinux 下直接解压二进制包即可同时说明 Eclipse、Scala IDE、IntelliJ IDEA 中 Scala 插件的安装和 SDK 设置方便读者快速跑通开发环境、避免常见环境问题。语法部分包含基本数据类型、var/val 变量与常量、if 表达式、类与单例对象、伴生类与伴生对象、apply 方法等常用知识点配有 Person、Lesson_Class 等可运行示例适合边读边动手练习。资源为单个 docx 文档压缩包约 2.04MB已有 2060 人学习下载可作为快速入门 Scala、理解 Spark 开发基础的简明参考资料。1. 先别急着打开 docxScala-升级版 到底在升什么拿到一份命名为 Scala-升级版.docx 的交付文档先别急着双击打开多数情况下它不是一个学习笔记而是一份已经拍板要执行的升级方案。所谓升级版不是说某个框架发布了新版而是项目里的 Scala 主版本要往前挪常见的路径是从 2.13 迁到 3.x。这个动作背后牵扯三件事——构建配置怎么改、依赖闭包怎么收敛、存量代码怎么批量迁移。文档只是把结论写下来真正干活的人要自己把每一步跑通。这篇文章按我处理同类任务的顺序展开先从版本和依赖里把现状捞出来再搭一个可复现的构建然后处理代码差异最后把最容易翻车的地方和验证方式讲透。适合正在接手 Scala 跨版本迁移的开发者也适合准备给团队排期的技术负责人。2. 从 Scala-升级版 提取交付边界先确认起点再谈终点这类升级文档最常见的通病是只写目标版本不写现状版本如果不先对齐起点后面所有依赖分析都是空转。我自己接手的第一个动作不是读文档而是把项目根目录的构建信息捞出来。先看编译器和 sbt 版本再看子模块之间的差异最后才拿文档里的“改动清单”去对号入座。原因是 Scala 的跨主版本升级不像换一个接口那样轻松编译器和标准库的版本一旦错位报错信息会把你引向完全错误的方向。2.1 版本起点不是看文档封面而是看编译器与构建文件先跑一组命令确认现状。下面这段在项目根目录执行前两个命令只做粗判断第三个命令才是真正生效的编译版本。# 看看当前命令行默认的 scala 编译器版本 scalac -version # 检查 sbt 版本太旧的话连 Scala 3 的构建都加载不了 cat project/build.properties # 查看项目实际的 Scala 编译版本多模块仓库要看每一个子项目 sbt show scalaVersion这里有个容易踩的细节scalac -version显示的是 PATH 上的编译器不一定等于项目里 sbt 正在用的编译器。比如你 shell 里装的是 2.13.10但项目里的project/build.properties已经把 sbt 指到了另一个目录最后编译用的版本以sbt show scalaVersion为准。project/build.properties里的sbt.version也不能只看格式它决定了 sbt 能不能加载依赖解析相关的插件老版本 sbt 在 Scala 3 的构建模式下会出一些很奇怪的错误报错信息往往和真正的代码问题无关。确认版本之后还要把目标版本定住。我的习惯是优先选 LTS 线比如 Scala 3.3.x而不是追最新的大版本。对存量项目来说“别人用了两年”比“多了几个新特性”重要得多。迁移期最怕的是目标版本本身还在剧烈演进今天调通的参数过两周又变了。文档封面如果只写“升级版”三个字那你第一步要做的就是把这句话翻译成具体的版本号组合。检查点命令用途默认编译器scalac -version只看 PATH 上的编译器sbt 版本cat project/build.properties判断 sbt 是否支持 Scala 3实际编译版本sbt show scalaVersion项目真正用的版本交叉编译范围sbt show crossScalaVersions判断是否有双版本编译配置crossScalaVersions是最容易被忽略的一行。如果 build.sbt 里已经写了Seq(2.13.10, 3.3.4)说明之前的维护者已经在做双版本兼容升级压力会小很多如果只有2.13.10那就老老实实当它从零开始。文档里如果写了“保持向下兼容”建议先在本地跑一遍sbt 2.13.10 compile和sbt 3.3.4 compile看看两个版本能不能同时通过。这一步能帮你判断后续是“顺水推舟”还是“推倒重来”。2.2 从 docx 里的改动项映射到三张清单方案文档里一般会有一堆背景描述但真正能指导落地的是改动项。我会把 docx 里的每条改动需求先映射到三张清单上不然后面改代码时会被无关信息干扰。第一张是构建清单包含 sbt 版本、scalaVersion、crossScalaVersions、scalacOptions 以及打包方式第二张是依赖清单列出所有%%坐标、scope 和发布状态第三张是代码清单重点标记隐式转换、宏、反射、序列化和编译器插件。这三张清单不是写给文档看的是写给接下来的编译日志看的。# 把全量编译的错误和警告落盘待办清单就从这里生成 sbt clean compile compile.log 21 grep -nE \[error\]|\[warn\] compile.log | head -200这里有一个常见的误区拿compile.log去数错误条数然后认为错误少就好做。实际上一处上游 API 变化可能引发下游几十个类型错误正确的做法是按“源文件”聚合而不是按“错误行”聚合。先看哪些文件报错最多再从依赖关系最底层的模块开始改错误会成片消失而不是越改越多。依赖清单的核查动作用sbt evicted最直接它会列出所有被降级或冲突的依赖其中跟 scala-library 相关的冲突优先级最高。跨主版本升级时一个库如果只有_2.13构件而没有_3构件sbt 不一定报错它可能选择另一个版本默默顶上但运行时会冒出奇奇怪怪的 ClassNotFound。所以sbt evicted之后凡是无法确认来源的冲突项都要去依赖坐标里逐条看。这时候不要轻信文档里写的“该库已支持 Scala 3”因为“支持”在 sbt 的坐标体系里意味着存在_3或经过交叉编译验证的_2.13构件这两者的行为差异只有运行测试才能暴露。# 冲突列表和完整 classpath是评估升级工作量的第二张底牌 sbt evicted sbt show fullClasspathshow fullClasspath输出的内容很长建议输出到文件里再按scala-library、scala3-library等关键字过滤。升级边界不需要一开始就定义到每个类但至少要能回答一个问题项目里到底有没有依赖闭包外的“裸 class 文件”。如果有那升级文档里必须先给这类资源单独安排兼容方案而不是直接假设编译器能解决。提示本地的编译错误会随着迁移进度变化建议每次改动前重新生成 compile.log不要拿昨天的错误清单指导今天的修改。3. 搭可复现的升级构建build.sbt 与 scalac 参数迁移构建配置是整个升级的骨架如果骨架是歪的代码改得再干净也会在 CI 上翻车。我在迁移第一天就会把 build.sbt 拆成“迁移期配置”和“目标配置”两部分而不是一次性切成 Scala 3 的干净形态。这样做的原因是迁移期间要频繁在两个版本之间切换配置的可逆性比美观重要。3.1 用最小 build.sbt 先跑通“新编译器读旧代码”迁移期的 build.sbt 不需要一次写全所有参数反而应该尽量精简。保留最基础的编译信息、依赖坐标以及关键 scalac 参数就够了。下面这份是我常用的骨架只多不少。// build.sbt —— 迁移期骨架 ThisBuild / scalaVersion : 3.3.4 ThisBuild / organization : org.example ThisBuild / version : 1.0.0 ThisBuild / scalacOptions Seq( -deprecation, -feature, -unchecked, -source:3.0-migration ) libraryDependencies Seq( org.example %% common-utils % 1.4.2, org.example %% data-model % 2.1.0 )%%在这里非常关键。它表示“按当前 Scala 二进制版本选择构件”在 Scala 2.13 下解析common-utils_2.13在 Scala 3 下解析common-utils_3。如果某个依赖只有_2.13构件又没有源码%%会让解析直接失败此时要么换一个维护中的库要么改用%显式指定旧构件但后者运行时风险很高只能在迁移期临时过渡。%写法会让你在 Scala 3 的 classpath 里混入 2.13 编译的字节码短时间能跑通长期维护时它会变成一颗定时炸弹。-source:3.0-migration是我在迁移期必开的一个参数。它让编译器在遇到旧语法时给出更明确的迁移提示而不是只丢一句“expected ... but found ...”。它的作用是降低排查成本不是自动改代码。真正能自动改的是迁移插件但前提是依赖能被新编译器顺利解析所以在配置阶段先不要引入太多重量级插件把主干跑通再谈自动化。project/build.properties里的 sbt 版本和 scalaVersion 是两套东西不要混为一谈。前者决定 sbt 本身后者决定编译器。我在同样情况下会优先使用 1.9.x 的 sbt因为这个区间的版本对 Scala 3 的交互命令支持比较完整而且很多旧插件在改名时出现过兼容断层。# project/build.properties sbt.version1.9.9配置写完先跑一次sbt show scalaVersion确认新编译器真的生效。这一步看似多余但能避免一种非常隐蔽的情况某个子模块的 .sbt 文件里有一行旧的scalaVersion覆盖了根目录配置导致你以为在用 Scala 3实际还在 2.13 下面编译。多模块项目尤其容易出现这种问题因为根目录的ThisBuild / scalaVersion不会自动覆盖子项目里显式写的scalaVersion。遇到这种情况优先把子模块里的显式 scalaVersion 改成继承根配置而不是在迁移期间维护多套编译器版本。3.2 scalac 参数翻译对照表别让旧参数把错误掩盖很多从 2.13 直接搬过来的 scalac 参数在 Scala 3 里要么失效要么改名。最典型的是未使用 import 的告警参数写法从-Ywarn-unused:imports变成了-Wunused:imports。如果你把旧的-Ywarn-unused原样贴过去编译器不会报错但它根本不生效等你后期打开 fatal warnings 时才会发现大量垃圾代码早就被当成了合法代码。目标Scala 2.13 写法Scala 3 写法说明废弃 API 警告-deprecation-deprecation两个版本通用语言特性警告-feature-feature两个版本通用未使用 import-Ywarn-unused:imports-Wunused:imports参数名变更未使用参数-Ywarn-unused:params-Wunused:params参数名变更致命警告-Xfatal-warnings-Xfatal-warnings两个版本通用迁移提示无-source:3.0-migrationScala 3 新增这张表不要求一次全部打开。我的习惯是先只开-deprecation -feature -unchecked和-source:3.0-migration把编译错误收敛干净后再开-Wunused。-Xfatal-warnings在迁移期一律不开否则一个未使用 import 就能把整个构建卡死你会分不清它到底是错误还是警告。等 CI 连续跑一周后再把它加进去做守门员那时才是它真正发挥作用的时间点。验证 scalacOptions 是否生效不要靠记忆用命令直接看。sbt 里可以单独查某个子项目的最终参数如下所示。# 查看某个模块最终生效的编译参数判断是否被旧配置覆盖 sbt show core / scalacOptions如果core子项目里自己写了一组旧的 scalacOptions根目录的ThisBuild / scalacOptions不会覆盖它而是两者按顺序拼接。最常见的翻车是子项目里留着-Ywarn-unused:imports根目录加了-Wunused:imports最后查出来的参数里既有旧写法又有新写法警告却没被真正控制住。排查这种问题show core / scalacOptions比人肉读 build.sbt 可靠得多因为你永远不知道某个不起眼的 .sbt 文件里藏着什么覆盖。依赖参数同样值得提前对齐。Scala 3 项目里org.scala-lang:scala-library实际走的是 2.13 基线不要再想着用scalaVersion去统一所有 scala-library这个前提不理解后面遇到sbt evicted时会花很多冤枉时间。判断一个依赖是否真的兼容 Scala 3最靠谱的方式是在目标版本下编译并运行测试而不是看它的坐标里有没有_3后缀因为带_3后缀的构件也可能只提供源码级别的兼容运行时行为仍然分叉。4. 代码迁移三件套清单、自动改写、手工兜底构建跑通之后真正的体力活才刚开始。代码迁移不能靠“全局搜索替换”也不能靠人肉盯着屏幕一整天。我用的是一个固定流程先生成错误清单再让自动化工具处理模式化改写最后手工处理那些工具改不动的边界。这个流程听起来普通但能保证每走一步都有可回滚的检查点。4.1 用编译日志生成待改清单而不是人肉翻源码我见过有同学直接在 IDE 里打开所有带红线的文件从上往下一个个改。这种做法的问题不在于改得慢而在于改的过程中新错误会被不断触发等改到一半时已经搞不清哪些是存量问题、哪些是这次改出来的问题。# 全量编一次把错误和警告分别落盘 sbt clean compile compile.log 21 # 按错误和警告分流方便后续按文件聚合 grep -E \[error\] compile.log | sort -u errors.txt grep -E \[warn\] compile.log | sort -u warnings.txt # 统计数量决定下一步的优先级 wc -l errors.txt warnings.txtsort -u会把相邻重复的错误合并但 Scala 编译器经常在同一文件上打印多条不同位置但同根源的错误。所以拿到errors.txt之后我还会按文件路径聚合从报错数量最多的文件开始处理。下面的命令把错误日志里的路径提取出来按出现次数排个序。# 按源文件聚合成“改动成本排行” grep -E \[error\] compile.log \ | sed -E s/^\[error\] ?// \ | sed -E s/:[0-9]:[0-9]:.*// \ | sort | uniq -c | sort -rn | head -30这个聚合逻辑不需要很精致能看出“哪些文件是重灾区”就够了。迁移期最常见的心理陷阱是把小 errors 先修完留下难啃的大文件但真正值得先动的是被最多代码依赖的底层模块。底层文件一旦迁移通过它子树上的错误会大幅减少顶层文件反而简单。拿到这份排行后把它和依赖方向对齐先改被依赖方再改调用方这是所有跨版本迁移都要遵守的顺序。迁移插件的价值在这个阶段才会体现出来。常见做法是在 build.sbt 里临时引入一个用于 Scala 3 迁移的插件然后让它对指定模块生成改写补丁。插件能做的是把大部分implicit改写成given/using、把旧式 extension 方法改成 Scala 3 的extension语法。注意插件生成的是“建议补丁”不是“正确结果”每个补丁都要过一遍编译和单测。补丁不能全盘接受尤其是涉及隐式优先级和反射的地方机器只能改语法改不了语义。4.2 隐式、扩展方法、宏改动量最大的三类差异Scala 3 最明显的语法变化集中在隐式体系和扩展机制上。这两块能自动改写大部分但自动改写之后经常出现语义不完全等价的情况所以要知道每一处改动背后的原因而不是机械地跟着补丁走。先看隐式参数。2.13 里常见的模式是implicit val加上implicit参数Scala 3 用given和using表达同样的意图。下面的对照是迁移中最常见的一处。// Scala 2.13 写法 implicit val defaultCtx: Ctx Ctx(local) def connect(name: String)(implicit ctx: Ctx): Unit ??? // Scala 3 写法 given defaultCtx: Ctx Ctx(local) def connect(name: String)(using ctx: Ctx): Unit ???这里有一点值得留意调用点通常不用改因为 using 参数和 implicit 参数一样都是自动补参但作用域的搜索规则并不完全相同。Scala 3 对 given 实例的查找更强调当前作用域和类型存在多个候选项时容易从“隐式找到了唯一一个”变成“ambiguous”。迁移后如果冒出大量 ambiguous不要急着到处加类型标注先看看是不是有两个库都发布了 Scala 3 版本导致同一类型的 given 出现了两次。扩展方法也是高频差异。2.13 里用implicit class做扩展Scala 3 推荐直接用extension。改写本身很简单但要注意扩展方法的作用域从“隐式导入”变成了“显式导入或定义在同一个包”。如果原来依赖隐式作用域到处可用改成 extension 后会有一批编译错误那是正常的把 import 补上就好。// Scala 2.13 的隐式类 implicit class RichString(val s: String) { def isBlank2: Boolean s.trim.isEmpty } // Scala 3 的扩展方法 extension (s: String) def isBlank2: Boolean s.trim.isEmpty宏是三类差异里最麻烦的。2.13 的黑匣子式宏在 Scala 3 里基本不可用编译器插件也一样。如果项目里有自己维护的宏迁移成本不是几小时能估完的因为 Scala 3 的宏基于 Quotes 和 ExprAPI 和 2.13 完全不同。如果宏来自第三方库先别急着找替代品去确认它有没有 Scala 3 版本没有的话我一般会先把宏调用改成普通方法数据结果可能略有不同但至少不会挡升级。宏依赖的评估应该在排期阶段就完成而不是等到改代码时才发现整个模块的编译期能力被连根拔起。模式匹配和枚举的变化也会影响一些边界。Scala 3 的enum比 2.13 里的sealed trait case object更简洁但对于已经在线上序列化过的数据不要因为“好看”就把 ADT 结构彻底换掉否则历史数据反序列化会直接翻车。这一点在下一章专门展开。5. Scala 升级避坑编译、依赖、运行时的 5 个高频问题这一章写给正在迁移路上挣扎的人。下面 5 个问题是同类升级里最高频的每个都按现象、原因、解决三步写方便直接对照。5.1 现象sbt evicted 一片红运行时报 NoClassDefFoundError明明编译通过了sbt run却抛NoClassDefFoundError类名指向scala.collection或scala.xml之类的内部结构。原因多半不是代码问题而是 classpath 里有多个 scala-library 来源最典型的场景是把一个只有_2.13的旧构件用%硬塞了进来运行时 sbt 选择的 Scala 3 版本和编译时不一致。解决方法是回退到%%并让依赖尽量跟随编译器基线项目里真正需要单独锁版本的库应该是业务库而不是 scala-library。5.2 现象implicit 改完 given/using 后调用点却报 ambiguous迁移插件会把implicit val改成given把implicit参数改成using但作用域搜索规则没有自动对齐。表现是同一个方法在迁移前能找到唯一隐式迁移后报 ambiguous。常见原因有两个一是两个库都定义了相同类型的 given二是旧代码里把隐式定义在伴生对象中Scala 3 的搜索顺序和 2.13 不同。先打开-source:3.0-migration看提示定位再把不必要的 given 移到更局部作用域通常能解决。如果实在找不到来源用sbt show core / fullClasspath把参与编译的 jar 列表拉出来对比两个 jar 里是不是都带同一个类型定义。5.3 现象sealed trait 序列化结构变了线上数据读不回来这个坑最容易在切换enum时出现。2.13 里把case object序列化成 JSON字段名或类型标记依赖类名改成 Scala 3enum后编译器生成的类名和方法签名都变了历史数据按旧字段找不到对应值。原因不是 Scala 3 的 enum 有问题而是序列化结构升级了。解决方法是迁移期不要改 ADT 的物理结构保留原来的sealed trait case object等线上数据全部能正常读回之后再讨论是否切到 enum。数据读不回来是没有后悔药的所以先做一条“旧数据反序列化”的单测再动手。5.4 现象第三方宏库或编译器插件直接报 incompatible现象很直白升级到 Scala 3 之后某个依赖里带macro字样的类在编译期抛出 “no macro paradise” 或 “unsupported flavor”。原因是 Scala 2 的宏体系已经被 Scala 3 的 Quotes/Expr 取代旧宏不可能直接兼容。解决路径有三条找维护者发布的 Scala 3 版本把宏替换成普通函数调用实在绕不开就暂时保留一个 2.13 独立模块只让跨模块边界传普通对象。第三招是过渡方案不能当长期架构否则升级完成后你还是要回来还这笔技术债。5.5 现象本地编译通过CI 里却因为 warning 变成 error本地明明没报错CI 一到-Xfatal-warnings就挂。原因是迁移期有人在子模块里留了旧 scalacOptions把-Ywarn-unused:imports和-Xfatal-warnings同时打开了而未使用 import 在 Scala 3 下被当成了 error。解决方法是先查show scalacOptions统一成新参数再把-Xfatal-warnings推迟到迁移完成后打开。这个坑看起来小但非常消耗信心因为它会让每次提交都红。6. 固化验证用双版本编译和模块标记文件把升级变成日常动作升级文档写完不算完真正的交付物是一套能反复跑的验证流程。我的做法是在 CI 里同时保留 2.13 和 3.3 两条编译线让每次提交都自动回答一个问题新代码有没有把旧兼容性破坏掉。这个做法成本不高但对迁移期特别有效。# CI 双版本编译2.13 保证兼容3.3 验证迁移进度 steps: - name: Scala 2.13 compatibility run: sbt 2.13.10 compile - name: Scala 3.3 migration run: sbt 3.3.4 compile会让 sbt 临时切换 scalaVersion先编 2.13 再编 3.3这样你不用手动改 build.sbt 就能完成双版本验证。这条配置在迁移合入主分支后也能长期保留只要两个版本都绿后续迭代就不会悄悄把代码推回旧语法。本地做分块迁移时我会用模块标记文件控制顺序。下面这段脚本按依赖顺序编译模块编译通过的模块留下标记之后的失败只看当前模块不会把上一模块的几百个错误重新翻出来。#!/usr/bin/env bash set -euo pipefail # 依赖顺序被依赖最多的基础模块先切 for module in core service api; do markermodules/$module/.scala3-ok if [ -f $marker ]; then echo skip $module continue fi echo compile $module under Scala 3 sbt project $module clean compile touch $marker done这套标记脚本只适合本地排查不适合当作 CI 的正式状态。因为 CI 每次从干净目录跑标记不存在所有模块都会编译这反而是好事。我自己的习惯是本地用标记文件快速定位CI 用双版本编译守门两者配合而不是互相替代。迁移快结束的时候有一件容易被忽略的事把临时写的dependencyOverrides、旧版scalacOptions和过渡用的%依赖全部清理掉。升级的价值不在于“代码能在 Scala 3 下编过”而在于团队以后不用再记两套写法。我上次接类似任务最大错误是想一口吃成胖子把全部模块一次性切过去日志几千行根本没法定点。后来改成按依赖顺序一个模块一个模块切再配合双编译和标记文件反而几天就清完了。从那以后我养成了一个习惯任何 Scala 升级先保证两条路能同时跑通再谈清理。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑