You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多项目SBT环境下无法同时运行两个Scala Play应用的问题求助

SBT多项目依赖类路径冲突问题解决

问题原因

SBT的.dependsOn()会将依赖项目的编译产物和依赖包加入当前项目的类路径。当两个依赖项目存在同名类/同包路径类(比如你代码中两个项目都有BuildInfo或自定义的ApplicationLoader)时,JVM会按照类路径的顺序加载第一个找到的类——而SBT中.dependsOn()的声明顺序直接决定了类路径的优先级,先声明的依赖会排在类路径前面,因此总是加载第一个项目的类。

解决方法

方案一:为每个服务使用独立类加载器

让两个Play服务在各自独立的类加载环境中运行,避免类路径共享导致的冲突。示例代码如下:

import java.net.URLClassLoader
import java.io.File
import play.api.{Application, ApplicationLoader, Environment, ApplicationLoader}

// 构建指定项目的类路径(编译类文件+依赖包)
def getProjectClasspath(projectDir: String): Array[URL] = {
  val scalaVersion = scala.util.Properties.versionNumberString
  val classesDir = new File(s"$projectDir/target/scala-$scalaVersion/classes").toURI.toURL
  val testClassesDir = new File(s"$projectDir/target/scala-$scalaVersion/test-classes").toURI.toURL
  
  // 加载项目依赖的jar包(可根据实际情况调整依赖目录)
  val libDir = new File(s"$projectDir/lib")
  val jars = if (libDir.exists()) libDir.listFiles().filter(_.getName.endsWith(".jar")).map(_.toURI.toURL) else Array.empty[URL]
  
  Array(classesDir, testClassesDir) ++ jars
}

// 为theSlab项目创建独立类加载器
val theSlabClasspath = getProjectClasspath("../the-slab")
val theSlabClassLoader = new URLClassLoader(theSlabClasspath, ClassLoader.getSystemClassLoader.getParent)

// 使用独立类加载器启动theSlab服务
theSlabServer = {
  val env = Environment.simple(path = new File("../the-slab"), classLoader = theSlabClassLoader)
  val context = ApplicationLoader.Context.create(env)
  
  // 通过类加载器实例化专属的ApplicationLoader
  val loaderClass = theSlabClassLoader.loadClass("configuration.TheSlabApplicationLoader")
  val slabLoader = loaderClass.getDeclaredConstructor().newInstance().asInstanceOf[ApplicationLoader]
  
  Option(playTestServer.start(slabLoader.load(context)))
}

同理,为tracker项目也创建独立的类加载器和启动逻辑,即可实现两个服务的隔离运行。

方案二:调整SBT依赖逻辑,依赖服务启动任务而非类

E2E测试不需要将两个项目的类加入当前类路径,而是让测试任务依赖两个项目的服务启动任务,让服务在独立JVM进程中运行。修改SBT配置如下:

lazy val e2eTest = project
  .in(file("e2e-test"))
  .disablePlugins(ScroogeSBT)
  .settings(standardSettingsWithLibraryDependencies: _*)
  .settings(buildTrackerJS := runScript("./tracker-js/build.sh"))
  .settings(
    // 测试前先启动两个服务
    (Test / test) := ((Test / test) dependsOn buildTrackerJS, tracker / Test / startServer, theSlab / Test / startServer).value,
    // 测试结束后停止服务
    (Test / test) := ((Test / test).andFinally {
      (tracker / Test / stopServer).value
      (theSlab / Test / stopServer).value
    }).value,
    // 原有配置保留
    Test / javaOptions ++= Seq(
      "--add-exports=java.base/sun.security.x509=ALL-UNNAMED",
      "--add-opens=java.base/sun.security.ssl=ALL-UNNAMED"
    ),
    libraryDependencies ++= List(
      "pekko-actor",
      "pekko-actor-typed",
      "pekko-protobuf-v3",
      "pekko-serialization-jackson",
      "pekko-slf4j",
      "pekko-stream"
    ).map { art => "org.apache.pekko" %% art % "1.0.1" },
    libraryDependencies ++= List(
      "org.scalatest" %% "scalatest" % scalaTestVersion % Test,
      "org.scalatestplus" %% "selenium-4-12" % "3.2.17.0" % Test,
      "org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test,
      "io.github.bonigarcia" % "webdrivermanager" % "5.6.2" % Test,
      "com.lihaoyi" %% "os-lib" % "0.9.2" % Test,
      "com.lihaoyi" %% "cask" % "0.9.1" % Test,
      "com.lihaoyi" %% "scalatags" % "0.12.0" % Test,
      "io.circe" %% "circe-core" % "0.14.6" % Test,
      "io.circe" %% "circe-parser" % "0.14.6" % Test
    )
  )

注:需要确保tracker和theSlab项目中定义了startServer和stopServer任务,用于启动/停止各自的Play服务。这种方式是E2E测试的标准实践,完全避免类路径冲突。

方案三:重构项目消除类名冲突

如果两个项目存在同名类(比如BuildInfo、ApplicationLoader),直接修改其中一个项目的类名或包路径,从根源上解决冲突。例如:

  • 将theSlab的ApplicationLoader重命名为TheSlabApplicationLoader
  • 将configuration包改为com.example.theslab.configuration

内容的提问来源于stack exchange,提问作者Jon Flynn

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 10:07:50