多项目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
相关产品推荐
相关产品推荐

