読者です 読者をやめる 読者になる 読者になる

Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Spring+Servlet3.0で実現する非同期リクエスト処理Comet

Spring/SpringMVC

阪本です。

最近めっきり春の陽気になってきました。
春と言えばSpring・・・ということで(-_-;)、Spring 3.2で非同期リクエスト処理がサポートされたので、触ってみました。

サポートされたのは、Servlet 3.0で追加された非同期処理(Comet)であり、WebSocketはまだ先のようです。。(Spring 4.0で実現予定)


SpringでCometする方法は主に2つあります。

  1. Callableを使用する
  2. DeferredResultを使用する

どちらの方法を使う場合でも、Servlet 3.0の仕様に合わせて、web.xmlServletとFilterに <async-supported>true</async-supported> を書く必要があります。

<servlet>
  <servlet-name>rootServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value></param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
  <async-supported>true</async-supported>
</servlet>

<filter>
  <filter-name>characterEncodingFilter</filter-name>
  <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
  <init-param>
    <param-name>encoding</param-name>
    <param-value>UTF-8</param-value>
  </init-param>
  <init-param>
    <param-name>forceEncoding</param-name> 
    <param-value>true</param-value> 
  </init-param>
  <async-supported>true</async-supported>
</filter>

以降、それぞれの方法について試してみます。

Callableを使用する

Controllerのメソッドの戻り値に、ModelAndViewではなくjava.util.concurrent.Callableを使用することで、Cometを実現します。

@Controller
@RequestMapping("/comet/*")
public class CometController {
    @RequestMapping(value = "callableTest")
    @ResponseBody
    public Callable<ResultEntity> callableTest() {
        return new Callable<ResultEntity>() {
            @Override
            public String call() throws Exception {
                Thread.sleep(5000);     // (※)

                ResultEntity resultEntity = new ResultEntity();
                resultEntity.setTime(System.currentTimeMillis());
                return resultEntity;
            }
        };
    }
}

上記の例では、callableTestへのリクエストを受け付けると、メソッド自体はCallableのインスタンスを返してすぐさま終了しますが、クライアントには、(※)で指定した約5秒後に応答が返されます(上記の戻り値であるResultEntityは、適当なEntityクラスです)。

(※)の部分に、何かのイベント待ちの処理などを入れることにより、サーバで発生したイベントを、すぐにクライアントに返すことが可能になります。

ただこの方法では、Callable#call()の中で長い時間処理が継続していた場合、Spring側でタイムアウトとなり、クライアントにはHTTPステータスコード500が返るため、あまりよろしくないです。。

そこで、タイムアウト時に返す値や、エラーハンドラーを設定できるDeferredResultを使用します。

DeferredResultを使用する

Controllerのメソッドの戻り値に、ModelAndViewやCallableではなくorg.springframework.web.context.request.async.DeferredResultを使用することで、エラーハンドリングを行えるようになります。

@Controller
@RequestMapping("/comet/*")
public class CometController {
    @RequestMapping(value = "deferredResultTest")
    @ResponseBody
    public DeferredResult<ResultEntity> deferredResultTest() {
        // 1. タイムアウト時に返す値を用意する
        ResultEntity timeoutEntity = new ResultEntity();
        timeoutEntity.setError(true);
        final DeferredResult<ResultEntity> deferredResult = new DeferredResult<>(10000L, timeoutEntity);

        // 2. 正常終了時の処理を登録する
        deferredResult.onCompletion(new Runnable(){
            @Override
            public void run() {
                // DeferredResult#setResult()が呼ばれた場合に、ここが実行される
            }
        });

        // 3. タイムアウト時の処理を登録する
        deferredResult.onTimeout(new Runnable(){
            @Override
            public void run() {
                // タイムアウトした場合に、ここが実行される
            }
        });

        Thread thread = new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException ex) {
                    e.printStackTrace();
                }
                // 4. 戻り値はDeferredResult#setResult()にセットする
                ResultEntity resultEntity = new ResultEntity();
                ResultEntity.setTime(System.currentTimeMillis());
                deferredResult.setResult(resultEntity);
            }
        };
        thread.start();

        return deferredResult;
    }
}

少し長いですが、順に見ていきます。

  1. DeferredResultのコンストラクタに、タイムアウト時間(ミリ秒)とタイムアウト時の戻り値を指定しています(上記の例では、タイムアウトは10秒)。これにより、タイムアウトになってもHTTPステータスコード500は返らず、指定した値を返すことができます。
  2. 正常に値を返す(タイムアウトになる前に値を返す)場合に実行する処理を登録します。ここで登録した処理は、DeferredResult#setResult()が呼ばれた場合に実行されます。なお、処理を登録する必要がない場合は、onCompletion()は実行不要です。
  3. タイムアウトした場合に実行する処理を登録します。ここで登録した処理はクライアントに応答を返す前に呼ばれ、必要に応じてクライアントに返す値をセットすることができます。値をセットするには、DeferredResult#setResult()を呼び出します。ただし、DeferredResultのコンストラクタでタイムアウト時の戻り値を指定している場合は、値を変えることはできません。
  4. 別スレッドで何かしらイベントが発生してクライアントに値を返す場合、DeferredResult#setResult()を呼び出します。

このようにして、タイムアウト処理も含めたCometを実現することができます。



なお、DeferredResultには、正常応答を返すsetResult()と、異常応答を返すsetErrorResult()がありますが、今のバージョン(3.2.2)では、内部は同じ処理になっているため、どちらを呼んでも変わりません。将来的に処理内容が変わるかもしれないので、異常応答を返す場合は、setResult()ではなく、setErrorResult()を用いる方がよいでしょう。



では。