背景
笔者最近尝试使用case class作为ActiveMQ消息的定义。同时实现,在消息发送前,将case class实例对象序列化成Json字符串;在消息接收到后,可以将消息中的Json字符串反序列化为cass class对象。上述工作抽象成trait来实现,让每一个case class定义的消息都混入该trait,以实现代码复用。由于使用scala编码,所以Json的序列化和反序列化使用的json4s组件。
case class Person
为序列化后发送的消息,object Person
负责将Json字符串反序列化为case class Person
对象。它们分别混入了序列化和反序列化的trait,TransformToJson
、TransformToObj
.
消息体
package io.neal.json.message
import io.neal.json.{TransformToJson, TransformToObj}
case class Person(name: String, age: Int, email: String) extends TransformToJson
object Person extends TransformToObj[Person]
序列化trait
import org.json4s._
import org.json4s.native.Serialization
import org.json4s.native.Serialization.{write => jWrite}
trait TransformToJson {
def toJson: String = {
implicit val format: AnyRef with Formats= Serialization.formats(NoTypeHints)
jWrite(this)
}
}
反序列化trait
import org.json4s._
import org.json4s.native.Serialization
import org.json4s.native.Serialization.{read => jRead}
trait TransformToObj[A] {
def toObj(json: String): A = {
implicit val format: AnyRef with Formats= Serialization.formats(NoTypeHints)
jRead[A](json)
}
}
测试用例
package io.neal.json.message
import org.scalatest.FlatSpec
class PersonTest extends FlatSpec {
private val personObj = Person("neal", 4, "[email protected]")
private val personJson ="""{"name":"neal","age":4,"email":"[email protected]"}"""
"Instance of Person converts to json string" should "be ok" in {
assertResult(personJson)(personObj.toJson)
}
"Json String converts to instance of Person" should "be ok" in {
assertResult(personObj)(Person.toObj(personJson))
}
}
问题
运行测试用例出现以下编译错误
Error:(11, 13) No Manifest available for A.
jRead[A](json)
Error:(11, 13) not enough arguments for method read: (implicit formats: org.json4s.Formats, implicit mf: scala.reflect.Manifest[A])A.
Unspecified value parameter mf.
jRead[A](json)
如果将TransformToObj
中的jRead[A](json)
修改为jRead(json)
,编译可以通过,但第二条测试用例抛出异常
Parsed JSON values do not match with class constructor
args=
arg types=
executable=Executable(Constructor(public scala.runtime.Nothing$()))
cause=null
types comparison result=
org.json4s.package$MappingException: Parsed JSON values do not match with class constructor
args=
arg types=
executable=Executable(Constructor(public scala.runtime.Nothing$()))
cause=null
types comparison result=
at org.json4s.reflect.package$.fail(package.scala:95)
at org.json4s.Extraction$ClassInstanceBuilder.instantiate(Extraction.scala:615)
at org.json4s.Extraction$ClassInstanceBuilder.result(Extraction.scala:639)
at org.json4s.Extraction$.$anonfun$extract$10(Extraction.scala:409)
at org.json4s.Extraction$.$anonfun$customOrElse$1(Extraction.scala:646)
...
原因
scala的泛型类型信息,比如上述代码TransformToObj[Person]
中的Person,在编译期时是存在的,但在jvm运行时TransformToObj
中是得不到Person类型信息的。因此,对于jRead[A](json)
,由于编译器在编译时,从类型模板参数A获取不到真实的类型信息,所以编译报错。对于jRead(json)
,由于没有指定类型模板参数,虽然编译期时不会报错,但运行时还会由于获取不到真实类型信息,导致反序列化失败抛异常。
解决方法
明白了问题原因,那么解决的思路就比较明确了,即在编译时把消息case class的具体类型信息保留下来,并在调用TransformToObj
的toObj
方法时将类型信息传递给它。
方法1:参数传递类型信息
package io.neal.json
import org.json4s._
import org.json4s.native.Serialization
import org.json4s.native.Serialization.{read => jRead}
import scala.reflect.Manifest
trait TransformToObj {
def toObj[A](json: String)(mf: Manifest[A]): A = {
implicit val format: AnyRef with Formats= Serialization.formats(NoTypeHints)
jRead(json)(format, mf)
}
}
package io.neal.json.message
import io.neal.json.{TransformToJson, TransformToObj}
case class Person(name: String, age: Int, email: String) extends TransformToJson
object Person extends TransformToObj {
def toObj(json: String): Person = super.toObj[Person](json)(manifest[Person])
}
方法2:覆盖protected参数传递类型信息
package io.neal.json
import org.json4s._
import org.json4s.native.Serialization
import org.json4s.native.Serialization.{read => jRead}
import scala.reflect.Manifest
trait TransformToObj[A] {
protected val mf: Manifest[A] = null
def toObj(json: String): A = {
implicit val format: AnyRef with Formats= Serialization.formats(NoTypeHints)
jRead(json)(format, mf)
}
}
package io.neal.json.message
import io.neal.json.{TransformToJson, TransformToObj}
import scala.reflect.Manifest
case class Person(name: String, age: Int, email: String) extends TransformToJson
object Person extends TransformToObj[Person] {
override protected val mf: Manifest[Person] = manifest[Person]
}