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>

appengine-web.xml

セッションを有効にするために以下の設定を追加する。

	<sessions-enabled>true</sessions-enabled>

デプロイ

ここまでできたら、通常通りにEclipseからGAEアカウントに対してデプロイをする。

これでは、「DWR2のsession=scopeはどうなるの?」ということになるが、特殊な環境下では致し方のないことのように思う。