GWT 的 AutoBean

看板java作者 (痞子軍團團長)時間11年前 (2013/12/29 14:56), 編輯推噓0(000)
留言0則, 0人參與, 最新討論串1/1
blog 版:http://blog.dontcareabout.us/2013/12/gwt-autobean.html BBS 版以 markdown 語法撰寫 ______________________________________________________________________ [AutoBean] 是目前打算拿來在 GWT 中處理 JSON 的工具。 不過在講正事之前,先扯兩段雜談 [毆飛] [AutoBean]: https://code.google.com/p/google-web-toolkit/wiki/AutoBean ## 雜談 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 上也可以使用。 [GSON]: https://code.google.com/p/google-gson/ [TypeAdapterFactory 的用法]: #1IVxn9h0 (java) [bGwtGson]: https://github.com/heroandtn3/bGwtGson ## 雜談 2:謎樣裏技? ## 我搞不太懂 [AutoBean] 在 GWT 當中扮演什麼樣的角色? 彷彿還蠻多人在用的(因為大家都炸同樣的問題... Orz), 但是官方指南似乎沒有這個東西(JavaDoc 當然還是有), 教學文件只有出現在 google code 的 wiki 上, 所以這是裏技嗎? 我比較怕這是即將被拋棄的裏技 Orz 畢竟要在 GWT 裡頭處理 JSON 並不是太麻煩。 官方指南建議的 JSNI / Overlay Types 寫起來堪稱簡單直覺, 尤其是跟 [AutoBean] 相比的話 [死]。 再不然 [GWT-Jackson] 好像也是種選擇? 剛好兩者的風格我都不愛 [淚目] 最大的哏在於 [AutoBean] 並沒有辦法直接處理 `List<T>` 這種東西, [這個 bug][Issue 6904] 在 2011.10 被提出之後, 2012.11 最後一個 comment 之後就無聲無息了, 目前最新的 GWT 2.5.1 還是有同樣的問題 Zzzz,只能靠 workaround。 後頭會詳述這些事情 [死]。 [GWT-Jackson]: https://github.com/nmorel/gwt-jackson [Issue 6904]: https://code.google.com/p/google-web-toolkit/issues/ detail?id=6904 ## 怎麼用? ## 好了,終於要進入正題了。 [握拳] 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*) [Joda]: http://www.joda.org/joda-time/ ## `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][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 的部份那才叫經典。 [decode workaround]: http://stackoverflow.com/questions/13651068/ gwt-autobean-how-to-handle-lists ### 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][encode 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 [encode workaround]: https://groups.google.com/d/msg/google-web-toolkit/nvIotNHy-Io/GkXz_WQXvR4J ## 結尾 murmur ## 拿 [GSON] 跟 [AutoBean] 相比是很有趣的。 一個是完美到不需要瞭解內部到底發生什麼事情, 一個則是太糟糕了,所以根本不想瞭解。 短時間之內,我可能還是不會放棄 [AutoBean], 除非 GWT 2.6 就遺棄 [AutoBean], 或是找到更好的 tool(而不是 [GWT-Jackson] 那種 style)。 都花了這麼多時間了,就看看能<strike>被炸到</strike>走到什麼程度。 寫到後來,都不知道到底是在介紹推廣還是在吐槽。 只能說,嗯... 我對 GWT 真的很有愛...... [遠目] -- 錢鍾書: 說出來的話 http://www.psmonkey.org 比不上不說出來的話 Java 版 cookcomic 版 只影射著說不出來的話 and more...... -- ※ 發信站: 批踢踢實業坊(ptt.cc) ◆ From: 59.115.237.204
文章代碼(AID): #1IlyUlH2 (java)
文章代碼(AID): #1IlyUlH2 (java)