SBT/Play2 多项目设置在运行/测试的类路径中不包括依赖项目

Posted

技术标签:

【中文标题】SBT/Play2 多项目设置在运行/测试的类路径中不包括依赖项目【英文标题】:SBT/Play2 multi-project setup does not include dependant projects in classpath in run/test 【发布时间】:2012-09-11 21:26:37 【问题描述】:

我有以下 SBT/Play2 多项目设置:

import sbt._
import Keys._
import PlayProject._

object ApplicationBuild extends Build 
  val appName         = "traveltime-api"
  val appVersion      = "1.0"

  val appDependencies = Seq(
    // Google geocoding library
    "com.google.code.geocoder-java" % "geocoder-java" % "0.9",
    // Emailer
    "org.apache.commons" % "commons-email" % "1.2",
    // CSV generator
    "net.sf.opencsv" % "opencsv" % "2.0",

    "org.scalatest" %% "scalatest" % "1.7.2" % "test",
    "org.scalacheck" %% "scalacheck" % "1.10.0" % "test",
    "org.mockito" % "mockito-core" % "1.9.0" % "test"
  )

  val lib = RootProject(file("../lib"))
  val chiShape = RootProject(file("../chi-shape"))

  lazy val main = PlayProject(
    appName, appVersion, appDependencies, mainLang = SCALA
  ).settings(
    // Add your own project settings here
    resolvers ++= Seq(
      "Sonatype Snapshots" at
        "http://oss.sonatype.org/content/repositories/snapshots",
      "Sonatype Releases" at
        "http://oss.sonatype.org/content/repositories/releases"
    ),
    // Scalatest compatibility
    testOptions in Test := Nil
  ).aggregate(lib, chiShape).dependsOn(lib, chiShape)

如您所见,该项目依赖于两个独立的子项目:lib 和 chiShape。

现在编译工作正常 - 所有源代码都已正确编译。但是,如果我尝试运行或测试,运行时中的任何一个任务都没有加载类路径上的子项目中的类,并且由于 NoClassFound 异常,事情变得一团糟。

例如 - 我的应用程序必须从文件加载序列化数据,它是这样的:测试启动 FakeApplication,它尝试加载数据并繁荣:

[info] CsvGeneratorsTest:
[info] #markerFilterCsv 
[info] - should fail on bad json *** FAILED ***
[info]   java.lang.ClassNotFoundException: com.library.Node
[info]   at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
[info]   at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
[info]   at java.security.AccessController.doPrivileged(Native Method)
[info]   at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
[info]   at java.lang.ClassLoader.loadClass(ClassLoader.java:423)
[info]   at java.lang.ClassLoader.loadClass(ClassLoader.java:356)
[info]   at java.lang.Class.forName0(Native Method)
[info]   at java.lang.Class.forName(Class.java:264)
[info]   at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:622)
[info]   at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1593)
[info]   ...

奇怪的是 stage 在 staged/ 中创建了一个带有 chi-shapes_2.9.1-1.0.jar 和 lib_2.9.1-1.0.jar 的目录结构。

如何让我的运行时/测试配置将子项目放入类路径?

更新:

我在 Global#onStart 中添加了以下代码:

  override def onStart(app: Application) 
    println(app)
    ClassLoader.getSystemClassLoader.asInstanceOf[URLClassLoader].getURLs.
      foreach(println)
    throw new RuntimeException("foo!")
  

当我启动测试时,类路径的填充非常非常糟糕,至少可以这么说:)

FakeApplication(.,sbt.classpath.ClasspathUtilities$$anon$1@182253a,List(),List(),Map(application.load-data -> test, mailer.smtp.test-mode -> true))
file:/home/arturas/Software/sdks/play-2.0.3/framework/sbt/sbt-launch.jar
[info] CsvGeneratorsTest:

启动分阶段应用程序时,有很多东西,应该是这样的:)

$ target/start
Play server process ID is 29045
play.api.Application@1c2862b
file:/home/arturas/work/traveltime-api/api/target/staged/jul-to-slf4j.jar

这很奇怪,因为我认为类路径中至少应该有测试 jar?

【问题讨论】:

我不确定,但你可以尝试用 Project 替换 RootProject 吗? 不,然后它会抱怨 [error] java.lang.AssertionError: assertion failed: Directory /home/arturas/work/tt-api/chi-shape 不包含在 build root /home/ arturas/work/tt-api/api 【参考方案1】:

看来我已经解决了。

罪魁祸首是ObjectInputStream默认忽略线程本地类加载器,只使用系统类加载器。

所以我从:

  def unserialize[T](file: File): T = 
    val in = new ObjectInputStream(new FileInputStream(file))
    try 
      in.readObject().asInstanceOf[T]
    
    finally 
      in.close
    
  

收件人:

  /**
   * Object input stream which respects thread local class loader.
   *
   * TL class loader is used by SBT to avoid polluting system class loader when
   * running different tasks.
   */
  class TLObjectInputStream(in: InputStream) extends ObjectInputStream(in) 
    override protected def resolveClass(desc: ObjectStreamClass): Class[_] = 
      Option(Thread.currentThread().getContextClassLoader).map  cl =>
        try  return cl.loadClass(desc.getName)
        catch  case (e: java.lang.ClassNotFoundException) => () 
      
      super.resolveClass(desc)
    
  

  def unserialize[T](file: File): T = 
    val in = new TLObjectInputStream(new FileInputStream(file))
    try 
      in.readObject().asInstanceOf[T]
    
    finally 
      in.close
    
  

我的班级没有发现问题消失了!

感谢 How to put custom ClassLoader to use? 和 http://tech-tauk.blogspot.com/2010/05/thread-context-classlaoder-in.html 关于反序列化和线程本地类加载器的有用见解。

【讨论】:

【参考方案2】:

这听起来类似于这个错误https://play.lighthouseapp.com/projects/82401/tickets/659-play-dist-broken-with-sub-projects,尽管那个错误是关于dist 而不是test。我认为该修复尚未发布到最新的稳定版本,因此请尝试从源代码构建 Play(并且不要忘记使用 aggregatedependsOn,如该链接所示。

或者,作为一种解决方法,在 sbt 中,您可以使用 project lib 导航到子项目,然后键入 test。这有点手动,但如果你愿意,你可以编写脚本。

【讨论】:

以上是关于SBT/Play2 多项目设置在运行/测试的类路径中不包括依赖项目的主要内容,如果未能解决你的问题,请参考以下文章

如何将“提供的”依赖项添加回运行/测试任务的类路径?

在 kotlin 多平台项目中运行测试

如何为本地主机测试运行设置虚拟目录?

如何找到Surefire / Junit4测试将搜索的类路径?

为hadoop设置的类路径在哪里

使用 Hyn/多租户在 Laravel 项目中设置测试