Data Modeling With Jackson Json and Scala
我用Scala有一年半左右,第一次看brianhsu介紹 case class ,就對 Scala 的簡潔著迷,當然,經過了一年半後,許多的面紗揭開後,Scala在與Java函式庫整合的小問題就跑出來了,接下來,我會寫寫怎麼用Jackson Json及Scala來設計一個Json WebService API的Model Objects.
Scala的case class在經過Scala compiler轉換後,就會生成一個POJO,例如這樣一個簡單的case class
會被轉換成如下的Java Code
這個POJO跟一般POJO的不同處在於
這在一般使用上,不是問題,反而是優點,然而,當於 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(...)。
第一的問題,在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的那個屬性之上。
當我們的Scala case class已經是Jackson Json Serializable後,我們要怎麼樣把scala case class object轉成json string再轉回來呢,請看底下的例子。
這邊我們會碰上Scala較Java語法上比較沒有那麼漂亮的地方,在Java裡,若是函式支援generic且又有個Class的參數,那麼,我們不用把傳兩次進去這個函式,如
因此在Scala的這邊,我會寫個JsonSerializer來把 scala 這邊的語法弄漂亮些
最後,
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(下一回,我們會看,如何讓Jackson Json能夠對多型的物件做處理
@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)
}
}