Search code examples
scalaserializationpersistenceclassloaderdeserialization

Deserialization throws 'ClassNotFoundException: JavaConversions$SeqWrapper' in Scala 2.10


I have a fairly complex object graph serialized out from Scala-2.9 and I need to read it into Scala-2.10. However, somewhere deep in the object graph Scala-2.10 throws:

! java.lang.ClassNotFoundException: scala.collection.JavaConversions$SeqWrapper
! at java.net.URLClassLoader$1.run(URLClassLoader.java:366) ~[na:1.7.0_21]
! at java.net.URLClassLoader$1.run(URLClassLoader.java:355) ~[na:1.7.0_21]
! at java.security.AccessController.doPrivileged(Native Method) ~[na:1.7.0_21]
! at java.net.URLClassLoader.findClass(URLClassLoader.java:354) ~[na:1.7.0_21]
! at java.lang.ClassLoader.loadClass(ClassLoader.java:423) ~[na:1.7.0_21]
! at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308) ~[na:1.7.0_21]
! at java.lang.ClassLoader.loadClass(ClassLoader.java:356) ~[na:1.7.0_21]
! at java.lang.Class.forName0(Native Method) ~[na:1.7.0_21]
! at java.lang.Class.forName(Class.java:266) ~[na:1.7.0_21]
! at java.io.ObjectInputStream.resolveClass(ObjectInputStream.java:623) ~[na:1.7.0_21]
...

What is the simplest way to load this serialized object into Scala-2.10? The object deserializes correctly with Scala-2.9, but looks like things have moved around in the standard library. Most of the members of scala.collection.JavaConversions are now in scala.collection.convert.Wrappers

Going forward, I am also interested in more robust ways of persisting large complex object graphs without having to explicitly specify the serialization for every class involved.


Solution

  • So here is what ended up working for me, thanks to @som-snytt for pointing me in the right direction:

    object MyWrappers {
      import java.{ lang => jl, util => ju }, java.util.{ concurrent => juc }
      import scala.collection.convert._
      import WrapAsScala._
      import WrapAsJava._
      import Wrapper._
    
      @SerialVersionUID(3200663006510408715L)
      case class SeqWrapper[A](underlying: Seq[A]) extends ju.AbstractList[A] with Wrappers.IterableWrapperTrait[A] {
        def get(i: Int) = underlying(i)
      }
    }
    
    import org.apache.commons.io.input.ClassLoaderObjectInputStream
    object Loader extends ClassLoader {
      override def loadClass(name: String) : Class[_] = {
        import javassist._
        try super.loadClass(name)
        catch {
          case e: ClassNotFoundException if name.startsWith("scala.collection.JavaConversions") => {
                val name2 = name.replaceFirst("scala.collection.JavaConversions",
                                              "MyWrappers")
                val cls = ClassPool.getDefault().getAndRename(name2, name)
                cls.toClass()
              }
        }
      }
    }
    
    val objectStream = new ClassLoaderObjectInputStream(Loader, stream)
    objectStream.readObject()
    

    This allows me to read in my original 2.9 serialized files directly into 2.10 without re-serialization. It depends on Javassist to do the Class indirection and uses ClassLoaderObjectStream from Apache Commons, even though that would be simple to roll your own. I am not that happy that I had to make my own copy of SeqWrapper (this turned out to be the only offending class in my file), but the wrapper classes in scala-2.10's scala.collection.convert.Wrappers have different SerialVersionUIDs than the corresponding classes in 2.9's scala.collection.JavaConversions even though the sources are textually identical. I originally tried to just redirect to scala.collection.convert.Wrappers and set the SerialVersionUID with Javassist:

    object Loader extends ClassLoader {
      override def loadClass(name: String) : Class[_] = {
        import javassist._
        try super.loadClass(name)
        catch {
          case e: ClassNotFoundException if name.startsWith("scala.collection.JavaConversions") => {
                val name2 = name.replaceFirst("JavaConversions", "convert.Wrappers")
                val cls = ClassPool.getDefault().getAndRename(name2, name)
                cls.addField(CtField.make("private static final long serialVersionUID = 3200663006510408715L;", cls))
                cls.toClass()
              }
        }
      }
    }
    

    That allowed me to read the serialized file without exception, but the object that was read in that way was incomplete. (If this approach worked and there was more than one problem class in the file, I would really need a lookup table for the SerialVersionUIDs, but that is beside the point). If anybody knows a way set the SerialVersionUID on the Javassist generated class without breaking anything else something else I would like to hear it.