当 运行 来自 IDE 时 RMI 服务器找到 scala.Option 但当来自 sbt 运行 时无法做到这一点

RMI server finds scala.Option when run from IDE but cannot do it when run from sbt

我正在尝试从一个 JVM 进程生成另一个 JVM 进程并使它们通过 RMI 进行通信。我设法让它从 IDE 开始工作,但是由于某种原因,当我尝试 运行 来自 sbt 的代码时,它失败了:

java.rmi.ServerError: Error occurred in server thread; nested exception is: 
  java.lang.NoClassDefFoundError: scala/Option
  at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:417) ~[na:1.8.0_60]
  at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:268) ~[na:1.8.0_60]

我的问题是弄清楚 运行 从 IDE 到 SBT 之间有什么变化。

代码

首先,我尝试使用随机端口号创建注册表,以避免因使用端口而导致失败:

@tailrec
def getRegister(attemptsLeft: Integer = 10): (Registry, Integer) = {
  val possiblePorts = (1024 to 65536)
  val randomPort    = possiblePorts(scala.util.Random.nextInt(possiblePorts.size))

  Try (LocateRegistry createRegistry randomPort) match {
    case Success(registry) => (registry, randomPort)
    case Failure(ex)       => if (attemptsLeft <= 0) throw ex
                              else getRegister(attemptsLeft - 1)
  }
}

我使用 LocateRegistry.createRegistry 因为它应该解决启动和结束 RMI 进程以及将当前 class 路径传递给它的问题。

当我启动子进程时,我复制 class 父进程的路径 - main class 包含在同一个项目中,因此我可以简单地复制用于 运行 父进程的 JVM 参数过程以确保它可以访问相同的库。

然后子进程使用以下代码:

Try {
  val server   = ... // class which will do the job
  val stub     = UnicastRemoteObject.exportObject(server, 0).asInstanceOf[Server]
  val registry = LocateRegistry getRegistry remotePort

  registry.bind(serverName, stub) // throws in SBT, succeeds in IDE
} match {
  case Success(_)  => logger debug "Remote ready"
  case Failure(ex) => logger error("Remote failed", ex)
                      System exit -1
}

我错过了什么?使用 LocateRegistry.createRegistry 应该复制父进程的 class 路径(已经在几个地方使用 Option,它必须可以访问 class),子进程可以访问这个 class 也是如此(我检查过以确保)。然而由于某种原因,当我 运行 sbt LocateRegistry.createRegistry 下的代码无法将 scala.Option 位置传递给 class 路径时。

我成功设置了 java.rmi.server.codebase 系统 属性。

我不确定到底是什么坏了,如果有人真的解释了我会很高兴。我疯狂地猜测,当我 运行 LocateRegistry getRegistry remotePort 它利用了 "java.class.path" 这有点不可靠。

当我从 IDE 启动应用程序时,它会将所有依赖项直接传递给 JVM - 所有使用的 JAR 都出现在 java.class.path 中。另一方面,当我从 SBT 启动它时,我得到的只是 /usr/share/sbt-launcher-packaging/bin/sbt-launch.jar.

我没有注意到这个问题,因为我在为子 JVM 填充 class 路径参数时不依赖这个 属性。相反,我使用了类似的东西:

lazy val javaHome = System getProperty "java.home"

lazy val classPath = System getProperty "java.class.path"

private lazy val jarClassPathPattern  = "jar:(file:)?([^!]+)!.+".r
private lazy val fileClassPathPattern = "file:(.+).class".r

def classPathFor[T](clazz: Class[T]): List[String] = {
  val pathToClass = getPathToClassFor(clazz)

  val propClassPath   = classPath split File.pathSeparator toSet

  val loaderClassPath = clazz.getClassLoader.asInstanceOf[URLClassLoader].getURLs.map(_.getFile).toSet

  val jarClassPath    = jarClassPathPattern.findFirstMatchIn(pathToClass) map { matcher =>
    val jarDir = Paths get (matcher group 2) getParent()
    s"${jarDir}/*"
  } toSet

  val fileClassPath   = fileClassPathPattern.findFirstMatchIn(pathToClass) map { matcher =>
    val suffix   = "/" + clazz.getName
    val fullPath = matcher group 1
    fullPath substring (0, fullPath.length - suffix.length)
  } toSet

  (propClassPath ++ loaderClassPath ++ jarClassPath ++ fileClassPath ++ Set(".")).toList
}

def getPathToClassFor[T](clazz: Class[T]) = {
  val url = clazz getResource s"${clazz.getSimpleName}.class"
  Try (URLDecoder decode (url.toString, "UTF-8")) match {
    case Success(classFilePath) => classFilePath
    case Failure(_)             => throw new IllegalStateException("")
  }
}

java.rmi.server.codebase 中重新使用这些额外的 JAR 后,一切都开始可靠地工作:

def configureRMIFor[T](clazz: Class[T]): Unit = {
  val classPath = classPathFor(clazz)
  val codebase  = if (classPath isEmpty) ""
                  else classPath map (new File(_).getAbsoluteFile.toURI.toURL.toString) reduce (_ + " " + _)

  logger trace s"Set java.rmi.server.codebase to: $codebase"
  System setProperty ("java.rmi.server.codebase", codebase)
}

不过,如果有知识渊博的人来解释一下究竟是什么造成了不同,我会很高兴。