Tuesday, November 15, 2011

Data Modeling With Jackson Json and Scala

我用Scala有一年半左右,第一次看brianhsu介紹 case class ,就對 Scala 的簡潔著迷,當然,經過了一年半後,許多的面紗揭開後,Scala在與Java函式庫整合的小問題就跑出來了,接下來,我會寫寫怎麼用Jackson Json及Scala來設計一個Json WebService API的Model Objects.

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的不同處在於
  1. immutable object,沒有辦法去更改這物件的屬性
  2. 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 且又有個Class的參數,那麼,我們不用把傳兩次進去這個函式,如

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