我正在尝试从另一个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
位置传递给类路径。
我设法使它工作设置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填充类路径参数时不依赖这个属性。相反,我使用了如下代码:
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)
}
不过,如果有更有知识的人来解释到底是什么造成了差异,那就太好了。