技術ネタ‎ > ‎

appengine上でWicketを使う際のポイント

主にwicket-1.4-RC6をベースに作業した内容になっています。

WicketApplicationで行う設定

セッション関連

  • WicketApplication#newSessionStore()をオーバーライドして「return new HttpSessionStore(this);」とする。
    セッションオブジェクトの保存をファイルではなく、HttpSessionに入れる事でAppEngine上ではDatastoreに保存される事になる。という事で、セッションオブジェクトもスケールの対象となるのでとてもありがたい。
  • appengine-web.xml内に「<sessions-enabled>true</sessions-enabled>」を追加し、セッションを利用可能に設定する。

リソースの更新チェック関連

リソースのチェックの為にModificationWatcherがThreadを生成していしまい、これがAppEngineの制限に引っかかってしまう。これを止めるには以下の方法のいずれかを使用する。
  • WicketApplication#init()内で「getResourceSettings().setResourcePollFrequency(null);」する必要がある。当然、リソースの更新チェックは行われなくなる。
  • WicketApplication#getConfigurationType()をオーバーライドして「return Application.DEPLOYMENT;」とする。
この件について、wicket-1.4-RC6以降で適用できるという条件でおもしろい記事があった。この方法を使うと開発時にDEVELOPMENTモードで起動しつつ、setSecurityManager(null)せずに済むかもしれない。
WicketApplicationクラス内で「super.getServletContext().getServerInfo().startsWith("Google App Engine Development")」がtrueを返すとローカル環境、"Google App Engine Development"とは違う値("Google App Engine/1.2.2"とか)を返された場合はAppEngine上、と判断ができるので、ローカルで動作しているときのみ上記の方法で自前のModificationWatcherを適用し、AppEngine上ではDEPLOYMENTモードで動作させる、とするのが良さそう。上記サイトのMyModificationWatcherクラスをAppEngineModificationWatcherとして、以下のようにすればおk。

WicketApplication

private boolean isLocalMode = true;

@Override
public String getConfigurationType() {
    isLocalMode =
            super.getServletContext().getServerInfo().startsWith(
                    "Google App Engine Development");
    return isLocalMode ? Application.DEVELOPMENT : Application.DEPLOYMENT;
}

@Override
protected ISessionStore newSessionStore() {
    return new HttpSessionStore(this);
}

@Override
protected void init() {
    super.init();
    if (isLocalMode) {
        getResourceSettings().setResourceWatcher(new AppEngineModificationWatcher());
    }
}

@Override
protected WebRequest newWebRequest(HttpServletRequest servletRequest) {
    if (isLocalMode) {
        getResourceSettings().getResourceWatcher(true).start(
                getResourceSettings().getResourcePollFrequency());
    }
    return super.newWebRequest(servletRequest);
}

NotSerializableException

  • javax.jdo.Query#execute()で返されたクエリ結果をWebPageに保持していると、ローカルでは問題無く動作するがAppEngine上ではNotSerializeExceptionが発生する事がある。これをローカルで簡単に気づくにはWebPage#onDetach()をオーバライドして、WebPage自身をシリアライズするコードを書いて、ローカルでの実行時のみそれを走らせるようにおくとAppEngineにデプロイ後にNotSerializeExceptionが発生しないかをチェックできて便利かも。
  • ついでに、onDetach()時には上記に加え、getSession()したオブジェクトのシリアライズも試しておくと便利。
  • ListDataProviderや、Repeater系のコンポーネントでList#subList()を使っている場合は要注意。subList()した結果、シリアライズ不可なRandomAccessSubListのインスタンスが返って来てしまう可能性がある。

ファイルのアップロード

org.apache.wicket.markup.html.form.upload.FileUploadFieldクラスを使用した、Wicket標準のファイルアップロードはそのままでは動作しない。org.apache.wicket.util.upload.DiskFileItemが、ファイルのサイズに関わらずDiskFileItem#getTempFile()経由でorg.apache.wicket.util.file.FileCleanerにアクセス(track()メソッドを実行)してしまい、その際のFileCreanerのstaticフィールドの初期化でThreadを生成してしまうため。
  • DiskFileItemを生成しているクラスはorg.apache.wicket.util.upload.DiskFileItemFactoryクラスで、このファクトリクラスはorg.apache.wicket.protocol.http.servlet.MultipartServletWebRequest.MultipartServletWebRequestクラスのコンストラクタ内で「DiskFileItemFactory factory = new DiskFileItemFactory();」という風に生成されている。
  • で、ファイルアップロードの際はorg.apache.wicket.protocol.http.servlet.MultipartServletWebRequest.MultipartServletWebRequestorg.apache.wicket.markup.html.form.Form.handleMultiPart()内で「WebRequest multipartWebRequest = ((WebRequest)getRequest()).newMultipartWebRequest(getMaxSize());」という風に生成されている。
DiskFileItemFactoryクラス内で、メモリ上で処理するかテンポラリファイルで処理するかを判断するしきい値となるサイズ(デフォルトでは10KB)が用意されているが、それとは関係無しにFileCleanerにアクセスしてしまうため、その値を触ってもダメ。というわけで、次のようにすればAppEngine上でもWicketのFileUploadFieldを使ったファイルアップロードが可能になる。
  • DiskFileItemクラスを継承し、絶対にファイルアクセスしないようにする。サイズに関係無くbyte[]とByteArrayOutputStreamを保持するようにして、以下のメソッドをオーバーライドして動作確認した。
    • public void delete()
    • public byte[] get()
    • public InputStream getInputStream()
    • public OutputStream getOutputStream()
    • public long getSize()
    • public boolean isInMemory() →常にtrueを返す。
    • public void write(final File file) →throw UnsupportedOperationException()する
  • DiskFileItemFactoryクラスを継承し、上記で作成したオリジナルのFileItemクラスのインスタンスを返すようにする。
    • public FileItem createItem(String fieldName, String contentType, boolean isFormField, String fileName)
  • org.apache.wicket.protocol.http.servlet.MultipartServletWebRequestをコピーして、new DiskFileItemFactory()している一行だけを、上記で作成したオリジナルのFileItemFactoryの生成に書き換える。
  • ファイルアップロードで使用するFormクラスの、handleMultiPart()メソッドをオーバーライドして、上記で作成したオリジナルのMultipartServletWebRequestクラスをgetRequestCycle().setRequest()するようにする。
    • getRequestCycle().setRequest(
      new AppEngineMultipartServletWebRequest(((WebRequest) getRequest())
      .getHttpServletRequest(), getMaxSize()));
MultipartServletWebRequestに対して、どのFileItemFactoryを使うか、を設定できれば後ろふたつの作業は必要無いんだけれども、そのあたりの拡張ポイントが見つからなかったのでWicketらしくないカンジに解決してしまった

Ajax関連や、Wicketが使用するリソース

Google App Engineのリクエスト処理の仕組み的に、静的ファイルとして応答できるものは極力そちらで処理し、アプリケーションノードで処理したくない。その際に問題になるのがWicketのリソースファイル。デフォルトの状態だとこれらのリソースファイルへのリクエストが全てアプリケーションノードでさばかれることになり、SpinUpに出くわしたりした場合にアプリケーションがもっさりしたカンジになってしまう。

そこで、これらのファイルは全てwar/resources/配下に配置し、static fileとして定義してデプロイすると良い。

  • org.apache.wicket.ajax.AbstractDefaultAjaxBehavior 配下のリソースファイル
  • org.apache.wicket.ajax.WicketAjaxReference 配下のリソースファイル
  • org.apache.wicket.markup.html.WicketEventReference 配下のリソースファイル
  • org.apache.wicket.extensions.ajax.markup.html.modal.ModalWindow res/配下のリソースファイル
Comments