简体   繁体   中英

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

I am trying to spawn one JVM process from another and make them communicate via RMI. I managed to make it work from IDE, but for some reason when I try to run the code from sbt it fails with:

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]

My problem is figuring out what changes between running it from IDE and SBT.

Code

First I am trying to create registry with random port number to avoid failure due to used port:

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

I used LocateRegistry.createRegistry because it should solve problems with starting and ending RMI process and passing current classpath into it.

When I start child process I copy class path of the parent process - main class is contained within the same project so I can simply copy JVM arguments used to run parent process to make sure that it will have access to the same libraries.

Then child process used following code:

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
}

What did I missed? Using LocateRegistry.createRegistry should copy the class path of the parent process (which uses Option in several places already, it has to have access to the class), child process has access this class as well (I checked to be sure). Yet for some reason when I run the code from under the sbt LocateRegistry.createRegistry fails to pass scala.Option location to the classpath.

I managed to make it work setting up java.rmi.server.codebase System property.

I am not sure what actually broke and I would be glad if someone actually explained it. My wild guess it that when I run LocateRegistry getRegistry remotePort it makes use of "java.class.path" which is kind of unreliable.

When I start application from IDE it passes all deps directly to JVM - all JARs used appear in java.class.path . On the other hand when I start it from SBT all I get is /usr/share/sbt-launcher-packaging/bin/sbt-launch.jar .

I didn't noticed the issue because I don't rely on this property while populating class path argument for child JVM. Instead I used something like:

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

After reusing those additional JARs in java.rmi.server.codebase everything started to work reliably:

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

Still, I it would be nice if someone more knowledgeable would come and explain it what exactly made the difference.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM