2013年12月29日 星期日

GWT 的 AutoBean

AutoBean 是目前打算拿來在 GWT 中處理 JSON 的工具。 不過在講正事之前,先扯兩段雜談 [毆飛]

雜談 1:這樣也可以?

要不是要弄 web API,其實也不會想碰 JSON, GWT RPC 好好的幹麼弄什麼 JSON [遠目]。 不過 server side 要處理 JSON 其實有 GSON, 正常 encode / decode 真的都還蠻簡單的, 簡單到不知道能介紹什麼 XD。 是說 LaPass(ptt.cc)因為有一個我覺得有點詭異的需求, 結果挖出了 TypeAdapterFactory 的用法, 看起來真的很乾淨很炫(炫到都快看不懂了 [遮臉]), 只能說好 library 如當是也。

理所當然的,會想看看有沒有 GWT 版的 GSON, 結果看到 bGwtGson 這玩意差點噴出來。 因為他的作法是用 GWT RPC 把東西丟到 server side, 這樣 server side 就有 GSON 可以用了… 揪咪…

我都不知道該說牛逼還是坑爹,這世界果然很廣大阿 [握拳]

喔對,順帶一提,GSON 在 AppEngine 上也可以使用。

雜談 2:謎樣裏技?

我搞不太懂 AutoBean 在 GWT 當中扮演什麼樣的角色? 彷彿還蠻多人在用的(因為大家都炸同樣的問題… Orz), 但是官方指南似乎沒有這個東西(JavaDoc 當然還是有), 教學文件只有出現在 google code 的 wiki 上, 所以這是裏技嗎?

我比較怕這是即將被拋棄的裏技 Orz

畢竟要在 GWT 裡頭處理 JSON 並不是太麻煩。 官方指南建議的 JSNI / Overlay Types 寫起來堪稱簡單直覺, 尤其是跟 AutoBean 相比的話 [死]。 再不然 GWT-Jackson 好像也是種選擇?

剛好兩者的風格我都不愛 [淚目]

最大的哏在於 AutoBean 並沒有辦法直接處理 List<T> 這種東西, 這個 bug 在 2011.10 被提出之後, 2012.11 最後一個 comment 之後就無聲無息了, 目前最新的 GWT 2.5.1 還是有同樣的問題 Zzzz,只能靠 workaround。 後頭會詳述這些事情 [死]。

怎麼用?

好了,終於要進入正題了。 [握拳]

GWT 已經內建 AutoBean,所以不用另外掛 jar 檔, 但是要在 gwt.xml 當中補上:

<inherits name="com.google.web.bindery.autobean.AutoBean"/>

如果你要把下面這個 JSON 字串轉成 Foo 的 instance

{
    "uid":"cde6c847-d072-4d33-82bd-93fa4710dc9b",
    "limit":50,
    "deleted":true,
    "update":1388157386000
}

首先… Foo 得是個 bean 的 interface,定義一堆 getter/setter, 名稱得跟 JSON 裡頭的一致:

interface Foo {
    public void setUid(String uid);
    public void setLimit(int limit);
    public void setDeleted(boolean deleted);
    public void setUpdate(Date update);
    public String getUid();
    public int getLimit();
    public boolean isDeleted();
    public Date getUpdate();
}

轉換的時候需要先建立一個 factory,通常會這樣寫

interface MyFactory extends AutoBeanFactory {
    AutoBean<Foo> foo();
}
MyFactory factory = GWT.create(MyFactory.class);

然後… 終於可以 decode 了:

String foo = "{" +
        "\"uid\":\"cde6c847-d072-4d33-82bd-93fa4710dc9b\"," +
        "\"limit\":50," +
        "\"deleted\":true," +
        "\"update\":1388157380000" +
    "}";
Foo instance = AutoBeanCodex.decode(factory, Foo.class, foo).as();

encode 的話就是:

String fooJson = AutoBeanCodex.encode(
    AutoBeanUtils.getAutoBean(instance)
).getPayload();

如果願意在 MyFactory 裡頭加一個 method:

interface MyFactory extends AutoBeanFactory {
    AutoBean<Foo> foo();
    //下面這個是新增的
    AutoBean<Foo> genFooBean(Foo foo);
}

那不用 AutoBeanUtils.getAutoBean() 而是

String fooJson = AutoBeanCodex.encode(
    factory.genFooBean(instance)
).getPayload();

有沒有比較快樂就見仁見智,不過後面會需要用到後面這招, 或著這麼說比較實在: 「請忘記 AutoBeanUtils 吧」。

注意事項

如果到這邊你還能忍受 AutoBean, 那先講幾個我已經炸到,但可以理解的哏, 主要是跟 GSON 的差異。

  1. Gson.toJson() 遇到 false / null 值不會省略該 field, 但是 AutoBean 會。 也就是說,如果 foo.setDeleted(false);, 那麼 AutoBeanCodex.encode() 出來的字串不會看到 deleted

    當然,這其實不妨礙正常運作。 GSON 的作法可能有些人還會覺得怪?

  2. 日期(java.util.Date)的處理。 Gson.toJson() 會用 Date.toString() 作值(反之亦然), 但是 AutoBean 則是用 Date.getTime()(反之亦然)。 只能說還是統統用 long 表示日期就算了 (然後在 JSON 當中最好還把這數字當成字串, 免得像 32bit 的 PHP 還給你耍花招 [怨念ing])。 至於 Joda 要解決的議題… 遇到再說 XD

有遇到會再補上來 Orz

List 的炸點

如果你永遠不會 decode / encode 一個 array 或是 List, 那恭喜你,除了寫法稍稍扭曲一點之外, AutoBean 是可以接受、也算好用的(應該啦…)。

實際上… 別鬧了,怎麼可能不處理 List

於是 AutoBean 就成了茶几──上頭擺滿了悲劇。

decode

GSON 吐出來的東西來看,一個 List<Foo> 的 instance 會長這樣 (喔對,我把 update 的型態改成 long 了 XD):

[
    {"uid":"cde6c847-d072-4d33-82bd-93fa4710dc9b",
     "limit":50,"deleted":false,"update":"1388157380000"},
    {"uid":"a391dedf-1f81-4380-a712-59eac4d9aea3",
     "limit":50,"deleted":false,"update":"1388157380000"}
]

想依樣畫葫蘆比照辦理時… 等等,AutoBeanCodex.decode() 的第二個參數要給什麼? 然後於是有人弄出了這個 workaround

首先,要建一個 interface 來代表 List<Foo> 這玩意:

interface FooList {
    public void setList(List<Foo> list);
    public List<Foo> getList();
}

factory 的 interface 則是:

interface MyFactory extends AutoBeanFactory {
    //下面這個暫時用不到
    AutoBean<Foo> genFooBean(Foo foo);

    AutoBean<FooList> fooList();
}

最後,要對拿到的 JSON 字串動手腳,變成這樣:

    FooList fooList = AutoBeanCodex.decode(
        factory, FooList.class, "{\"list\":" + foo + "}"
    ).as();
    List<Foo> instance = fooList.getList();

簡單地說,就是 Java 的部份你要讓他有個 class 為依歸, 但是光這樣還不夠,因為 AutoBean 不知道要從何處理起, 所以 JSON 的部份你也要偽造一下……

等等,還沒完,好戲壓箱底、好酒沈甕底, encode 的部份那才叫經典。

encode

要把一個 List<Foo> 轉成 JSON,這到底是有多難? 不難,如果把剛剛 AutoBeanCode.decode() 出來的 fooList 再次轉成 JSON, 那麼只要 factory 加上

interface MyFactory extends AutoBeanFactory {
    //下面這個暫時用不到
    AutoBean<Foo> genFooBean(Foo foo);

    AutoBean<FooList> fooList();
    //下面這個是新增的
    AutoBean<FooList> genFooListBean(FooList instance);
}

立馬就轉,沒有問題!(也完全沒意義 ==”)

AutoBeanCodex.encode(
    factory.genFooListBean(fooList)
).getPayload();

如果是把既有的 List<Foo> instance 轉換, 依照上面的邏輯,得先實做那個毫無意義的 FooList

FooList fooList = new FooList() {
    List<Foo> list = new ArrayList<Foo>();

    @Override
    public void setList(List<Foo> list) {
        this.list = list;
    }

    @Override
    public List<Foo> getList() {
        return list;
    }
};

//FooImpl 就容許我跳過,反正就是 implements Foo 的東東
fooList.getList().add(new FooImpl());

AutoBeanCodex.encode(
    factory.genFooListBean(fooList)
).getPayload();

執行上面這段程式碼,你就會發現 AutoBeanCodex.encode() 快樂的炸了 NPE,而且完全搞不懂發生了什麼事情。

這是個已知的 bug(Issue 6904), 雖然不知道會不會有人去解…… Orz。 而世界還真的是很廣大,有人也找出了 workaround, 解法就是你不能直接丟 FooImpl 的 instance, 得用 MyFactory.genFooBean() 產生出 AutoBean<Foo>, 再藉由它(as())取得 Foo 的 instance 才可以…… 寫的我自己都亂了,看 code 比較實在:

interface MyFactory extends AutoBeanFactory {
    AutoBean<Foo> genFooBean(Foo foo);
    AutoBean<FooList> fooList();
    AutoBean<FooList> genFooListBean(FooList instance);
}

fooList.getList().add(
    //原本是 new FooImpl()
    factory.genFooBean(new FooImpl()).as()
);

AutoBeanCodex.encode(
    factory.genFooListBean(fooList)
).getPayload();

套最近流行的句型: 「如果這不叫脫褲子放屁,我還真他媽的不知道什麼才叫脫褲子放屁」

喔對,無論 genFooBean() 還是 genFooListBean() 都不能用官方文件用的 AutoBeanUtils.getAutoBean()。 如果拿他替換 genFooBean(),一樣噴 NPE; 如果拿它替話 genFooListBean(),不會噴 NPE, 而是轉換出來的 JSON 字串會是 null。

WTF

結尾 murmur

GSONAutoBean 相比是很有趣的。 一個是完美到不需要瞭解內部到底發生什麼事情, 一個則是太糟糕了,所以根本不想瞭解。

短時間之內,我可能還是不會放棄 AutoBean, 除非 GWT 2.6 就遺棄 AutoBean, 或是找到更好的 tool(而不是 GWT-Jackson 那種 style)。 都花了這麼多時間了,就看看能被炸到走到什麼程度。

寫到後來,都不知道到底是在介紹推廣還是在吐槽。 只能說,嗯… 我對 GWT 真的很有愛…… [遠目]

沒有留言:

張貼留言