たかふーのブログ このページをアンテナに追加 RSSフィード Twitter

2011-04-09

GWTに触ってみるよ!(4)


過去3回、本当に内容スカスカのGWTネタを投下してきましたが、今度こそソレッぽく動くものを作ってみたいと思います。

という訳で、「Hello, world」に次ぐ基本アプリである「足し算アプリ」を作ってみます。


以前さらっと新規作成したプロジェクトの中身を晒しましたが、Google Pluging for Eclipseウィザードで「Generate GWT project sample code」にチェックを入れていた為、サンプルアプリが含まれた状態で、プロジェクトのモロモロが作成されました。

今回は、そのチェックをOFFのまま作成してみましょう。

作成した結果をtreeコマンドで出力したものがコチラ。

$ tree -a
.
├── .classpath
├── .project
├── .settings
│   ├── com.google.appengine.eclipse.core.prefs
│   ├── com.google.gdt.eclipse.core.prefs
│   └── com.google.gwt.eclipse.core.prefs
├── src
│   ├── META-INF
│   │   └── jdoconfig.xml
│   ├── imai78
│   │   └── gwt
│   │       └── learning
│   └── log4j.properties
└── war
    └── WEB-INF
        ├── appengine-web.xml
        ├── classes
        │   ├── META-INF
        │   │   └── jdoconfig.xml
        │   ├── imai78
        │   │   └── gwt
        │   │       └── learning
        │   └── log4j.properties
        ├── lib
        │   ├── appengine-api-1.0-sdk-1.4.3.jar
        │   ├── appengine-api-labs-1.4.3.jar
        │   ├── appengine-jsr107cache-1.4.3.jar
        │   ├── datanucleus-appengine-1.0.8.final.jar
        │   ├── datanucleus-core-1.1.5.jar
        │   ├── datanucleus-jpa-1.1.5.jar
        │   ├── geronimo-jpa_3.0_spec-1.1.1.jar
        │   ├── geronimo-jta_1.1_spec-1.1.1.jar
        │   ├── gwt-servlet.jar
        │   ├── jdo2-api-2.3-eb.jar
        │   └── jsr107cache-1.1.jar
        ├── logging.properties
        └── web.xml

14 directories, 23 files

非常にさっぱりしてますね。


これに対して、以前作成したプロジェクトを写経するつもりで、可能な限り最低限の加筆修正だけで「足し算アプリ」を作ってみたいと思います。

作成するファイル、更新するファイルは以下の通り。

<PROJECT-ROOT>/src/imai78/gwt/learning/Calc.gwt.xml(追加)
<PROJECT-ROOT>/src/imai78/gwt/learning/client/CalcEntryPoint.java(追加)
<PROJECT-ROOT>/src/imai78/gwt/learning/client/CalcService.java(追加)
<PROJECT-ROOT>/src/imai78/gwt/learning/client/CalcServiceAsync.java(追加)
<PROJECT-ROOT>/src/imai78/gwt/learning/server/CalcServiceImpl.java(追加)
<PROJECT-ROOT>/war/index.html(追加)
<PROJECT-ROOT>/war/WEB-INF/web.xml(更新)

そんなにたくさんないです。


Calc.gwt.xml

ファイルCalc.gwt.xmlは以下のように書きます。

<?xml version="1.0" encoding="UTF-8"?>
<module rename-to='calc'>
  <inherits name='com.google.gwt.user.User'/>
  <inherits name='com.google.gwt.user.theme.standard.Standard'/>
  <entry-point class='imai78.gwt.learning.client.CalcEntryPoint'/>
  <source path='client'/>
  <source path='shared'/>
</module>

このファイルが置かれるパッケージが、「アプリケーションのルート」として扱われるようです。で、このXMLファイルの名前は大文字で始まらないとダメらしいですね。(それで1時間くらい悩まされた><)

基本的にはサンプルのXMLリネームして、EntryPointの実装クラス名を書き換えたくらいです。


CalcEntryPoint.java

このJavaファイルも新規に作成しました。

package imai78.gwt.learning.client;

import com.google.gwt.core.client.EntryPoint;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.DialogBox;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.RootPanel;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;

public class CalcEntryPoint implements EntryPoint {

    private final CalcServiceAsync calcService = GWT.create(CalcService.class);

    public void onModuleLoad() {
        // パーツ定義
        final TextBox xBox = new TextBox();
        final TextBox yBox = new TextBox();
        xBox.setText("1");
        yBox.setText("2");
        Button calcButton = new Button("Calc");
        Button resetButton = new Button("Reset");

        final DialogBox dialog = new DialogBox();
        dialog.setAnimationEnabled(true);
        Button dialogCloseButton = new Button("Close");
        final Label answer = new Label();

        // レイアウト定義
        VerticalPanel dialogVPanel = new VerticalPanel();
        dialogVPanel.add(answer);
        dialogVPanel.add(dialogCloseButton);
        dialog.setWidget(dialogVPanel);

        RootPanel panel = RootPanel.get();
        panel.add(xBox);
        panel.add(yBox);
        panel.add(calcButton);
        panel.add(resetButton);

        // イベント定義
        calcButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                Integer xValue = new Integer(xBox.getText());
                Integer yValue = new Integer(yBox.getText());
                calcService.calc(xValue, yValue, new AsyncCallback<Integer>() {
                    public void onSuccess(Integer result) {
                        answer.setText(result.toString());
                    }
                    public void onFailure(Throwable caught) {
                        System.out.println(caught.toString());
                    }
                });

                dialog.show();
            }
        });
        resetButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                xBox.setText("");
                yBox.setText("");
                answer.setText("");
            }
        });
        dialogCloseButton.addClickHandler(new ClickHandler() {
            public void onClick(ClickEvent event) {
                dialog.hide();
            }
        });
        dialog.hide();
    }
}

このonModuleLoad()というメソッドが、GWTで作成されたWebアプリケーションがブラウザからリクエストを受け取った時に、返信するHTMLを作成します。

クライアントサイドの実装は基本的にこのclientパッケージに入っていないといけない、と今の時点では思い込んでいます。


ちなみにこのEntryPointというインターフェイスをimplementsした実装を作った上で前述のgwt.xmlを定義する事で、GWTアプリケーションのエントリーポイントが作られていくのですが、1つのアプリケーション内に複数作る事もどうやら可能みたいです。

GWT で、複数かつ個別に動作するモジュールを作ったときのメモ - あさとの @drillbits

CalcService.java

これも新規作成です。Javaインターフェイスですね。

package imai78.gwt.learning.client;

import com.google.gwt.user.client.rpc.RemoteService;
import com.google.gwt.user.client.rpc.RemoteServiceRelativePath;

@RemoteServiceRelativePath("plus")
public interface CalcService extends RemoteService {
    Integer calc(int x, int y) throws IllegalArgumentException;
}

サーバサイドで行いたい処理のインターフェイスを定義しています。

RemoteServiceというGWT謹製のインターフェイスをimplementsしています。このインターフェイスを用いる事で、GWT内で定義されたシグネチャRPCの送受信部分に変換してくれるって事みたいだと想像して勝手に納得しておく事にします。


ちなみにこのインターフェイスを定義した時点では、次に示すCalcServiceAsync.javaが存在しない事にお怒りになられるので、エラーが出てしまいます。


CalcServiceAsync.java

これも新規作成のインターフェイスなのですが、このAsyncインターフェイスはGWTの特徴的な存在です。

package imai78.gwt.learning.client;

import com.google.gwt.user.client.rpc.AsyncCallback;

public interface CalcServiceAsync {

    void calc(int x, int y, AsyncCallback<Integer> callback);

}

このインターフェイスは手で作るのではなく、CalcService.javaを作った時に出るエラーに対してCommand+1(WindowsだとCtrl+1でしたっけ?)で補完をお願いすると、CalcService.javaを作った直後にお怒りだったGoogle Plugin for Eclipse閣下が作ってくれてしかもお怒りを鎮めてくださいます。


CalcServiceImpl.java

これが、先に定義したインターフェイスの実装となるものです。

package imai78.gwt.learning.server;

import imai78.gwt.learning.client.CalcService;

import com.google.gwt.user.server.rpc.RemoteServiceServlet;

@SuppressWarnings("serial")
public class CalcServiceImpl extends RemoteServiceServlet implements CalcService {

    public Integer calc(int x, int y) throws IllegalArgumentException {
        return new Integer(x + y);
    }

}

RemoteServiceServletというクラスを継承しております。GWTの重要な何かがここにありそうですね。

その他の部分ですが、"今回は「足し算アプリ」なので足し算をさせています"なんて事をわざわざ書くのもどうかと思われるほどのことしか書いていません><


index.html

「なんだ、HTML書かなくて良い訳じゃないのか、GWTはJavaコードからHTMLとJavaScriptを作ってくれると聞いていたから、作らないで良いと思ってたのに」という感想を抱いたHTMLファイルです。

<!doctype html>
<html>
  <head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
    <title>index</title>
    <script type="text/javascript" language="javascript"
            src="calc/calc.nocache.js"></script>
  </head>

  <body>
    <iframe src="javascript:''" id="__gwt_historyFrame" tabIndex='-1' style="position:absolute;width:0;height:0;border:0"></iframe>

  </body>
</html>

Google Plugin for EclipseのPackage Explorer上でNewを選んだ際に出てくるウィザードを使って「Google Web Toolkit用のHTML Page」を作成すると、こういったHTMLを出力してくれます。今回の場合は生成されたファイルをそのまま放っておけば良いです。

ここって、例えばAjaxな機能をバッサリ捨て去った場合、これはHTMLではなくXMLとかそういう類のものでも良いのかな?


web.xml

このファイルに限り「編集」ですね。

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE web-app
    PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
    "http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
    <!-- TODO: Add <servlet> tags for each servlet here. -->
    <servlet>
        <servlet-name>calcServlet</servlet-name>
        <servlet-class>imai78.gwt.learning.server.CalcServiceImpl</servlet-class>
    </servlet>
    <!-- TODO: Add <servlet-mapping> tags for each <servlet> here. -->
    <servlet-mapping>
        <servlet-name>calcServlet</servlet-name>
        <url-pattern>/calc/plus</url-pattern>
    </servlet-mapping>
    <!-- TODO: Optionally add a <welcome-file-list> tag to display a welcome file. -->
    <welcome-file-list>
        <welcome-file>calc.html</welcome-file>
    </welcome-file-list>
</web-app>

servletservlet-mapping、welcome-file-listをそれぞれ書き足しております。

CalcServiceImplをサーブレットとして登録している事から、サーバサイドの実装はやっぱりCalcServiceImplがエントリーポイントになるようです。本当にJavaクライアントの実装をしてるんだなー、と思わされる一瞬です。


動かす

上記を仕込んだ上で、このプロジェクトを「Web Application(Googleマーク付き)」で実行すると動かせる状態になります。

Google Plugin for Eclipseを使う場合、「Development Mode」というビューに出てくるURLをダブルクリックすれば自動的にブラウザで開いてくれます。(ちなみにURLは「http://127.0.0.1:8888/calc.html?gwt.codesvr=127.0.0.1:9997」でした)

f:id:imai78:20110409045254p:image:medium

このような結果で、足し算もちゃんできました。

f:id:imai78:20110409045255p:image:medium

オッケーな感じですね!


まとめ

GWTで簡単なものを作ってみた訳ですが、Google App Engineの使用を前提にしている為か、ほとんど手を動かす事もなく動かせるWebアプリケーションが出来上がりました。

GWTのプログラミングは「これ本当にWebなの?」とか「何が嬉しくてこんなプログラミングをさせようと思ったの?」とか感じますが、これは多分クライアントサイドを「SwingSWTのようなGUIアプリケーション」としてプログラミングさせる事で実装者のイメージをそのまま活かせるようにしたかったのかなー?なんて考えると、ちょっと納得できるものがあります。

「実際、JavaScriptで似たような事は書く訳ですし、じゃあそのままJavaで書けば良いじゃない」という事なんでしょうね、きっと。設定ファイルも生Servletやstruts-config.xmlに比べれば全然記述量が少なくて済みますし。

「こういうものだ」という風に考えれば、それほどストレスに感じる事はありませんでした。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/imai78/20110409/1302360195