case class
Scala的case class在經過Scala compiler轉換後,就會生成一個POJO,例如這樣一個簡單的case class
case class Disaster(event: String)
會被轉換成如下的Java Code
public class Disaster implements ScalaObject, Product, Serializable { public String event() { return event; } public int hashCode() { return ScalaRunTime$.MODULE$._hashCode(this); } public String toString() { return ScalaRunTime$.MODULE$._toString(this); } public boolean equals(Object obj) { //removed } public Disaster(String event) { this.event = event; super(); scala.Product.class.$init$(this); } private final String event; }
這個POJO跟一般POJO的不同處在於
- immutable object,沒有辦法去更改這物件的屬性
- getter的命名不是Java通用的 getXxxx() 而是 xxxx()
這在一般使用上,不是問題,反而是優點,然而,當於 Java binding framework 如 Jackson Json 或 JAXB 要做整合時,就變成問題了,原因在於,多數的 java binding frameworks 在將資料轉回Java Object時,多是呼叫沒代參數的default constructor,然後再呼叫setter(s)把物件的屬性傳進去。
另一個問題則是,多數的java binding frameworks在偵側那些屬性是serializable時,要麻是用java annotation一個個屬性去標,要不然就是用 refection 去找 getXxxx()
而好死不死的plain scala case class沒有辦法滿足上面兩個條件,因此,我們要想辦法繞過這兩個問題。第二個問題比較好解,只要在屬性上標上 @scala.reflect.BeanProperty ,Scala compiler就會自動幫你把你的屬性加上 getXxxx() 及 setXxxx(...)。
case class Disaster(@BeanProperty event: String)
第一的問題,在Jackson Json的解法是用 @JsonCreator 去指定不代變數default constructor的替代品,然而,在這邊我們須要把每一個屬性的名稱,用 @JsonProperty("xxx") 標在 constructor 的參數上,這是因為,Java compiler在編譯時,會把 parameter 的名稱擦去,如 public Disaster(String event) 在經過編譯後的 bytecode 中只剩下 public Disaster(String p1),因此我們須要用java annotation來強制幫參數取個名稱,這樣,當Jackson在把 Json String 轉成Object時,才知道該把那個json object的屬性對到java object的那個屬性之上。
case class Disaster @JsonCreator()( @BeanProperty @JsonProperty("event") event: String )
當我們的Scala case class已經是Jackson Json Serializable後,我們要怎麼樣把scala case class object轉成json string再轉回來呢,請看底下的例子。
val disaster = new Disaster("88 typhoon") val mapper = new org.codehaus.jackson.map.ObjectMapper() val json = mapper.writeValueAsString(disaster) assert(json === """{"event": "88typhon"}"""} val copy = mapper.readValue[Disaster](json, classOf[[Disaster]) assert(copy === disaster)
這邊我們會碰上Scala較Java語法上比較沒有那麼漂亮的地方,在Java裡,若是函式支援generic
Disaster copy = mapper.readValue(json, Disaster.class)
因此在Scala的這邊,我會寫個JsonSerializer來把 scala 這邊的語法弄漂亮些
object JsonSerializer { private val mapper = new ObjectMapper def fromJson[T <: AnyRef](jsonString: String)(implicit m: Manifest[T]): T = { mapper.readValue(jsonString, m.erasure).asInstanceOf[T]; } } // readValue the scala way. val disaster = JsonSerializer.fromJson[Disaster](json)
最後,
@JsonCreator
不一定是要標在constructor上面,他也可以標在static method上面,這樣一來,可以讓你做到scala constructor無法做到的一些處理case class Disaster( @BeanProperty event: String, location: Option[GeoPoint]) { /** helper method for jackson json */ def getLocation = location.getOrElse(null) } object Disaster { @JsonCreator def newInstance( @Property("event") event: String, @Property("location") location: GeoPoint): Disaster = { val locationOption = if (location == null) { None } else { Some(location) } return new Disaster(event, locationOption) } }下一回,我們會看,如何讓Jackson Json能夠對多型的物件做處理
No comments:
Post a Comment