チャット作成で学ぶ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) 生成され、共有していたため、全てのチャットルームで同じチャットメッセージが表示されていたわけです。図にすると以下のようになります。
では、URL毎にChatServiceImplのインスタンスが生成されるようにすれば、チャットのデータを自然に分割できるのではないでしょうか?以下の図を見てください。
わかりましたか?今回の記事のキモとなるのは、この考え方です。サーブレットAPIには、インスタンスの「置き場所」として「スコープ」という概念があることはご存知ですね。requestやsession、applicationと言ったものです。
Springにもスコープと言う概念はありましたが、バージョン1の頃は「singleton」(コンテナ内で一意) もしくは「prototype」(毎回newされる) の二つしかスコープがなく、おかげで「サービスはステートレスに設計するもの」と言う「常識」が確立されていました。
しかし、Spring2からは「カスタムスコープ」という機能が取り入れられ、Springは標準で「request」や「session」と言ったスコープを提供しています。
カスタムスコープは自作することもできます。今回のように、「リクエスト生成元のURL」に応じてインスタンスを保持しておくことができるのです。
カスタムスコープの作成
カスタムスコープを作成するのは、意外と簡単です。以下の手順を踏むだけで、自作したスコープを利用できます。
- Scopeインターフェースの実装クラスを作成
- Springコンテナに、自作スコープの名前と1で作成したクラスの名前を登録
- スコープを適用したい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のインスタンスは常に同じです。これでは、スコープを作成した意味がありません。
ここで
まとめ
今回は「Webリモーティング」と言う本来の趣旨から外れて、Spring2の高度な機能である「カスタムスコープ」を使って、複数のチャットルームを扱えるようにアプリケーションを改善しました。
カスタムスコープを使用すると、Beanの生存期間や範囲をいくらでも設定できるので、今回のようなステートフルなサービスを作成するのにもってこいです。
今回のソースコードは
こちらからダウンロードできます。
ご意見、ご感想お待ちしています。
1 2
このサイトについて
TrackBack URL :


