セッション管理の基礎
|
セッション管理の基礎
ここではもう少し実際的なセッションを使ったサーブレットSimpleMessageReceptionistとSimpleMessageManagerを試してみよう。SimpleMessageReceptionistは簡単なホテルのメッセージ承りの担当者で、クライアントからのメッセージを受け付ける。この受付係りはSingleThreadModelを実装して複数存在していても良い。またSimpleConciergeやSimpleMessageBoyなどと別の名前で(継承して)同じ仕事をするサーブレットが多数存在しても構わない。下図はその受付画面である。
これらの受付係を総括するのが管理者であるSimpleMessageManagerなる単一のサーブレットである。ただし、今回はデータベースを使うところまで進んでいないので、管理者の役割は極めて簡単な作業、即ち各受付係が受けたメッサージを保管し、これをアクセスしたクライアントにその内容を一覧で報告するだけである。下図はその報告画面である。
SimpleMessageReceptionistはvisitTimes(訪問回数)とamessage(受け付けたメッセージ)及びSimpleMessageManager(管理者)をそのクライアントとのsessionオブジェクトにバインドする。バインドとはなんであろうか?APIのメソッド一覧からわかるように、sessionオブジェクトのAttribute(属性)としてオブジェクトに名前をつけて追加するものである。従ってsessionが存在するかぎりこれらの情報はそのsessionから取得することができる。sessionとamessageオブジェクトとの関係は下図のようになっている。
オブジェクトのバインドにより、イベントを発生できる。各受付からのこのイベントをSimpleMessageManagerが感知して、これを受理する。マネージャは通常自分の席に座ってタバコを吸っていれば良い。SimpleMessageManagerはただひとつのインスタンスしか存在しないいわゆるシングルトンである。イベントを受理するメソッドは複数のスレッドが同時にアクセスする可能性があるので同期化されている。同期化については別途学ぶことにする。
シングルトンについて少し説明しよう。SimpleMessageManagerは、最初にこれがコンテナにロードされインスタンス化されたときには、まずそのinit()メソッドが実行される。そこではsimpleMessageManagerなる変数に自分自身を登録する。simpleMessageManagerはこのオブジェクトのインスタンス変数であるが、staticと定義され、ただひとつのオブジェクトしか存在しない。このオブジェクトはpublicであり、SimpleMessageReceptionistのオブジェクトたちからは参照が可能である。各SimpleMessageReceptionistのオブジェクトはこのオブジェクトを使ってこれを自分が今受け持っているセッションにバインドする。もうひとつstaticなインスタンス変数であるmessagesは各セッションについているmessageを受領し保管するためのテーブルである。これはこのオブジェクトからのみアクセスできるprivateなオブジェクトである。このようにこのサーブレットはコンテナ上にひとつだけ存在し、システム管理者が削除しないかぎり存続してSimpleMessageReceptionistのオブジェクトからのメッセージを待つとともに、自分をHTTPでアクセスしたクライアントにその内容を報告する。ただしこのプログラムのままではmessagesの内容を削除することが無いので蓄積されるメッセージは増えつづける。
それではSimpleMessageManagerクラスのリストを示そう。
SimpleMessageManagerサーブレット |
package basic_servlets; import java.io.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; /** * セッションの実習教材その2(メッセージ・サービス管理者) * 作成日
: (01/07/03 14:30:52) *
@author: */ public class SimpleMessageManager extends
HttpServlet implements HttpSessionBindingListener { public static SimpleMessageManager simpleMessageManager; private static Hashtable messages; /** *
SimpleMessageManagerの初期化、必要なもののインスタンス化 */ public void init() { simpleMessageManager = this; messages = new Hashtable(); } /** *
ServletInfoの設定 */ public String getServletInfo() { return "SimpleMessageManager, Version 1.0
by Terry"; } /** *
sessionにmessageがセットされたので、これをmessagesというテーブルに保管 */ public synchronized void
valueBound(HttpSessionBindingEvent event) { HttpSession session = event.getSession(); AMessage amessage = (AMessage)session.getAttribute("aMessage");
//メッセージの取得 Date acceptedTime =
amessage.acceptedTime; //受理時刻をキーとして messages.put(acceptedTime, amessage); //テーブルに保管する } /** *
sessionがバインドから外されたときは何もしない。 */ public synchronized void valueUnbound(HttpSessionBindingEvent
event) {} public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException { performTask(request, response); } public void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException { performTask(request, response); } /** * セッション管理はクライアントからのアクセスで、いままで蓄積しているメッセージを * すべて報告する。 */ public void performTask(HttpServletRequest
request, HttpServletResponse response){ try { //応答ページ作成
response.setContentType("text/html; charset=Shift_JIS"); PrintWriter out =
response.getWriter();
out.println("<HTML><HEAD><TITLE>Simple Message
Manager");
out.println("</TITLE></HEAD><BODY><H1>今までに預かったメッセージの一覧です</H1><PRE>"); Enumeration
messageKeys = messages.keys(); while
(messageKeys.hasMoreElements()) {
Date key = (Date) messageKeys.nextElement();
AMessage amessage = (AMessage)messages.get(key);
out.println();
out.println(" 送信者 : "+amessage.fromName);
out.println(" 宛先 : "+amessage.toName);
out.println(" メッセージ: "+amessage.message);
out.println(" 承り時刻 : "+amessage.acceptedTime); }
out.println("</BODY></HTML>"); //クライアントに送信 out.flush(); out.close(); response.flushBuffer(); } catch(Throwable theException){ } } } |
このオブジェクトをSimpleMessageReceptionistサーブレットがsetAttributeメソッドを用いて自分の扱っているsessionにバインドしたときはvalueBoundメソッドがeventをパラメタとして呼び出される。以下に示すように、このメソッドではeventからsessionを取得し、そのsessionの属性であるamessageをとりだしてこれをHashtableのmessagesに蓄積する。キーは受け付け時刻としている。ミリ秒まで同じ時刻に二つのバインドがなされる可能性はかぎりなくゼロだが、心配な人はSimpleMessageReceptionistの時刻を呼び出している部分を10ミリ程度のsleepを含めて同期ブロック化する。
イベントの受信部分 |
public synchronized void valueBound(HttpSessionBindingEvent event) { HttpSession session = event.getSession(); //メッセージの取得 AMessage amessage = (AMessage)session.getAttribute("aMessage"); Date acceptedTime = amessage.acceptedTime; //受理時刻をキーとして messages.put(acceptedTime, amessage); //テーブルに保管する } |
SimpleMessageReceptionistについては特に説明する必要もないであろう。sessionにバインドされたamessageのfromNameは、sessionが継続している限り変わらないとしている。この部分は不自然に思われるかもしれない。あとで学ぶセキュリティではユーザ認証を説明する予定であるが、一般にはメッセージを入力するたびにユーザ名とパスワードの入力が必要としないようにする。以下はそのプログラムリストである。
SimpleMessageReceptionistサーブレット |
package basic_servlets; import java.io.*; import java.lang.*; import java.util.*; import javax.servlet.*; import javax.servlet.http.*; /** *
SimpleMessageReceptionistサーブレット * セッションの実習教材その2(メッセージ・サービス受け付け担当) * 作成日
: (01/06/27 12:55:32) *
@author: Terry */ //public class SimpleMessageReceptionist
extends HttpServlet implements SingleThreadModel { public class SimpleMessageReceptionist extends
HttpServlet { public void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException { performTask(request, response); } public void doPost(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException { performTask(request, response); } public String getServletInfo() { return "SimpleMessageReceptionist,
Version 1.0 by Terry"; } /** * このサーブレットはセッション管理の簡単なアプリケーションである。 *
Receptionistは、クライアントに簡単なメッセージ受付サービスを行う *
Receptionistの顧客対応はSimpleMessageServiceManagerが管理する * */ public void performTask(HttpServletRequest
request, HttpServletResponse response) { try { //到来要求が最初のものかどうか調べる(この作業は応答のボディに何かを出力する前に行うこと) HttpSession session =
request.getSession();
session.setMaxInactiveInterval(3600); //このセッションのタイムアウトを秒で設定 int visitTimes = 0; String fromName =
""; String toName =
""; String message = ""; //応答ページ作成の準備
response.setContentType("text/html; charset=Shift_JIS"); //Tomcatの場合は、次のヘッダ行を追加する //
response.setHeader("Cache-Control",
"no-cache=\"set-cookie,set-cookie2\""); PrintWriter out = response.getWriter();
out.println("<HTML><HEAD><TITLE>Simple Message
Receptionist</TITLE>");
out.println("<META HTTP-EQUIV=\"pragma\"
CONTENT=\"no-cache\">"); //本ページはキャッシングをさせない
out.println("<META HTTP-EQUIV=\"Expires\"
CONTENT=\"-1\">");
out.println("</HEAD><BODY><H1>メッセージ受け付けサービス</H1><PRE>");
if (session.isNew()) { //初めて、セッションを作って初期画面をクライアントにかえす
session.setAttribute("visitTimes", new Integer(1));
out.println("いらっしゃいませ。メッセージを承ります。"); } else { //再度訪問者、訪問回数をインクリメント
Integer number =
(Integer)session.getAttribute("visitTimes");
if (number != null) visitTimes = number.intValue();
visitTimes++;
session.setAttribute("visitTimes", new
Integer(visitTimes));
//セッションにvisitTimesをストア
//メッセージの情報を保存
fromName = request.getParameter("fromName");
toName = request.getParameter("toName");
message = request.getParameter("message");
if (fromName==null) fromName="";
if (toName==null) toName="";
if ((fromName.equals(""))||(toName.equals(""))){
out.println("メッセージが不完全です。修正して送りなおして下さい。");
}else{ //必須入力項目不足なし、メッセージの受理 //
fromName = new String(fromName.getBytes("8859_1"),
"Shift_JIS");
//Tomactの場合はこれが必要 //
toName = new String(toName.getBytes("8859_1"),
"Shift_JIS");
//Tomactの場合はこれが必要 //
message
= new String(message.getBytes("8859_1"),
"Shift_JIS"); //Tomactの場合はこれが必要
AMessage amessage = new AMessage(session, fromName, toName, message,
new Date(System.currentTimeMillis()));
session.setAttribute("aMessage", amessage);
//セッションにamessageをストア
session.setAttribute("aManager",
SimpleMessageManager.simpleMessageManager);
out.println(toName+"様へのメッセージは承りました。 "+amessage.acceptedTime);
toName = "";
message = "";
} } //デバッグ情報の送信 // dumpRequest(request, out); //デバッグ用要求情報のダンプ // sessionDump(request, session,
out); //デバッグ用セッション情報のダンプ
out.println("</PRE>"); //応答のページの送信 String url =
"http://" + request.getServerName() + ":" + request.getServerPort() +
request.getRequestURI(); String url1 =
response.encodeURL(url); //このメソッドはWTEでは機能しないので現在使っていない if
(request.isRequestedSessionIdFromCookie()==false){ //Tomcatの場合はここをコメントアウトする
url1 = url + ";jsessionid=" + session.getId(); } out.println(" ***
メッセージをどうぞ ***");
out.println("<FORM ACTION=\""+url1+"\"
METHOD=\"POST\">"); if
(fromName.equals("")){
out.println("<BR>あなたのお名前:<INPUT TYPE=\"text\"
NAME=\"fromName\">"); }else{
out.println("<BR>あなたのお名前: "+fromName+"<INPUT
TYPE=\"hidden\" NAME=\"fromName\"
VALUE=\""+fromName+"\">"); }
out.println("<BR>相手様のお名前:<INPUT TYPE=\"text\"
NAME=\"toName\"
VALUE=\""+toName+"\">");
out.println("<BR>メッセージ:<BR><TEXTAREA
NAME=\"message\" ROWS=\"4\"
COLS=\"40\"></TEXTAREA>");
out.println("<BR><INPUT TYPE=\"submit\"
VALUE=\"送る\">");
out.println("<INPUT TYPE=\"reset\" VALUE=\"やり直す\"></FORM></BODY></HTML>"); //SingleThreadModelによる複数の本サーブレットのインスタンスにも対応可能なことの実験用 // Thread.sleep(10000L); //クライアントに送信(明示的な後始末) out.flush(); out.close();
response.flushBuffer();
} catch(Throwable theException) { } } /** * 到来要求の内容をHTMLに追加する */ protected void requestDump(HttpServletRequest
request, PrintWriter out) throws java.io.IOException {
out.println("要求情報:");
out.println("
getAuthType: " + request.getAuthType());
out.println("
getCharacterEncoding: " + request.getCharacterEncoding());
out.println("
getContentLength: " + request.getContentLength());
out.println("
getContentType: " + request.getContentType());
//out.println("
getContextPath: " + request.getContextPath()); //VisualAgeにはまだ組み込まれていない
out.println("
getMethod: " + request.getMethod());
//out.println("
getLocale: " + request.getLocale().toString()); //VisualAgeにはまだ組み込まれていない
out.println("
getPathInfo: " + request.getPathInfo()); out.println(" getPathTranslated: " +
request.getPathTranslated());
out.println("
getProtocol: " + request.getProtocol());
out.println("
getQueryString: " + request.getQueryString());
out.println("
getRemoteAddr: " + request.getRemoteAddr());
out.println("
getRemoteHost: " + request.getRemoteHost());
out.println("
getRemoteUser: " + request.getRemoteUser());
out.println("
getRequestURI: " + request.getRequestURI());
out.println("
getRequestedSessionId: " + request.getRequestedSessionId());
out.println("
isRequestedSessionIdValid: " + (new
Boolean(request.isRequestedSessionIdValid())).toString());
out.println("
isRequestedSessionIdFromCookie: " + (new
Boolean(request.isRequestedSessionIdFromCookie())).toString());
out.println(" isRequestedSessionIdFromURL:
" + (new Boolean(request.isRequestedSessionIdFromURL())).toString());
out.println("
getScheme: " + request.getScheme());
//out.println("
isSecure: " + (new Boolean(request.isSecure())).toString());
//VisualAgeにはまだ組み込まれていない
out.println("
getServerName: " + request.getServerName());
out.println("
getServerPort: " + request.getServerPort());
ServletContext context = getServletContext();
out.println("
getRealPath: " + context.getRealPath(request.getRequestURI()));
out.println("
getServletPath: " + request.getServletPath());
out.println();
out.println("要求パラメタ:");
Enumeration paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) { String name = (String)
paramNames.nextElement(); String[] values =
request.getParameterValues(name); out.println(" " + name + ":"); for (int i = 0; i < values.length; i++) { //
out.println(" " + values[i]); //
Tomcatの場合は上の1行は以下の2行のように変更すること //
TomcatはURLエンコードされているパラメタのコードが標準のISO8859_1(Latin-1)であると仮定し //
それを単にStringに(つまりUnicodeで)格納して各サーブレットに引き渡している。 //
したがってそのためパラメタ文字列を取得する際にはISO8859_1から自分が期待する //
エンコードへの変換をしなければならない。 String correctValue = new
String(values[i].getBytes("8859_1"), "Shift_JIS"); out.println(" " +
correctValue); } }
out.println();
out.println("要求ヘッダのダンプ:");
Enumeration headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) { String name = (String) headerNames.nextElement(); String value = request.getHeader(name); out.println(" " + name + " : " + value); }
out.println();
out.println("属性(Attributes):");
Enumeration attributeNames = request.getAttributeNames();
while (attributeNames.hasMoreElements()) { String name = (String)
attributeNames.nextElement(); Object value = request.getAttribute(name); out.println(" " + name + " : " + value.toString()); }
out.println();
out.println("クッキー(Cookies):");
Cookie[] cookies = request.getCookies();
if (cookies.length > 0) { for (int i = 0; i < cookies.length; i++) { String name =
cookies[i].getName(); String value =
cookies[i].getValue();
out.println(" "
+ name + " : " + value); } }
out.println();
out.println("*************"); } /** *
HTTP要求のなかのセッションに関わる情報リストをクライアントにHTMLで返す。 * 作成日
: (01/07/02 13:12:40) *
@param request javax.servlet.http.HttpServletRequest *
@param session javax.servlet.http.HttpSession *
@param out java.io.PrintWriter */ public void sessionDump(HttpServletRequest request,
HttpSession session, PrintWriter out) throws java.io.IOException { out.println(); out.println("セッション情報:"); out.println(" isRequestedSessionIdValid: " +
request.isRequestedSessionIdValid()); out.println(" isRequestedSessionIdFromCookie: " +
request.isRequestedSessionIdFromCookie()); out.println(" isRequestedSessionIdFromURL: " +
request.isRequestedSessionIdFromURL()); out.println(" getCreationTime: " + (new
Date(session.getCreationTime())).toString()); out.println(" getId: " + session.getId()); out.println(" getLastAccessedTime: " + (new Date(session.getLastAccessedTime())).toString()); out.println(" getMaxInactiveInterval: " +
session.getMaxInactiveInterval()); out.println(" 属性(Attributes):"); Enumeration sessionAttributeNames =
session.getAttributeNames(); while
(sessionAttributeNames.hasMoreElements()) { String name = (String)
sessionAttributeNames.nextElement(); Object value = session.getAttribute(name);
out.println("
" + name + " : " + value.toString()); } } } |
このプログラムでセッションに関してひとつだけ注意しなけばならないのは、
String urlEncode = response.encodeURL(url);
なる行ではエンジンがcookieによるセッション維持が可能かどうか判断して、可能でなければセッションIDを与えられたURLに付加する。しかしWTEのエンジンはこれを正しく実行しないので、これと等価な以下の3行が追加されている。
if (request.isRequestedSessionIdFromCookie()==false){
urlEncode = url + ";jsessionid=" + session.getId();
}
Tomcatのエンジンを使用する場合はこの3行はコメントアウトする。
このアプリケーションを、各自自分のPCのデスクトップ上にIEを複数立ち上げて複数のクライアントがSimpleMessageReceptionistを同時にアクセスしても正常に動作することを確認しよう。更に、以下のようなサーブレットを作ってSimpleMessageBoyも同時にメッセージの受付をやってくれることを確認されたい。
package
basic_servlets; /** * SimpleMessageBoyサーブレット * セッションの実習教材その3(メッセージ・サービス受け付け担当) * 作成日 : (01/06/27 12:55:32) * @author: Terry */ //public
class SimpleMessageBoy extends SimpleMessageReceptionist implements
SingleThreadModel { public
class SimpleMessageBoy extends SimpleMessageReceptionist { } |
このサーブレットはSimpleMessageReceptionistを単に継承しただけのクラスである。特に説明するまでもない。