-ページ情報(2)

TapestryのInsertコンポーネントは、多くの場合、OGNLでバインドしたページオブジェクトのプロパティ値を表示します。が、この場合にやってはいけない間違いを犯すことが多くみられます。最もシンプルにやってはいけない間違いケースは以下のとおりです。

package sample.simple;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.html.BasePage;
public class Home extends BasePage {
  private int hardCount = 1;
  public int getCount() {
    return hardCount;
  }
  public void click(IRequestCycle cycle) {
    hardCount++;
  }	
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification
    PUBLIC "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
    "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
<page-specification class="sample.simple.Home">
  <component id="click" type="DirectLink">
    <binding name="listener" expression="listeners.click"/>
  </component>
  <component id="count" type="Insert">
    <binding name="value" expression="count"/>
  </component>
</page-specification>
<html>
  <head>
    <title>test</title>
  </head>
  <body>
    hard: <span jwcid="count">0</span><br>
    <a href="#" jwcid="click">click</a>
  </body>
</html>

実行すると、画面には以下のとおり表示されます。

hard: 1

click

クリックするとDirectLinkコンポーネントの動作によって、Home#click(IRequestCycle)メソッドが実行され、HomeのメンバーであるhardCountの値をインクリメントしていきます。一見、正常に動作しているようですが、二点おかしな点を指摘します。

  • もうひとつブラウザを立ち上げてみてください。カウンターが続行してインクリメントされていきます。正しい動きと錯覚しがちですが大変な間違いです。ページオブジェクトのprivateメンバーに保持しているはずの値が見えてしまってます。これはページオブジェクトがプールされていて、常に再利用していることに起因しています。よって、ページオブジェクトのメンバーにリクエスト毎の情報(たとえばユーザーIDや個人情報等)を置くと、他ユーザーからみえてしまうなどのバグの原因です
  • 5分以上、アクセスしないで放置してみてください。カウンターがリセットされます。これは、ページオブジェクトがプールに戻されてからデフォルトで5分経過するとオブジェクト破棄のバッチ処理が走るために、インクリメントされた値をもったページオブジェクトが無くなり、新たにページオブジェクトが生成されているためです。

この対応の一つはAbstractPage#initialize()メソッドをオーバーライドして、メンバーの初期化を行うことです。

  protected void initialize() {
    hardCount = 1;
  }

この例は、アプリケーションとしてまったくきちんと動かない(常に初回1、そしてずっと2になるだけ)のですが、これでプールに戻るページオブジェクトに不安定な値が残ることはなくなります。
もう一つの対応は、property-specificationを用いることです。

package sample.simple;
import org.apache.tapestry.IRequestCycle;
import org.apache.tapestry.html.BasePage;
public abstract class Home extends BasePage {
  public abstract int getCount();
  public abstract void setCount(int count);
  public void click(IRequestCycle cycle) {
    setCount(getCount() + 1);
  }	
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE page-specification
    PUBLIC "-//Apache Software Foundation//Tapestry Specification 3.0//EN"
    "http://jakarta.apache.org/tapestry/dtd/Tapestry_3_0.dtd">
<page-specification class="sample.simple.Home">
    <property-specification name="count" type="int" initial-value="1"/>
    <component id="click" type="DirectLink">
        <binding name="listener" expression="listeners.click"/>
    </component>
    <component id="count" type="Insert">
        <binding name="value" expression="count"/>
    </component>
</page-specification>

ページオブジェクトにメンバーをおかず、プロパティのGetterおよびSetterをabstractメソッドにします(したがって、クラスもabstractになります)。そして、ページスペックXMLのほうに、<property-specification>エレメントを記述します。エレメントのname属性にプロパティ名、type属性に型を、そしてinitial-value属性にプロパティ初期値を指定します。こうすると、JavassistによりTapestry内部でabstractメソッド・必要メンバーの実装が自動的に行われます。

コラム: Javassistの生成するクラス

<property-specification>で定義されたプロパティを、Tapestryは以下の「ように」実装します。

package sample.simple;
public abstract class Home$Enhance_0 extends Home {
  private int _$count;
  public int getCount() {
    return _$count;
  }
  public void setCount(int count) {
    _$count = count;
  }
}

実際には動的なバイトコードエンジニアリングのために、ソースは存在しませんし、厳密には継承ではありません。あくまでイメージです。また、実装予定の、GetterおよびSetterどちらか一方でもユーザー実装メソッドがあると、Javassistを利用する前にTapestryで例外を生成します。

さらに、<property-specification>エレメントで定義されたプロパティについては、該当プロパティをinit-value属性値で初期化するPageDetachListenerを自動的に追加してくれます。そのため、ページオブジェクトがプールに戻るまえに常に指定値で初期化され、問題としている「他ユーザーにメンバー情報が共有されてしまう」というバグが自然回避されます。このメカニズムは、PageLoader#createPropertyInitializer()メソッドで行われています。ぜひ一度ソースコードで確認ください。
さて、これで使い方の間違いは正せましたが、このままではカウンターがインクリメントされず、常に初期値1、後ずっと2という状態です。アプリケーションとして正しく動作させる方法は、以下のとおりです。

<property-specification name="count" type="int" initial-value="1" persistent="yes"/>

このpersistent属性を「yes」と設定(デフォルトはno)するだけで、きちんと動作します。

  • もうひとつブラウザを立ち上げても、それぞれ別の値でインクリメントしていきます->OK。
  • 5分放置して後も、きちんとインクリメントを続けられます->OK。

このメカニズムはIPageRecorderというものを使ってます。この解説は次回に。今回のサンプルもいつもの、http://www.kjps.net/user/kurihara/ に置いておきます。「SimplePage sample [2004/06/10]」です。