-ページ情報(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]」です。