RMI服务器从IDE运行时发现scala.Option但从sbt

时间:2015-09-07 10:33:18

标签: scala sbt rmi

我正在尝试从另一个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进程以及将当前类路径传递给它的问题。

当我启动子进程时,我复制父进程的类路径 - 主类包含在同一个项目中,因此我可以简单地复制用于运行父进程的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应该复制父进程的类路径(在几个地方已经使用Option,它必须能够访问该类),子进程也可以访问这个类(我检查过确定)。但由于某种原因,我在sbt LocateRegistry.createRegistry下运行代码时无法将scala.Option位置传递给类路径。

1 个答案:

答案 0 :(得分:0)

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

我不确定究竟发生了什么,如果有人真的解释过,我会很高兴。我疯狂地猜测,当我运行LocateRegistry getRegistry remotePort时,它会使用"java.class.path",这有点不可靠。

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

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

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)
}

不过,如果有更多知识渊博的人会来解释它到底有什么不同,我会很高兴。