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 的差異。
Gson.toJson()
遇到 false / null 值不會省略該 field, 但是 AutoBean 會。 也就是說,如果foo.setDeleted(false);
, 那麼AutoBeanCodex.encode()
出來的字串不會看到deleted
。當然,這其實不妨礙正常運作。 GSON 的作法可能有些人還會覺得怪?
日期(
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
拿 GSON 跟 AutoBean 相比是很有趣的。 一個是完美到不需要瞭解內部到底發生什麼事情, 一個則是太糟糕了,所以根本不想瞭解。
短時間之內,我可能還是不會放棄 AutoBean,
除非 GWT 2.6 就遺棄 AutoBean,
或是找到更好的 tool(而不是 GWT-Jackson 那種 style)。
都花了這麼多時間了,就看看能被炸到走到什麼程度。
寫到後來,都不知道到底是在介紹推廣還是在吐槽。 只能說,嗯… 我對 GWT 真的很有愛…… [遠目]