トップ » 技術記事 » チャット作成で学ぶWebリモーティング(7) - Spring2のカスタムスコープ

チャット作成で学ぶWebリモーティング(7) - Spring2のカスタムスコープ はてなブックマーク数 このエントリーをブックマークに追加

チャットデータの保持方法を検討する

では、「チャットルームごとに異なるURLを割り当てる」という前提で、チャットデータをルームごとに分割する方法を考えましょう。しかも、既存のソースに手を入れずに、です。

チャットのデータを保持しているクラスは、今まで通り「ChatServiceImpl」です。

package chat.impl;

import java.util.ArrayList;
import java.util.List;

import chat.ChatMessage;
import chat.ChatService;

public class ChatServiceImpl implements ChatService {
  private final List<ChatMessage> messages = new ArrayList<ChatMessage>();

  public synchronized void addMessage(ChatMessage message) {
    this.messages.add(message);
  }

  public synchronized List<ChatMessage> getAllMessages() {
    return messages;
  }
}

このクラスのフィールド「messages」に全て格納されているわけですね。今までは、このクラスのインスタンスがSpringのコンテナ上で一つだけ (つまりSingleton) 生成され、共有していたため、全てのチャットルームで同じチャットメッセージが表示されていたわけです。図にすると以下のようになります。

beans.bmp

では、URL毎にChatServiceImplのインスタンスが生成されるようにすれば、チャットのデータを自然に分割できるのではないでしょうか?以下の図を見てください。

beans2.bmp

わかりましたか?今回の記事のキモとなるのは、この考え方です。サーブレットAPIには、インスタンスの「置き場所」として「スコープ」という概念があることはご存知ですね。requestやsession、applicationと言ったものです。
Springにもスコープと言う概念はありましたが、バージョン1の頃は「singleton」(コンテナ内で一意) もしくは「prototype」(毎回newされる) の二つしかスコープがなく、おかげで「サービスはステートレスに設計するもの」と言う「常識」が確立されていました。
しかし、Spring2からは「カスタムスコープ」という機能が取り入れられ、Springは標準で「request」や「session」と言ったスコープを提供しています。

カスタムスコープは自作することもできます。今回のように、「リクエスト生成元のURL」に応じてインスタンスを保持しておくことができるのです。

カスタムスコープの作成

カスタムスコープを作成するのは、意外と簡単です。以下の手順を踏むだけで、自作したスコープを利用できます。

  1. Scopeインターフェースの実装クラスを作成
  2. Springコンテナに、自作スコープの名前と1で作成したクラスの名前を登録
  3. スコープを適用したいBeanに対し、scope属性を指定
スコープの実装

では、カスタムスコープの作成に取り掛かりましょう。スコープの作成方法は、「org.springframework.beans.factory.config.Scope」インターフェースを実装したクラスを作成するところから始まります。

Scopeインターフェースは、以下のようなメソッドを持ちます。もっとも重要なのはget()メソッドです。

Object get(String name, ObjectFactory factory)
スコープからインスタンスを取得する際、コンテナによって呼び出されるメソッド。引数のnameはBeanのID、factoryはObjectFactoryのインスタンスです。
ObjectFactoryはgetObject() (引数なし) と言うメソッドを持っており、それを呼び出すことで新しいインスタンス (コンテナによるDIは済み) を得ることができます。

スコープを自作する場合は、スコープの範囲に応じて同一、もしくは異なるインスタンスを返せばよいのです。

String getConversationId()
スコープを識別するためのID文字列を返します。例えば、HttpSessionであればセッションIDなどです。
Object remove(String name)
指定したIDのインスタンスをスコープから除去します。除去に成功したインスタンスを戻り値として返します。 (nullを返しても構いません)
void registerDestructionCallback(String name, Runnable destructionCallback)
スコープ内のオブジェクト、もしくはスコープ自身が削除されるときに呼び出されるコールバックの登録を行います。

以上を踏まえて、URL毎に異なるスコープを持つよう実装したクラスが以下の「URLScope.java」です。
DWRのWebContext.getCurrentPage()メソッド (こちらのページを参照) を用いてアクセスもとのURLを取得し、そのURL毎にスコープ (単純なHashMap) を生成/管理しています。

package chat;

import java.util.HashMap;
import java.util.Map;

import org.directwebremoting.WebContext;
import org.directwebremoting.WebContextFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

public class URLScope implements Scope {
  // URL毎にスコープ (Map<BeanId, Object>) を保持する
  private Map<String, Map<String, Object>> urlScopes =
    new HashMap<String, Map<String,Object>>();

  public Object get(String name, ObjectFactory factory) {
    Map<String, Object> urlScope = getUrlScope(true);
    if (urlScope == null) {
      return null;
    }
    synchronized (urlScope) {
      Object value = urlScope.get(name);
      if (value == null) {
        value = factory.getObject();
        urlScope.put(name, value);
      }
      return value;
    }
  }

  public String getConversationId() {
    WebContext wc = WebContextFactory.get();
    if (wc == null) {
      return null;
    }
    String url = wc.getCurrentPage();
    return url;
  }

  public Object remove(String name) {
    Map<String, Object> urlScope = getUrlScope(false);
    if (urlScope == null) {
      return null;
    }
    synchronized (urlScope) {
      return urlScope.remove(name);
    }
  }

  private Map<String, Object> getUrlScope(boolean create) {
    WebContext wc = WebContextFactory.get();
    if (wc == null) {
      return null;
    }
    String url = wc.getCurrentPage();
    synchronized (urlScopes) {
      Map<String, Object> urlScope = urlScopes.get(url);
      if (urlScope == null && create) {
        urlScope = new HashMap<String, Object>();
        urlScopes.put(url, urlScope);
      }
      return urlScope;
    }
  }

  public void registerDestructionCallback(String arg0, Runnable arg1) {
  }
}

今回はregisterDestructionCallback()以外のメソッドをきちんと実装しましたが、本来get()だけでも問題ありません。

これで、新しいURLからのアクセスがあるたびに、新しいスコープとオブジェクトの生成が行われます。

カスタムスコープをSpringコンテナに登録

こうして作成したスコープを使用するには、まずSpringに対して「スコープの名前」と「スコープの実装クラス」を伝える必要があります。そのためには、CustomScopeConfigurerクラスを以下のように設定します。

<bean
  class="org.springframework.beans.factory.config.CustomScopeConfigurer">
  <property name="scopes">
    <map>
      <!-- キーがスコープの名称、値が実装クラス -->
      <entry key="url">
        <bean class="chat.URLScope" />
      </entry>
    </map>
  </property>
</bean>

これで、”url”スコープをどこからでも使用できるようになりました。

カスタムスコープの使用

あとは当初の予定通り、ChatServiceImplのインスタンスを”url”スコープで管理するようにします。scope=”url”と指定しているのがお分かりでしょう。

<bean id="chatService" class="chat.impl.ChatServiceImpl" scope="url">
  <aop:scoped-proxy/>
</bean>

ここで一つ注意が必要。Springがバージョン1の頃から持っているSingletonやPrototypeと言ったスコープ以外では、ここに示したようにの記述が必須です。

その理由は、SpringのDIが、Beanの生成時にしか行われない事によるものです。chatServiceをリモートプロキシに対してDIしている、以下の設定を見てください。

<bean id="chatServiceRemote" class="remote.ChatServiceRemote">
  <!– SingletonスコープのBeanに、URLスコープのBeanをDIしている –>
  <property name="chatService" ref="chatService" />

  …(略)…

</bean>

“chatServiceRemote”にはスコープの指定を行っていないので、デフォルトのSingletonスコープが適用されます。つまり、コンテナ内に唯一のインスタンスとなるわけです。このインスタンスを生成するときに、ChatServiceImplのインスタンスが直接DIされてしまうと、結局、chatServiceRemoteによって使用されるChatServiceImplのインスタンスは常に同じです。これでは、スコープを作成した意味がありません。
ここでを指定しておくと、”chatService”のインスタンスが参照されるとき、常にカスタムスコープ (ここではURLScope) からのget()が行われるようになるわけです。

まとめ

今回は「Webリモーティング」と言う本来の趣旨から外れて、Spring2の高度な機能である「カスタムスコープ」を使って、複数のチャットルームを扱えるようにアプリケーションを改善しました。
カスタムスコープを使用すると、Beanの生存期間や範囲をいくらでも設定できるので、今回のようなステートフルなサービスを作成するのにもってこいです。
今回のソースコードは
こちら
からダウンロードできます。

ご意見、ご感想お待ちしています。

Series Navigation«Spring2の名前空間拡張機能を用いたDWRの設定

1 2

このサイトについて

八角研究所
株式会社八角研究所のWEBサイトですよー。 いろんなものを創り出すことのできる環境をコツコツ構築中。 いったい、いつになったらできるのか。 この技術情報サイトもそのための活動の一環のつもり。

執筆者紹介

shiraishi

shiraishi

最近書いてばっかりいます。 眠いとおんなじことばかり書きます。 そして、大概眠いです。

TrackBack URL :