Java開発者のためのAjax: Google Web Toolkitを探る

1つのJavaコードベースからAjaxアプリケーションを開発する

最近リリースされた包括的なAPI兼ツール・セットであるGWT(Google Web Toolkit)を利用すると、動的なWebアプリケーションを、ほとんどJava™コードのみで作成することができます。この記事では著者のPhilip McCarthyが、人気の『Java開発者のためのAjax』シリーズに戻り、GWTでは何ができるのかを解説します。そして、これが皆さんにとって適切なものかどうかを判断するためのヒントを提供します。(注記: この記事で解説するソースコードを含んだZIPファイルの更新版をダウンロードできるようになりました。)

Philip McCarthy (philmccarthy@gmail.com), Software development consultant, Independent

Philip McCarthyは、JavaとWeb技術を専門とするソフトウェア開発のコンサルタントです。最近は、Hewlett Packard LabsやOrangeでのデジタル・メディアやテレコム関係のプロジェクトに関係していました。現在は、ロンドン市の会計ソフトウェアに関わっています。



2006年 6月 27日

GWT(参考文献を見てください)は、Webアプリケーションの開発に対して、変わった手法をとっています。GWTでは、通常見られるようにクライアントサイドとサーバーサイドのコードベースを分離するのではなく、コンポーネント・ベースのGUIを作成するためのJava APIを用意し、そうしたGUIをコンパイルしてユーザーのWebブラウザーで表示するようにしています。GWTを使った開発は、通常のWebアプリケーション開発よりもSwingやSWTによる開発にずっと近く、またHTTPプロトコルやHTML DOMモデルから離れるための抽象化が行われています。実際のところ、アプリケーションがWebブラウザーで表示されるということ自体が、まるで偶然のように思えるのです。

GWTはこうした離れ業を、コード生成によって実現しています。つまりGWTのコンパイラーが、クライアントサイドのJavaコードからJavaScriptを生成するのです。GWTは、GWT自体が提供するAPIの他に、java.langパッケージとjava.utilパッケージのサブセットをサポートしています。コンパイルされたGWTアプリケーションは、HTMLやXML、それにJavaScriptなどの断片から構成されていますが、こうした要素は解読しようとしても極めて難しく、コンパイルされたアプリケーションはブラック・ボックス、つまりJavaバイトコードのGWT版、と考えた方が得策です。

この記事では、リモートのWeb APIから気象情報をフェッチしてブラウザーに表示する、単純なGWTアプリケーションの作成手順を説明します。その中で、GWTの機能について可能な限り多く取り上げるようにし、また皆さんが突き当たりそうな潜在的な問題についても触れることにします。

単純なものから始める

リスト1は、GWTを使って作成できる最も単純なアプリケーションのJavaソースコードを示しています。

リスト1. 最も単純なGWTの例
public class Simple implements EntryPoint {

   public void onModuleLoad() {
     final Button button = new Button("Say 'Hello'");

     button.addClickListener(new ClickListener() {
        public void onClick(Widget sender) {
        Window.alert("Hello World!");
        }
     });

     RootPanel.get().add(button);
   }
}

これは、SwingやAWT、あるいはSWTを使って書かれたGUIコードによく似ています。皆さんが想像される通り、リスト1は、クリックすると「Hello World!」メッセージを表示するボタンを作成します。このボタンは、HTMLページのボディーを囲むGWTラッパーであるRootPanelに追加されます。図1は、GWTシェルの内部で実行している、このアプリケーションの動作を示しています。このシェルはデバッグ用のホスト環境であり、GWT SDKの中に含まれている単純なWebブラウザーを利用しています。

図1. 最も単純なGWTの例が動作しているところ
最も単純なGWTの例が動作しているところ

Weather Reporterアプリケーションを構築する

ここでは、GWTを使って単純なWeather Reporter(気象レポート)アプリケーションを作成します。このアプリケーションのGUIはユーザーに対して、ZIPコードを入力するための入力ボックスと、温度表示にCelsius(摂氏)を使うかFahrenheit(華氏)を使うかの選択を表示します。ユーザーがSubmitボタンをクリックすると、アプリケーションはYahoo!の無料Weather APIを使って、選択された場所に関するRSSフォーマットのレポートを取得します。そしてこの文書のHTML部分が抽出され、ユーザーに表示されます。

GWTアプリケーションは『モジュール』としてパッケージされ、ある特定な構造に従っている必要があります。module-name.gwt.xmlという名前のコンフィギュレーション・ファイルは、クラスを定義します。このクラスはアプリケーションのエントリー・ポイントとして動作し、また他のGWTモジュールからリソースを継承する必要があるかどうかを示します。このコンフィギュレーション・ファイルは、アプリケーションのソース・パッケージ構造の中に置く必要があります。このファイルを置くレベルとしては、クライアントサイドJavaコードのすべてが置かれているclientという名前のパッケージと、プロジェクトのWebリソース(画像やCSS、HTMLなど)を含んだpublicという名前のディレクトリーと同じレベルです。最後に、publicディレクトリーは、モジュールの修飾名を含むmetaタグの付いたHTMLファイルを含んでいる必要があります。GWTのランタイムJavaScriptライブラリーは、このファイルを使ってアプリケーションを初期化します。

GWTのapplicationCreatorは、エントリー・ポイント・クラスの名前を与えると、この基本的な構造を生成してくれます。ですからapplicationCreator developerworks.gwt.weather.client.Weatherを呼ぶと、Weather Reporterアプリケーションの開始点として使用できるプロジェクト・アウトラインが生成されます。このアプリケーションに対するソース・ダウンロードにはAntビルドファイルが含まれており、このファイルの中には、この構造に準拠するGWTプロジェクトを扱うために便利なターゲットが幾つか含まれています(ダウンロード・セクションを見てください)。

基本的なGUIを開発する

まず、アプリケーションのユーザー・インターフェース・ウィジェットの基本的なレイアウトを開発します(振る舞いは何も追加しません)。GWT UIで描画できるほとんど全てのものに対するスーパークラスは、Widgetクラスです。Widgetは常にPanelの中に含まれていますが、PanelそのものもWidgetなので、ネストすることができます。様々なタイプのパネルに、様々なレイアウト振る舞いが用意されています。ですからGWTのPanelの役割は、AWT/SwingでのLayoutやXULでのBoxの役割と似ています。

すべてのウィジェットやパネルは、最終的には、それらをホストするWebページに付加する必要があります。リスト1で見た通り、これらはRootPanelに直接付加することができます。あるいは、RootPanelを使って、それらのIDやクラス名で特定されるHTML要素への参照を取得することもできます。ここでは、input-containerという名前とoutput-containerという名前の、2つの別々なHTML DIV要素を使います。前者はWeather Reporterに対するUIコントロールを含み、後者は気象情報そのものを表示します。

リスト2は、基本的なレイアウトを設定するために必要なコードを示しています。これには特に説明は不用でしょう。HTMLウィジェットは、HTMLマークアップに対する単なるコンテナーです。Yahoo!のweather feedからのHTML出力は、ここに表示されます。これらのコードはすべて、EntryPointインターフェースが提供する、WeatherクラスのonModuleLoad() メソッドの中に入ります。このメソッドは、Weatherモジュールを埋め込んだWebページがクライアントのWebブラウザーにロードされると呼び出されます。

リスト2. Weather Reporter用のレイアウト・コード
public void onModuleLoad() {

   HorizontalPanel inputPanel = new HorizontalPanel();

   // Align child widgets along middle of panel
   inputPanel.setVerticalAlignment(HasVerticalAlignment.ALIGN_MIDDLE);

   Label lbl = new Label("5-digit zipcode: ");
   inputPanel.add(lbl);

   TextBox txBox = new TextBox();
   txBox.setVisibleLength(20);

   inputPanel.add(txBox);

   // Create radio button group to select units in C or F
   Panel radioPanel = new VerticalPanel();

   RadioButton ucRadio = new RadioButton("units", "Celsius");
   RadioButton ufRadio = new RadioButton("units", "Fahrenheit");

   // Default to Celsius
   ucRadio.setChecked(true);

   radioPanel.add(ucRadio);
   radioPanel.add(ufRadio);

   // Add radio buttons panel to inputs
   inputPanel.add(radioPanel);

   // Create Submit button
   Button btn = new Button("Submit");    

   // Add button to inputs, aligned to bottom
   inputPanel.add(btn);
   inputPanel.setCellVerticalAlignment(btn,
      HasVerticalAlignment.ALIGN_BOTTOM);

   RootPanel.get("input-container").add(inputPanel);

   // Create widget for HTML output
   HTML weatherHtml = new HTML();

   RootPanel.get("output-container").add(weatherHtml);
}

図2は、GWTシェルの中で描画されたレイアウトを示しています。

図2. 基本的なGUIレイアウト
基本的なGUIレイアウト

CSSでスタイルを追加する

描画されたWebページでは、あまりも平凡なので、CSSでスタイル規則を追加した方がよいでしょう。GWTアプリケーションをスタイル化するためには幾つかの方法があります。まず、ウィジェットはそれぞれデフォルトで、『project-widget』という形式のCSSクラス名を持っています。例えばgwt-Buttonと gwt-RadioButtonは、GWTウィジェットの持つコア・クラス名のうちの2つです。パネルは通常、複雑にネストされたテーブルとして実装され、デフォルトのクラス名を持ちません。

ウィジェット・タイプそれぞれにクラス名、というデフォルトの方式のおかげで、アプリケーション全体に渡って簡単かつ統一的にウィジェットをスタイル化することができます。当然ながら通常のCSSセレクター・ルールが適用されるため、これを利用すると、コンテキストに応じて異なるスタイルを同じウィジェット・タイプに適用することができます。あるいはもっと柔軟に、setStyleName() メソッドやaddStyleName() メソッドを呼ぶことによって、ウィジェットのデフォルト・クラス名を臨機応変に置き換えたり、補強したりすることもできます。

リスト3は、こうした方法を組み合わせ、Weather Reporterアプリケーションの入力パネルにスタイルを適用したものです。weather-input-panelというクラス名は、inputPanel.setStyleName("weather-input-panel") へのコールによって、Weather.javaの中で作成されます。

リスト3. Weather Reporterアプリケーションの入力パネルにCSSスタイルを適用する
/* Style the input panel itself */
.weather-input-panel {
   background-color: #AACCFF;
   border: 2px solid #3366CC;
   font-weight: bold;
}

/* Apply padding to every element within the input panel */
.weather-input-panel * {
   padding: 3px;
}

/* Override the default button style */
.gwt-Button {
   background-color: #3366CC;
   color: white;
   font-weight: bold;
   border: 1px solid #AACCFF;
}

/* Apply a hover effect to the button */
.gwt-Button:hover {
   background-color: #FF0084;
}

図3は、こうしたスタイルを適用したアプリケーションを示しています。

図3. スタイルを適用した入力パネル
スタイルを適用した入力パネル

クライアントサイドの振る舞いを追加する

アプリケーションの基本的なレイアウトとスタイル化が終わったので、クライアントサイドの振る舞いを追加することにしましょう。GWTでは、おなじみのリスナー・パターンを使ってイベント処理を行います。GWTには、マウス・イベントやキーボード・イベント、変更イベントなどに対するListenerインターフェースが用意され、さらには幾つかのアダプター・クラスやヘルパー・クラスまで用意されています。

一般的にイベント・リスナーの追加は、Swingプログラマーにはおなじみの匿名内部クラス・イディオムを使って行います。しかしGWTでは、どのListenerメソッドでも、最初のパラメーターはイベントのsenderです。これは通常、ユーザーが触ったばかりのウィジェットです。これはつまり、同じListenerインスタンスを、必要なだけ複数のウィジェットに付加できるということを意味します。そしてsenderパラメーターを使えば、どのウィジェットがイベントを発行したのかが判断できるのです。

リスト4は、Weather Reporterアプリケーションでの、2つのイベント・リスナーの実装を示しています。Submitボタンにはクリック・ハンドラーが追加され、TextBoxにはキーハンドラーが追加されています。TextBoxに焦点がある時にSubmitボタンをクリックするかEnterキーを押すと、関連のハンドラーによってプライベートのvalidateAndSubmit() メソッドが呼ばれます。リスト4のコードの他に、txBoxとucRadioがWeatherクラスのインスタンス・メンバーになっており、検証メソッドからアクセスできるようになっています。

リスト4. クライアントサイドの振る舞いを追加する
// Create Submit button, with click listener inner class attached
Button btn = new Button("Submit", new ClickListener() {

   public void onClick(Widget sender) {
      validateAndSubmit();
   }
});

// For usability, also submit data when the user hits Enter 
// when the textbox has focus
txBox.addKeyboardListener(new KeyboardListenerAdapter(){

   public void onKeyPress(Widget sender, char keyCode, int modifiers) {

      // Check for Enter key
      if ((keyCode == 13) && (modifiers == 0)) {
         validateAndSubmit();
      }        
   }      
});

リスト5は、validateAndSubmit() メソッドの実装を示しています。これはごく単純であり、検証ロジックをカプセル化したZipCodeValidatorクラスに依存しています。もしユーザーが、有効な5桁のZIPコードを入力しなかった場合には、validateAndSubmit() はエラー・メッセージを警告ボックス(GWTの世界ではWindow.alert() へのコールとして表現されます)に表示します。ZIPコードが有効な場合は、そのZIPコードと、ユーザーが選択したCelsiusあるいはFahrenheitいずれかでの単位がfetchWeatherHtml() に渡されます。これについては後ほど触れることにします。

リスト5. validateAndSubmitロジック
private void validateAndSubmit() {

   // Trim whitespace from input
   String zip = txBox.getText().trim();

   if (!zipValidator.isValid(zip)) {
     Window.alert("Zip-code must have 5 digits");
     return;
   }

   // Disable the TextBox
   txBox.setEnabled(false);

   // Get choice of celsius/fahrenheit
   boolean celsius = ucRadio.isChecked();
   fetchWeatherHtml(zip, celsius);
}

GWTシェルでのクライアントサイドのデバッグ

ここで少し寄り道をすることにします。GWTシェルにはJVMフックがあり、これを利用するとJava IDEの中でクライアントサイドのコードをデバッグできることを説明しましょう。つまりWeb UIと対話動作することによって、クライアント上で実行されるJavaScriptに対応したJavaコード・ステップをたどることができるのです。生成されたJavaScriptをクライアントサイドでデバッグすることは基本的に無理なことを考えると、これは重要な機能です。

com.google.gwt.dev.GWTShellクラスを介してGWTシェルを起動するようにEclipseのデバッグ・タスクをコンフィギュレーションするのは簡単です。図4は、Submitボタンがクリックされた後、validateAndSubmit() メソッドのブレークポイントでEclipseが停止したところを示しています。

図4. EclipseでクライアントサイドのGWTコードをデバッグする
EclipseでクライアントサイドのGWTコードをデバッグする

サーバーサイドのコンポーネントと通信する

これでWeather Reporterアプリケーションは、ユーザー入力を収集し、検証できるようになりました。次のステップは、サーバーからデータをフェッチすることです。通常のAjax開発では、このためにはJavaScriptから直接サーバーサイドのリソースを呼び、JSON(JavaScript Object Notation)あるいはXMLとしてエンコードされた戻りデータを受信します。しかしGWTでは、この通信プロセスは、独自のRPC(remote procedure call)機構の背後に抽象化されています。

GWTの用語では、クライアント・コードは、Webサーバー上で実行している『サービス』と通信します。こうしたサービスを公開するために使われているRPC機構は、Java RMIが使っている方式と似ています。これはつまり、そのサービスに関するサーバーサイドの実装と、幾つかのインターフェースを書きさえすればよい、ということを意味します。クラアント・スタブと、サーバーサイドのスケルトン・プロキシーに関しては、コード生成とリフレクション(reflection)が面倒を見てくれるのです。

従って最初のステップは、Weather Reporterサービスに対するインターフェースを定義することです。このインターフェースはGWTのRemoteServiceインターフェースを拡張する必要があり、またGWTクライアント・コードに公開すべきサービス・メソッドの署名を含んでいます。GWTでのRPCコールはJavaScriptコードとJavaコードの間にあるため、GWTでは、言語の境目にまたがって引数や戻り値を調停するためのオブジェクト・シリアル化機構を採用しています(どんなものが使えるかについては、囲み記事「シリアライズ可能型」を見てください)

シリアライズ可能型

GWTでシリアライズ可能な型としては、次のようなものが挙げられます。

  • 基本型(intなど)と、基本ラッパー・クラス(Integerなど)はシリアライズ可能です。
  • StringとDateはシリアライズ可能です。
  • シリアライズ可能型の配列は、それ自体がシリアライズ可能です。
  • ユーザー定義のクラスは、その一時メンバーでない全メンバーがシリアライズ可能であり、それらがGWTのIsSerializableインターフェースを実装している場合は、シリアライズ可能です。
  • Collectionクラスは、その中に含まれるシリアライズ可能型を記述したJavadoc注釈と合わせて使うことができます。

いずれにせよ、クライアント・コードはGWTが実装するJavaクラスの小さなサブセットに限定されているため、こうしたシリアライズ可能型は非常に多くのものを網羅しています。

サービス・インターフェースが定義できたので、次のステップは、このインターフェースをGWTのRemoteServiceServletクラスを拡張(継承)するクラスの中で実装することです。名前からも分かる通り、これはJava言語のHttpServletの特別なものです。ですからどんなサーブレット・コンテナーでもホストすることができます。

ここで、GWTの変わった点として挙げておきますが、サービスのリモート・インターフェースは、アプリケーションのclientパッケージの中にある必要があります。これは、リモート・インターフェースがJavaScript生成プロセスの中に組み込まれている必要があるためです。しかしサーバーサイドの実装クラスはリモート・インターフェースを参照するため、Javaのコンパイル時依存性が、サーバーサイドとクライアント・コードの間に存在することになります。これに対する私のソリューションは、リモート・インターフェースをclientのcommonサブパッケージの中に入れてしまうことです。そうしておいて、commonをJavaビルドに含め、clientパッケージの他の部分は入れないようにするのです。これによって、JavaScriptに変換されるだけのクライアント・コードからクラス・ファイルが生成されるのを防ぐことができます。もっと優雅な方法としては、パッケージ構造をクライアントサイドのコードとサーバーサイドのコードという2つのソース・ディレクトリーに分割し、共通のクラスを両方のディレクトリーにコピーするようにします。

リスト6は、Weather Reporterアプリケーションで使用するリモート・サービス・インターフェース、WeatherServiceを示しています。このインターフェースは、ZIPコードとCelsius/Fahrenheitフラグを入力として受け取り、HTMLによる気象の記述を含んだStringを返します。またリスト6はYahooWeatherServiceImplの概要も示しています。YahooWeatherServiceImplは、与えられたZIPコードに対する気象のRSSフィードをYahoo!のweather APIを使って取得し、そこからHTML記述を抽出します。

リスト6. WeatherServiceリモート・インターフェースと、その部分的な実装
public interface WeatherService extends RemoteService {

   /**
    * Return HTML description of weather
    * @param zip zipcode to fetch weather for
    * @param isCelsius true to fetch temperatures in celsius, 
    * false for fahrenheit
    * @return HTML description of weather for zipcode area
    */
   public String getWeatherHtml(String zip, boolean isCelsius) 
      throws WeatherException;
} 

public class YahooWeatherServiceImpl extends RemoteServiceServlet
   implements WeatherService {

   /**
    * Return HTML description of weather
    * @param zip zipcode to fetch weather for
    * @param isCelsius true to fetch temperatures in celsius, 
    * false for fahrenheit
    * @return HTML description of weather for zipcode area
    */
   public String getWeatherHtml(String zip, boolean isCelsius) 
      throws WeatherException {

     // Clever programming goes here
   }
}

ここから、標準的なRMIの手法とは異なってきます。JavaScriptからのAjaxコールは非同期なので、サービスを呼び出すためにクライアント・コードが使用する非同期インターフェースを定義するために、少し余計な作業が必要です。この非同期インターフェースのメソッド・シグニチャーはリモート・インターフェースのメソッド・シグニチャーとは異なるため、GWTは魔法の名前一致検出(Magical Coincidental Naming)に依存しています。つまり、非同期インターフェースとリモート・インターフェースの間にはコンパイル時の静的な関係は存在しないのですが、GWTはその関係を、命名規則から判断するのです。リスト7は、WeatherServiceに対する非同期インターフェースを示しています。

リスト7. WeatherServiceに対する非同期インターフェース
public interface WeatherServiceAsync {

   /**
    * Fetch HTML description of weather, pass to callback
    * @param zip zipcode to fetch weather for
    * @param isCelsius true to fetch temperatures in celsius,
    * false for fahrenheit
    * @param callback Weather HTML will be passed to this callback handler
    */
   public void getWeatherHtml(String zip, boolean isCelsius, 
      AsyncCallback callback);
}

見て分かると思いますが、大まかな考え方を説明すると、MyServiceAsyncというインターフェースを作成し、各メソッド・シグニチャーに相手を用意することによって、戻り型を削除し、AsyncCallbackという型のパラメーターを追加するのです。また非同期インターフェースは、リモート・インターフェースと同じパッケージの中にある必要があります。AsyncCallbackクラスには、onSuccess()とonFailure() という2つのメソッドがあります。サービスへのコールが成功した場合には、サービス・コールの戻り値を付けてonSuccess() が呼び出されます。もしリモート・コールが失敗した場合はonFailure() が呼び出され、サービスが生成した(失敗の原因を表現する)Throwableが渡されます。


クライアントからサービスを呼び出す

WeatherServiceと、その非同期インターフェースが用意できたので、サービスを呼び出してレスポンスを処理するようにWeather Reporterクライアントを変更することができます。最初のステップは、単なる型通りの設定コードです。つまり、GWT.create(WeatherService.class) を呼び、そこから戻るオブジェクトをダウンキャストすることによって、使用するWeatherクライアントに対するWeatherServiceAsyncのインスタンスを作成します。次に、setServiceEntryPoint() が呼ばれるように、ServiceDefTargetにWeatherServiceAsyncをキャストする必要があります。setServiceEntryPoint() は、対応するリモート・サービス実装がデプロイされているURLにあるWeatherServiceAsyncスタブを指します。これは実質的に、コンパイル時にハードコード化されることに注意してください。このコードはWebブラウザーの中にデプロイされるJavaScriptになるため、実行時にこのURLをプロパティー・ファイルから参照する方法はありません。そのため当然ながら、コンパイルされたGWT Webアプリケーションの移植性は制限されます。

リスト8は、WeatherServiceAsyncオブジェクトの設定と、その次に、先ほど触れたfetchWeatherHtm() の実装を示しています(クライアントサイドの振る舞いを追加するを見てください)。

リスト8. RPCを使ってリモート・サービスを呼び出す
// Statically configure RPC service
private static WeatherServiceAsync ws = 
   (WeatherServiceAsync) GWT.create(WeatherService.class);
static {
   ((ServiceDefTarget) ws).setServiceEntryPoint("ws");
}

/**
 * Asynchronously call the weather service and display results
 */
private void fetchWeatherHtml(String zip, boolean isCelsius) {

   // Hide existing weather report
   hideHtml();

   // Call remote service and define callback behavior
   ws.getWeatherHtml(zip, isCelsius, new AsyncCallback() {
      public void onSuccess(Object result) {

         String html = (String) result;

         // Show new weather report
         displayHtml(html);
      }

      public void onFailure(Throwable caught) {
         Window.alert("Error: " + caught.getMessage());
         txBox.setEnabled(true);
       }
   });
}

このサービスのgetWeatherHtml() への実際のコールは、単純に実装できます。匿名コールバック・ハンドラー・クラスが、単純にサーバーのレスポンスを表示メソッドに渡すだけです。

図5は、このアプリケーションの動作の様子です。ここではYahoo!のweather APIからフェッチした気象レポートを表示しています。

図5. Yahoo!からフェッチしたレポートをWeather Reporterアプリケーションが表示している
Yahoo!からフェッチしたレポートをWeather Reporterアプリケーションが表示している

サーバーサイドでの検証の必要性

GWTでは、クライアントサイドとサーバーサイドのコードを1つにまとめていますが、これは本質的に危険です。GWTによる抽象化でクライアントとサーバーとの区別が見えない状態で全てをJava言語でプログラムするため、クライアントサイド・コードは実行時に信頼できるものだ、と誤解しがちです。これは大きな間違いです。Webブラウザーの中で実行するコードは全て、悪意のユーザーによる改ざんや完全バイパス攻撃を受ける可能性があります。GWTは上位レベルの抽象化によって、ある程度問題を軽減していますが、2次的な攻撃点、つまりGWTクライアントとサービスとの間で発生するHTTPトラフィック、という問題点は残っています。

例えば私が、Weather Reporterアプリケーションの弱点を狙う攻撃者だとしましょう。図6は、MicrosoftのFiddlerツールが、Weather Reporterクライアントから(サーバー上で実行している)WeatherServiceへのリクエストをインターセプトしている様子を示しています。いったんインターセプトできると、Fiddlerを使ってリクエストの任意部分を変更できてしまうのです。ハイライトされたテキストは、私が規定したZIPコードがリクエストのどこにエンコードされているかを発見できたことを示しています。こうなれば、これを例えば「10001」から「XXXXX」など、好き勝手に変えることができます。

図6. Fiddlerを使ってクライアントサイドの検証をバイパスする
Fiddlerを使ってクライアントサイドの検証をバイパスする

さて、YahooWeatherServiceImplの中にある無邪気なサーバーサイド・コードが、ZIPコードに対してInteger.parseInt() を呼ぶとしましょう。本来であればZIPコードは、WeatherのvalidateAndSubmit() メソッドの中に組み込まれた検証チェックをパスしたはずです。しかし今見たように、このチェックは無効にされ、今やNumberFormatExceptionが投げられてしまいます。

この場合は何もひどいことは起こらず、単にクライアントのエラー・メッセージが攻撃者に見えてしまうだけです。しかし潜在的には、もっと機密性の高いデータを扱うGWTアプリケーションに対して大量の攻撃が仕掛けられる危険性があります。例えばZIPコードではなく、注文追跡アプリケーションでのID番号だった場合を考えてみてください。この値をインターセプトし、変更することによって、他の顧客に関する重要な金融データが公開されてしまう可能性があります。データベース・クエリーで値を使用するところでは、どこでも同じ方法でSQL注入攻撃を許してしまう可能性があります。

これまでAjaxアプリケーションを扱ったことのある人には、これは何も難しい話ではありません。単純に、どんな入力値であっても、サーバーサイドで再検証することで二重チェックする必要があるということです。重要なことは、GWTアプリケーションの中で自分が書くJavaコードの一部は、実行時には基本的に信頼できないのだと頭に置いておくことです。しかしGWTの持つ、この問題には、明るい兆しがあるのです。Weather Reporterアプリケーションに関して言えば、私はクライアントで使用するZipCodeValidatorを既に書きました。ですから単純にこれをclient.commonパッケージに移動し、同じ検証をサーバーサイドでも再利用すればよいのです。リスト9は、このチェックをYahooWeatherServiceImplの中に組み込んだものです。

リスト9. YahooWeatherServiceImplの中に組み込んだZipCodeValidator
public String getWeatherHtml(String zip, boolean isCelsius) 
       throws WeatherException {

   if (!new ZipCodeValidator().isValid(zip)) {
      log.warn("Invalid zipcode: "+zip);
      throw new WeatherException("Zip-code must have 5 digits");
   }

JSNIを使ってネイティブのJavaScriptを呼ぶ

Webアプリケーション開発では、視覚効果ライブラリーが一般的になりつつあります。その効果としては、ユーザーとの対話動作に細かな手がかりを与えるものや、単に見かけを良くするものまで、様々なものがあります。ここでは、Weather Reporterアプリケーションを少しばかり華やかにしましょう。この種の機能はGWTには用意されていませんが、JSNI(JavaScript Native Interface)が答えを持っているのです。JSNIを使用すると、GWTのクライアントJavaコードから直接JavaScriptをコールできるようになります。つまり、Scriptaculousライブラリー(参考文献を見てください)や、Yahoo!のUser Interfaceライブラリーなどの視覚効果を利用できるようになるのです。

JSNIは、Java言語のnativeキーワードと、特別なコメント・ブロックに埋め込まれたJavaScript、という組み合わせを巧妙に使っています。これは例で示した方が分かりやすいでしょう。リスト10は、あるElementに対して、与えられたScriptaculous効果を呼び出すメソッドを示しています。

リスト10. JSNIを使ってScriptaculous効果を呼び出す
/**
 * Publishes HTML to the weather display pane
 */
private void displayHtml(String html) {
   weatherHtml.setHTML(html);
   applyEffect(weatherHtml.getElement(), "Appear");
}

/**
 * Applies a Scriptaculous effect to an element
 * @param element The element to reveal
 */
private native void applyEffect(Element element, String effectName) /*-{

   // Trigger named Scriptaculous effect
   $wnd.Effect[effectName](element);
}-*/;

コンパイラーには、private native void applyEffect(Element element, String effectName);しか見ないので、これは完璧に有効なJavaコードです。GWTはコメント・ブロックの内容を構文解析し、逐語的にJavaScriptを出力します。GWTには、ウィンドウ・オブジェクトと文書オブジェクトを参照するための、$wnd変数と$doc変数が用意されています。ここでは、単純に最上位レベルのScriptaculous Effectオブジェクトにアクセスし、JavaScriptの大括弧オブジェクト・アクセサー構文を使って、呼び出し側が定義する名前付きファンクションを呼び出しています。このElementの型は、GWTが提供する「magic」型であり、Widgetの基礎となっているHTML DOM要素をJavaとJavaScriptの両方のコードで表現します。Stringは、JSNIを介してJavaコードとJavaScriptとの間で透明に渡すことのできる数少ない型の1つです。

これで、サーバーからデータが返ってくると格好良くフェード・インする気象レポートができました。あとは仕上げとして、視覚効果が終了したらZIPコードTextBoxを再度イネーブルにするだけです。Scriptaculousは、非同期コールバック機構を使うことで、視覚効果のライフサイクルをリスナーに通知しています。ここからは、JavaScriptにGWTクライアントJavaコードをコールバックさせる必要があるため、話が少し複雑になります。JavaScriptでは、どんなファンクションでも、好きなだけ引数を付けて呼び出せるため、Javaスタイルのオーバーローディングは存在しません。これはつまりJSNIでは、起こり得るオーバーロードの曖昧さを解消するために、面倒な構文を使ってJavaメソッドを参照しなければならない、ということを意味します。GWTのドキュメンテーションでは、この構文を次のように記述しています。

[instance-expr.]@class-name::method-name(param-signature)(arguments)

静的メソッドはオブジェクト参照を必要とせずに呼び出されるので、instance-expr.の部分はオプションです。この場合も、例を使って説明した方が分かりやすいでしょう。リスト11を見てください。

リスト11. JSNIを使ってJavaコードの中へコールバックする
/**
 * Applies a Scriptaculous effect to an element
 * @param element The element to reveal
 */
private native void applyEffect(Element element, String effectName) /*-{

  // Keep reference to self for use inside closure
  var weather = this;

  // Trigger named Scriptaculous effect
  $wnd.Effect[effectName](element, { 
     afterFinish : function () {

     // Make call back to Weather object
     weather.@developerworks.gwt.weather.client.Weather::effectFinished()();
     } 
  });
}-*/;

/**
 * Callback triggered when a Scriptaculous effect finishes.
 * Re-enables the input textbox.
 */
private void effectFinished() {
  this.txBox.setEnabled(true);
  this.txBox.setFocus(true);
}

applyEffect() メソッドは、追加のafterFinish引数をScriptaculousに渡すように変更されています。afterFinishの値は、視覚効果が終了した時に呼び出される匿名ファンクションです。これはGWTのイベント・ハンドラーが使用している匿名内部クラス・イディオムと少し似ています。Javaコードへの実際のコールバック手順としては、コール呼び出しの対象となるWeatherオブジェクトのインスタンスを規定し、次にWeatherクラスの完全修飾名、そして次に、呼び出すべきファンクションを規定します。最初の、中身が空の括弧の対は、何も引数を取らないeffectFinished() という名前のメソッドを呼ぼうとしていることを示しています。2番目の括弧の対はファンクションを規定しています。

ここで面白いことは、weatherというローカル変数がthis参照のコピーを保持していることです。JavaScriptコールの意味体系の動作方式上、afterFinishファンクション内部のthis変数は、(Scriptaculousがファンクション・コールを行うので)実際はScriptaculousオブジェクトなのです。閉包(closure)の外でthisをコピーすることは、単純な回避策です。

JSNIの機能を幾つか説明できたところで、ScriptaculousをGWTに統合するためには、Scriptaculousの視覚効果機能をカスタムのGWTウィジェットとしてラップする方がずっと賢明だということを指摘しておきましょう。実はこの方法こそ、Alexei SokolovがGWT Component Libraryの中で行ったことなのです(参考文献を見てください)。

これでWeather Reporterアプリケーションは終了しました。次は、GWTによるWeb開発の長所短所を少し説明することにしましょう。


なぜGWTを使うのか

皆さんの想像とは逆に、GWTアプリケーションは、驚くほど非Web風です。基本的にGWTは、軽量GUIアプリケーション用のランタイム環境としてブラウザーを利用しています。その結果、通常のWebアプリケーションよりも、MorfikやOpenLaszlo、さらにはFlasでの開発にずっと近いのです。従ってGWTは、1つのページ上に表現力豊富なAjax GUIとして存在するWebアプリケーションに最適です。これはつまりGoogleが最近ベータ・リリースしたカレンダーや表計算アプリケーションの特徴でもありますが、それは偶然ではないでしょう。これらは素晴らしいアプリケーションですが、この手法で全てのビジネス・シナリオを解決できるわけではありません。Webアプリケーションの大部分はページ中心のモデルに非常に適しており、Ajaxによって、必要な場合には高度な対話機構を採り入れることができます。しかしGWTは、通常のページ中心のアプリケーションとは、あまり相性が良くありません。GWTウィジェットを通常のHTMLフォーム入力と組み合わせることは可能ですが、GWTウィジェットの状態は、ページの他の部分からは隔離されています。例えば、通常のフォームの一部としてのGWTのTreeウィジェットから、選択された値を送信する単純な方法がありません。

ライセンス

GWTのランタイム・ライブラリーは、Apache License 2.0の条件でライセンスされており、GWTを自由に使って商用アプリケーションを作成することができます。しかしGWTツールチェインは、バイナリー形式でしか提供されておらず、変更は許されていません。この中には、JavaからJavaScriptへのコンパイラーも含まれています。これはつまり、皆さんが生成したJavaScriptで発生するエラーがコントロールできない、ということを意味します。特に問題なのは、GWTがユーザー・エージェントの検出に依存している点です。そのために、新しいブラウザーのリリースの都度、GWTツールキットをアップデートしなければなりません。

もし皆さんが、J2EEアプリケーション環境の中でGWTを使おうと決断したのであれば、GWTによる設計によって統合は比較的単純になるはずです。このシナリオでは、GWTサービスは、StrutsでのActionに似たもの、つまりバックエンドのビジネス・ロジック呼び出しへのWebリクエストを単純に代行する薄い中間レイヤー、として考える必要があります。GWTサービスは単なるHTTPサーブレットなので、例えばStrutsやSpringMVCの中に容易に統合でき、認証フィルターの後ろに置くことができます。

しかしGWTには、幾つか極めて重大な欠陥があります。第1に、漸減的な方式が使えないことです。最近のWebアプリケーション開発でのベスト・プラクティスでは、JavaScriptなしで動作するようにページを作成しておき、JavaScriptが利用できる場合には飾りを付け、あるいは振る舞いを追加しています。ところがGWTでは、もしJavaScriptが利用できない場合には、何もUIが使えません。一部のWebアプリケーションでは、これは致命的です。また、国際化もGWTにとって大きな問題です。GWTのクライアントJavaクラスはブラウザーで実行するため、実行時に、ローカライズされた文字列を取得すべくプロパティーやリソース・バンドルにアクセスすることができません。各クラアイントサイド・クラスのサブクラスをロケール毎に作成する、という複雑な回避策(参考文献を見てください)はありますが、GWTの技術者達は、もっと現実的なソリューションを見つけるべく努力しています。

コード生成の問題

GWTのアーキテクチャーで最も議論になるのは、クライアントサイドのコードに対してはJava言語に切り換えるようになっている点です。GWT推進者の一部は、クライアントサイドのコードをJava言語で書くことは、JavaScriptを書くよりも本質的に好ましいことだと主張します。これは全員の共通意見ではありません。JavaScriptコーディング派の多くは、時には非常に面倒なJava開発のためにJavaScriptの柔軟性や表現力を犠牲にすることには、大きな抵抗感を持っています。JavaScriptの代わりにJavaコードで置き換えることが魅力的な場合としては、熟練したWeb開発者がいないチームで開発を行うような場合です。しかし、もしそのチームがAjax開発に向かうのであれば、Javaコード屋が手元の独自ツールを使って意味不明なJavaScriptを作り出すのに任せるよりは、スキルを持ったJavaScriptプログラマーを雇った方が賢明と言えます。またGWTはJavaScriptやHTTP、HTMLなどにまたがって行って抽象化を行っていますが、そこには漏れがあります。そのため必然的にバグが発生し、熟練したWebプログラマーが、その追跡のために苦労しています。開発者でありブロガーであるDimitri Glazkovの言葉を借りれば、「JavaScriptを扱えない人は、Webアプリケーション用のコードを書くべきではありません。HTML、CSS、そしてJavaScriptの3つは、Webアプリケーションにとって必須なのです。」(参考文献を見てください)

Javaでのコーディングには静的型付けとコンパイル時のチェックがあるため、JavaScriptによるプログラミングよりも本質的にエラーを起こしにくい、と主張する人がいます。しかしそれは誤った主張です。どのような言語であれ、質の悪いコードは書けるものであり、その証拠としてバグだらけのJavaアプリケーションが山のようにあります。またGWTのコード生成も、バグ無しという前提になっています。しかし、オフラインでの構文チェックと、クライアントサイド・コードの検証は、確かに有利です。これは、Douglas CrockfordによるJSLintという形でJavaScript用に利用可能です。GWTはユニット・テストに関しては優れており、クライアントサイド・コードをJUnitと統合することができます。ユニット・テストのサポートは、相変わらずJavaScriptには全く欠けている領域です。

Weather Reporterアプリケーションを開発する中で、クライアントサイドのJavaコードに関して最も素晴らしいと思ったのは、両方の階層間で同じ検証クラスを共有できるという機能です。これは明らかに開発努力の削減につながります。同じことは、RPCを介して転送されるすべてのクラスについても当てはまります。一度コード化すれば、クライアント・コードでもサーバー・コードでも両方で使えるのです。残念ながら、抽象化には漏れがあります。例えば私のZIPコード・バリデーターでは、本当は正規表現を使ってチェックを行いたかったのですが、GWTはString.match() メソッドを実装していません。もし実装していたとしても、GWTでの正規表現は、クライアント・コードとしてデプロイされる場合とサーバー・コードとしてデプロイされる場合とで構文的な違いがあります。これは、GWTがホスト環境の基礎となっているregexp機構に依存しているためであり、不完全な抽象化によって起こるトラブルの一例と言うことができます。

GWTが大いに得点を上げるのは、そのRPC機構に関してであり、またJavaコードとJavaScriptとの間でのオブジェクト・シリアル化が組み込まれている点です。これによって、通常のAjaxアプリケーションにありがちな大仕事を無くすことができます。ただし、これには前例があるのです。GWTの他の機能は不要でも、この機能だけは欲しいのであれば、DWR(Direct Web Remoting)は十分検討に値します(DWRでは、JavaコードとJavaScriptとの間でのオブジェクト・マーシャリング(object marshalling)が可能なRPCが用意されています・・・参考文献を見てください)。

またGWTは、Ajaxアプリケーション開発の、下位レベルの課題の幾つかをうまく抽象化しています(例えばブラウザー間での互換性対応やDOMイベント・モデル、Ajaxコールの実行など)。しかし、最近のJavaScriptツールキット(Yahoo! UI LibraryやDojo、MochiKitなど)は、似たようなレベルの抽象化を、コード生成に頼る必要なく実現しています。しかもこうしたツールキットはどれもオープンソースのため、必要に合わせてのカスタム化や、発生するバグの修正も行うことができます。こうしたことは、GWTのブラック・ボックスでは不可能です(囲み記事、ライセンスを見てください)。


まとめ

GWTは非常に便利な機能を提供する包括的なフレームワークです。しかしGWTは、「完璧かゼロ」という手法をとっており、Webアプリケーション開発という市場の中の、比較的小さなニッチを対象としています。この短い記事で、GWTの機能や限界についての手掛かりが得られたと思います。GWTは万人向きのものではありませんが、大きな技術的成果であることは変わりなく、皆さんが次期Ajaxアプリケーションを設計する際には真剣に考慮対象とすべきものです。ここで取り上げた内容の他にも、GWTには様々なものが含まれています。さらに詳しく知るためには、Googleのドキュメンテーションを読むか、あるいはGWTの開発者フォーラム(参考文献を見てください)に参加してください。


ダウンロード

内容ファイル名サイズ
GWT Weather Reporter application j-ajax4-gwt-weather.zip2.1KB

参考文献

学ぶために

  • GWTのホームページには、ドキュメンテーションやダウンロードへのリンクが用意されています。
  • Ajax for Java developers: Build dynamic Java applications」(Philip McCarthy著, developerWorks, 2005年9月)は、Ajaxについての第一歩です。
  • GWT Internationalizationを見てください。GWTの開発者であるScott Blumが、現在ではGWTアプリケーションの国際化に非現実的な手法が必要なことを解説しています。
  • GWT And Now Script#: The Agony of a Thousand Puppies」を見てください。 Web開発者であるDimitri Glazkovが、GWTではWebの抽象化レイヤーが曖昧になる問題を解説しています。
  • GWT on del.icio.usから、GWTのdel.icio.usタグについての話を知ってください。
  • Direct Web Remoting (DWR) はオープンソースのJavaライブラリーであり、JavaScriptからJavaコードへのRPC機構を提供しています。
  • Java technologyゾーンには、Javaプログラミングに関するあらゆる話題を網羅した記事が豊富に用意されています

製品や技術を入手するために

  • GoogleのWeb Toolkit SDK(GWT SDK)をダウンロードしてください。
  • script.aculo.usは、視覚効果のためのJavaScriptライブラリーです。
  • GWT Component Libraryは、GWTで作成されたサードパーティー・コンポーネントを集めたものです。この中にはScriptaculous用のラッパーも含まれています。
  • JSLint: The JavaScript Verifierを見てください。JSLintは、通常のJavaScriptコードに対して構文検証や構造検証を行うことができます。

議論するために

  • GWT Developer Forumは、GWTの開発者が頻繁に登場するディスカッション・グループであり、GWT用の臨時ドキュメンテーション・ソースとしても機能しています。
  • developerWorksblogsからdeveloperWorksのコミュニティーに参加してください。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Web development, Open source
ArticleID=212079
ArticleTitle=Java開発者のためのAjax: Google Web Toolkitを探る
publish-date=06272006