Google App Engine/Java上で、DWR2を使ったSessionの管理をするには
先のログで「Google App Engine(GAE)上に配置したDWR2では、scope=sessionの設定がうまくいかない」と書いた。
GAEが準じているとするServetAPI2.5の参照実装であるTomcat6.0では、dwr.xmlにscope=sessionで定義したJavaBean(POJO)はセッションのスコープをもつ(ように見える)。
ただ、(これも先のログで書いたが)DWRのドキュメントを見る限りでは、「DWR2のsession=scopeの設定は、不変オブジェクトを指向している」と読めなくもない。Sessionオブジェクトは「便利なオブジェクト」であるが、永続化の1種であるから、これもあながち間違っていない(「Sessionオブジェクトに入れるからjava.io.Selializableをimplする」習慣の人もいると思う)。
とはいえ、ショッピングカートのような動きはSessionオブジェクトを使いたくなるのが人情。これは不変オブジェクトというわけにはいかない。
GAEでは、セッション情報をメモリーキャッシュとデータストアに保存すると明記されているが、(これも以前のログで検証したが)HttpSessionのAPIは、概ね通常のWebアプリケーションと同様の結果を返す。
裏を返せば、通常のWebアプリケーションサーバーにおいて、メモリーという「揮発性の高い」空間に配置されたSessionオブジェクトは、GAEを使うことで「消えてなくなるリスク」が低減する。
それでは、DWR2でセッションスコープを使うにはどうしたらいいか、となると、単純にGAEに用意されたHttpSessionを利用すればいいのだと思う。
dwr.xmlに定義したJavaBeanからは、簡単にServletContextやSessionオブジェクトが取得できるようになっている。これがGAE上で機能することを検証すればよい。
以下に簡単なサンプルを示す。
画面のイメージは以下で、String1、・・・、String5は、Sessionオブジェクトに保存したJavaBean(TestBean.java;後述)のメンバー変数(Map)から取り出したValuesである。
ここで、「Mapの更新」ボタンを押すと、String5から順にMapからremoveされて、Sessionオブジェクトに保存される。Mapから削除した後に画面をリフレッシュしても、削除されたStringxが再表示されることはない。
以下の作業は、GAEプラグインを入れたEclipse3.4(Gamymede)で行った。
プロジェクトの作成
プロジェクトの新規作成で、Web Application Projectを選び、プロジェクト(GaeDWRTest)を作成する。このとき、GWTは使わないのでチェックをはずす。
jarのコピー
dwr.jarをwar/WEB-INF/lib下に配置する。
Javaクラスの作成
DWRから呼び出されるJavaBean(TestDWRBean.java)と、Sessionオブジェクトに保管するJavaBean(TestBean.java)の2つを作成する。
TestBean.java
package test; import java.io.Serializable; import java.util.Map; public class TestBean implements Serializable{ private static final long serialVersionUID = -9110143420906726458L; private Map<Integer,String> testMap; public TestBean() { } public Map<Integer,String> getTestMap() { return testMap; } public void setTestMap(Map<Integer,String> testMap) { this.testMap = testMap; } }
TestDWRBean.java
以下のコードから明らかなように、このクラスでは、DWRを経由したメソッドの呼び出しの度にSessionオブジェクトへのアクセス(と、更新/保管)を行う。DWR側で用意しているWebContextFactoryクラスをつかって、Sessionオブジェクトを取得する(これが、GAEでうまくいくかがポイント)。
package test; import java.util.Collection; import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpSession; import org.directwebremoting.WebContext; import org.directwebremoting.WebContextFactory; public class TestDWRBean{ public TestDWRBean() { WebContext wctx = WebContextFactory.get(); HttpSession session = wctx.getSession(); boolean ns = session.isNew(); if(ns){ Map<Integer, String> mp = new HashMap<Integer,String>(); mp.put(new Integer(1), "String1"); mp.put(new Integer(2), "String2"); mp.put(new Integer(3), "String3"); mp.put(new Integer(4), "String4"); mp.put(new Integer(5), "String5"); TestBean tb = new TestBean(); tb.setTestMap(mp); session.setAttribute("testBean", tb); } } public Map<Integer, String> updateMapValues(){ WebContext wctx = WebContextFactory.get(); HttpSession session = wctx.getSession(); TestBean tb = (TestBean)session.getAttribute("testBean"); // 呼び出されるごとに1つずつ減らしていく。 Map<Integer, String> mp = tb.getTestMap(); Collection<String> cl = mp.values(); int sz = cl.size(); if(sz > 0){ mp.remove(sz); tb.setTestMap(mp); } session.setAttribute("testBean", tb); return mp; } public Collection<String> getMapValues() { WebContext wctx = WebContextFactory.get(); HttpSession session = wctx.getSession(); TestBean tb = (TestBean)session.getAttribute("testBean"); return tb.getTestMap().values(); } }
dwr.xml
war/WEB-INF/dwr.xmlを以下のように定義する。
sessionオブジェクトへのアクセスをDWRに任せないので、scope=sessionの定義はしない。
<?xml version="1.0" encoding="utf-8"?> <!DOCTYPE dwr PUBLIC "-//GetAhead Limited//DTD Direct Web Remoting 2.0//EN" "http://directwebremoting.org/schema/dwr20.dtd"> <dwr> <allow> <create creator="new" javascript="TestDWRBean"> <param name="class" value="test.TestDWRBean"/> </create> </allow> </dwr>
demo.htmlの作成
上で示した画面の作成する。
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <head> <title>DWRのサンプリング</title> <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <style type="text/css"> body { margin:0; padding:0; } #container { margin: 2px; padding: 3px; line-height:1.5em; width: 500px; height: auto; border:1px dashed #999999; } </style> <script src='dwr/interface/TestDWRBean.js'></script> <!-- engine.jsは必須 --> <script src='dwr/engine.js'></script> <!-- util.jsは必須 --> <script src='dwr/util.js'></script> <script type="text/javascript"> function init() { makeTable(); } function makeTable() { TestDWRBean.getMapValues( // Callback関数 function(data) { dwr.util.setValue("demoReply", data); } ); } function update() { TestDWRBean.updateMapValues( // Callback関数 function() { makeTable(); } ); } </script> </head> <body onload="init()"> <div id="container"> <p> セッションスコープのDWRのデモです。<br> 「mapの更新」ボタンを押してください。 </p> <b>Session TestBeanのメンバー変数(map)の値:</b> <br> <span id="demoReply"></span> <br> <br> <input value="mapの更新" type="button" onclick="update()"> </div> </body> </html>
web.xml
war/WEB-INF/web.xmlは、普通にDWRを使うときと同じ。ケースによってSessionのインターバルタイムを変更する。welcome-fileを、demo.htmlに変更する。
<?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 xmlns="http://java.sun.com/xml/ns/javaee" version="2.5"> <servlet> <servlet-name>dwr-invoker</servlet-name> <display-name>DWR Servlet</display-name> <description>Direct Web Remoter Servlet</description> <servlet-class>org.directwebremoting.servlet.DwrServlet</servlet-class> <init-param> <param-name>debug</param-name> <param-value>false</param-value> </init-param> <init-param> <param-name>activeReverseAjaxEnabled</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>initApplicationScopeCreatorsAtStartup</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>maxWaitAfterWrite</param-name> <param-value>-1</param-value> </init-param> <init-param> <param-name>crossDomainSessionSecurity</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>allowScriptTagRemoting</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dwr-invoker</servlet-name> <url-pattern>/dwr/*</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>demo.html</welcome-file> </welcome-file-list> </web-app>
デプロイ
ここまでできたら、通常通りにEclipseからGAEアカウントに対してデプロイをする。
これでは、「DWR2のsession=scopeはどうなるの?」ということになるが、特殊な環境下では致し方のないことのように思う。