備註:若後續有改動,此文件不會再更新,請改參考此文件
分為兩個部份:「class 定義」、「class 間的關係」。
由於 generic type 的定義哏(後敘),
先寫「class 定義」再寫「class 間的關係」比較保險。
備註:若後續有改動,此文件不會再更新,請改參考此文件
分為兩個部份:「class 定義」、「class 間的關係」。
由於 generic type 的定義哏(後敘),
先寫「class 定義」再寫「class 間的關係」比較保險。
AutoBean 是目前打算拿來在 GWT 中處理 JSON 的工具。 不過在講正事之前,先扯兩段雜談 [毆飛]
要不是要弄 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 上也可以使用。
我搞不太懂 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 就成了茶几──上頭擺滿了悲劇。
以 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 的部份那才叫經典。
要把一個 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
拿 GSON 跟 AutoBean 相比是很有趣的。 一個是完美到不需要瞭解內部到底發生什麼事情, 一個則是太糟糕了,所以根本不想瞭解。
短時間之內,我可能還是不會放棄 AutoBean,
除非 GWT 2.6 就遺棄 AutoBean,
或是找到更好的 tool(而不是 GWT-Jackson 那種 style)。
都花了這麼多時間了,就看看能被炸到走到什麼程度。
寫到後來,都不知道到底是在介紹推廣還是在吐槽。 只能說,嗯… 我對 GWT 真的很有愛…… [遠目]
這一陣子因為專案需要 server 與 browser 之間有即時雙向溝通的能力,所以就用了 websocket。
又由於 server 端綁定 PHP(還好 browser 也綁定 Chrome XD),
再加上傳輸的資料還沒有很複雜,單純字串就可以解決,
所以沒有用現有的 GWT websocket library,統統自己來了...... [遮臉]
於是也就順便寫了這篇文章,借 websocket 介紹下面兩個主題:
(當然行有餘力的話也想涵蓋 GWT RPC,不過目前無法)
這三個主題在作〈GWT 版 GAE Channel API〉的時候都有用到 (但是 RPC 的部份只抄其 code、不明其理 [遮臉]), 不過拿 websocket 來介紹可能比較實在一點,GAE 的 channel API 可能太少人用了 [死]。
Image img = new Image("http://an.url/pic.jpg");
@RemoteServiceRelativePath("mineRPC") public interface MineService extends RemoteService { public String startGame(); public GameInfo shoot(String id, int x, int y) throws Exception; }至於那個 annotation 先跳過,後頭會解釋
public interface MineServiceAsync { void startGame(AsyncCallback<String> callback); void shoot(String id, int x, int y, AsyncCallback<GameInfo> callback); }
public class MineServiceImpl extends RemoteServiceServlet implements MineService { private AI_Interface ai = new RandomAI(); //FIXME change your ai here! @Override public String startGame() { String id = UUID.randomUUID().toString(); MineGM setting = new MineGM(); setServer(id, setting); return id+",Random"; } private void setServer(String id, MineGM setting){ this.getThreadLocalRequest().getSession().setAttribute(id+"ID", setting); } private MineGM getServer(String id) throws Exception{ String name = id+"ID"; if(this.getThreadLocalRequest().getSession().getAttribute(name)!=null){ return (MineGM) this.getThreadLocalRequest().getSession().getAttribute(name); }else{ throw new Exception("還沒開局"); } } @Override public GameInfo shoot(String id, int x, int y) throws Exception{ MineGM server = getServer(id); if(x==-1 || y==-1 || server.getMap()[x][y]!=-1){ return MineGM.toGameInfo(server); } if(!server.shoot(x, y, MineGM.USER)){ int[] xy = new int[2]; do{ ai.guess(MineGM.toGameInfo(server), xy); }while(server.shoot(xy[0], xy[1], MineGM.AI)); } setServer(id, server); return MineGM.toGameInfo(server); } }
<!-- in web.xml --> <servlet-mapping> <servlet-name>mineRPC</servlet-name> <url-pattern>/_mine/mineRPC</url-pattern> </servlet-mapping>
this.getThreadLocalRequest().getSession()其餘的程式碼... 嗯... 不在 GWT 的範圍當中,跳過 XD
MineServiceAsync msa = GWT.create(MineService.class);然後就可以用 msa.shoot() 來告訴 server 要踩哪個位置
msa.shoot(this.gameID, hitX, hitY, new AsyncCallback<GameInfo>(){ @Override public void onFailure(Throwable caught) { Window.alert("shoot : "+caught.getLocalizedMessage()); } @Override public void onSuccess(GameInfo result) { setShootResult(result); } });
public static final int UNKNOW = -1; private int x; private int y; private int total; //總共幾個地雷 private int remainder; //剩下幾個地雷 private boolean[][] answer; //地雷分佈圖 private int[][] map; //玩家看到的地圖 private int[] playerHit = new int[2]; //分別踩了幾個
public boolean shoot(int hitX, int hitY, boolean who){ map[hitX][hitY] = count(hitX, hitY); //踩到空地的連鎖反應 if(map[hitX][hitY]==0){ for(int i=-1; i<2; i++){ if(hitX+i==x || hitX+i<0){continue;} for(int j=-1; j<2; j++){ if(hitY+j==y || hitY+j<0){continue;} if(map[hitX+i][hitY+j] != -1){ continue; }else{ shoot(hitX+i, hitY+j, who); } } } } //不同人踩到地雷要給不同值 if(map[hitX][hitY]==9){ remainder--; if(who){ playerHit[0]++; }else{ map[hitX][hitY]=-9; playerHit[1]++; } } return Math.abs(map[hitX][hitY])==9; }
public static GameInfo toGameInfo(MineGM server) { GameInfo result = new GameInfo(); result.setMap(server.getMap()); result.setRemainder(server.remainder); result.setTotal(server.total); result.setPlayerHit(server.playerHit); return result; }
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent"> <ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui"> <ui:style> .title{ padding-left: 5px; } </ui:style> <g:FlowPanel> <g:InlineLabel ui:field="title" styleName="{style.title}"></g:InlineLabel> <g:InlineLabel ui:field="hitCount"></g:InlineLabel> </g:FlowPanel> </ui:UiBinder>
public class PlayerInfo extends Composite { private static PlayerInfoUiBinder uiBinder = GWT.create(PlayerInfoUiBinder.class); interface PlayerInfoUiBinder extends UiBinder<Widget, PlayerInfo> { } @UiField Label title; @UiField Label hitCount; public PlayerInfo() { initWidget(uiBinder.createAndBindUi(this)); setHitCount(0); } public void setHitCount(int i) { hitCount.setText(""+i); } public void setName(String name){ title.setText(name+":"); } }
<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent"> <ui:UiBinder xmlns:ui="urn:ui:com.google.gwt.uibinder" xmlns:g="urn:import:com.google.gwt.user.client.ui" xmlns:m="urn:import:org.psmonkey.product.client.mine"> <ui:style> .playerInfo{ width: 480px; } .cpu{ width: 220px; background-color: red; } .player{ width: 220px; background-color: #64A0C8; } .remainder{ width: 40px; color: white; background-color: gray; text-align: center; } </ui:style> <g:VerticalPanel> <g:HorizontalPanel styleName="{style.playerInfo}"> <m:PlayerInfo ui:field="cpu" styleName="{style.cpu}"></m:PlayerInfo> <g:Label ui:field="remainder" styleName="{style.remainder}"></g:Label> <m:PlayerInfo ui:field="player" styleName="{style.player}"></m:PlayerInfo> </g:HorizontalPanel> <g:FlexTable ui:field="map"></g:FlexTable> </g:VerticalPanel> </ui:UiBinder>
public class Cell{然後再來包個「世界」的 class
private boolean now;
private boolean next;
public Cell(){}
public Cell(boolean alive) {now = alive;}
public boolean isAlive(){return now;}
public void setNextAlive(boolean alive){
this.next = alive;
}
public void nextTurn() {now = next;}
}
public class World{好了,物件都包好了,那麼現在該來解決畫面了
private int width;
private int height;
private Cell[][] cell;
public World(int w, int h){
width = w;
height = h;
cell = new Cell[w][h];
for(int i=0; i<2; i++){
tmpW = w+i;
if( tmpW<0 || tmpW==width){
continue;
}
for(int j=-1; j<2; j++){
tmpH = h+j;
if( tmpH<0 || tmpH==height){
continue;
}
if(tmpH==h && tmpW==w){
continue;
}
if(getCell(tmpW, tmpH).isAlive()){
sum++;
}
}
}
switch(sum){
case 0: case 1: case 4: case 5:
case 6: case 7: case 8:
getCell(w, h).setNextAlive(false);
break;
case 3:
getCell(w, h).setNextAlive(true);
break;
}
}
public Cell getCell(int w, int h){
return cell[w][h];
}
}
public class Monitor extends VerticalPanel{constructor 要把「世界」跟「棋盤」正式建立起來
private World world;
private Grid board;
private int width = 10, height = 10;
}
public Monitor(){這邊先跑出來兩個新的 method
world = new World(width, height);
board = new Grid(width, height);
this.add(board);
Button next = new Button("下一世代");
//點下一步要幹些什麼好事?
next.addClickHandler(new ClickHandler(){
@Override
public void onClick(ClickEvent event) {
nextTurn();
}
});
this.add(next);
//重新整理畫面
refresh();
}
public void nextTurn(){至於 refresh(),我打算讓活著的細胞用「X」來表示
world.nextTurn();
refresh();
}
private void refresh() {剩下的就是找到啟動的地方
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
if (world.getCell(i, j).isAlive()) {
board.setText(i, j, "X");
} else {
board.setText(i, j, " ");
}
}
}
}
public class Enter implements EntryPoint {好啦~ 恭喜你,只剩下 compile 以及找個網頁空間放 .js 檔
public void onModuleLoad() {
RootPanel.get().add(new Monitor());
}
}
你應該要用機械語言;而不是用高階語言撰寫,再透過工具轉換成機械語言中心思想是 Joel 的文章《The Law of Leaky Abstractions》
Returns the same result as System#currentTimeMillis(), but as a double. Because emulated long math is significantly slower than doubles in web mode, this method is to be preferred.因為在瀏覽器上頭,使用 double 處理起來比模擬 long 還要快得多(btw... 為甚麼 Animation 只用了 Duration.currentTimeMillis() 取得時間,而沒有用 Duration.elapsedMillis() 去計算時間差,這我一直想不透 XD)。另一個 run(int duration) 其實還是呼叫 run(int, double),只是自動以當下時間傳給 startTime。
if (animations == null) {這邊要回頭看一下 Animation 的資料結構。講起來有點饒舌。大致上來說,Animation 有一些 static 的 field 跟 method,目的是統一處理系統當中所有的 Animation object(程式碼 point-A)。這裡也可以看到,其實 Animation 裡頭是用 Timer 來實做的。Timer 的細節得先跳過,這裡只要知道看到 animationTime.schedule(int delayMillis) 就表示隔了 delayMillis 個 ms 就會執行 updateAnimations() 的內容,而 updateAnimations() 會呼叫 update()。那麼,勢必有需要好好看一下 update() 的內容:
animations = new ArrayList(); //point-A
animationTimer = new Timer() {
@Override
public void run() {
updateAnimations();
}
};
}
animations.add(this);
private boolean update(double curTime) {裡頭依照不同的狀況,呼叫了 onUpdate(), onStart() 跟 onComplete()。嗯?為甚麼只有 onUpdate() 是 abstract 的呢?因為這兩個到最後還是去呼叫 onUpdate(),progress 的值給 0 表示剛開始、給 1 表示結束。接下來就是最詭異的部份啦,傳給 onUpdate() 的數值,居然還要經過 interpolate() 的計算,這又是為甚麼呢?根據 javadoc 的說法:
boolean finished = curTime >= startTime + duration;
if (started && !finished) {
// Animation is in progress.
double progress = (curTime - startTime) / duration;
onUpdate(interpolate(progress));
return false;
}
if (!started && curTime >= startTime) {
// Start the animation.
started = true;
onStart();
// Intentional fall through to possibly end the animation.
}
if (finished) {
// Animation is complete.
onComplete();
started = false;
running = false;
return true;
}
return false;
}
Interpolate the linear progress into a more natural easing function.看個對照圖可能比較好懂:
An Animation is a continuous event that updates progressively over time at a non-fixed frame rate.哈哈... 根本就是騙人的,哪來什麼動畫 [笑]。簡單地說「Animation 是一個會持續要求 update 動作的物件。而間隔的時間是不固定的。」阿?這什麼鬼?還是用程式碼來說明好了...
import com.google.gwt.animation.client.Animation;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Label;
public class HelloAnimation extends AbsolutePanel{
private Label hello = new Label("Hello Animation");
private final int WIDTH = 800;
private final int HEIGHT = 200;
public HelloAnimation(){
this.setPixelSize(WIDTH, HEIGHT);
this.add(hello); //要先加上去,才有辦法移動
Player player = new Player(this);
player.run(5*1000); //point-A
}
public void playOneFrame(double progress){
this.setWidgetPosition(hello, (int)(WIDTH*progress), HEIGHT/2);
}
}
class Player extends Animation{
HelloAnimation target;
public Player(HelloAnimation t){
target = t;
}
@Override
protected void onUpdate(double progress) {
target.playOneFrame(progress);
}
}