Seasar 2.4リリース! 今更でも恥ずかしくない、始めてみようDIプログラミング HOT deploy 編

昨日は SMART deploy 編と言いつつ,実際に試せるのは WARM/COOL だけで,HOT deploy は追加のコードが必要とだけ書いて説明を端折ってしまいました.心より恥じる.
そんなわけで (どんなわけで?),今日は昨日のサンプルを HOT deploy 対応にしてみます.


まずは実行用の Test クラスを修正します.

Test.java
package test;

import org.example.service.PrintService;
import org.seasar.framework.container.S2Container;
import org.seasar.framework.container.factory.SingletonS2ContainerFactory;
import org.seasar.framework.container.hotdeploy.HotdeployUtil;

public class Test {
    public static void main(String[] args) throws Exception {
        SingletonS2ContainerFactory.init();
        S2Container container = SingletonS2ContainerFactory.getContainer();

        Class.forName(PrintService.class.getName());
        for (int i = 0; i < 3; ++i) {
            HotdeployUtil.start();
            PrintService printer = (PrintService) container
                    .getComponent("printService");
            printer.print();
            HotdeployUtil.stop();
        }
    }
}

main() メソッドでコンテナを初期化している最初の 2 行は昨日と同じです.
その後の Class#forName() は後述.
for 文は HOT deploy の効果を確認するために追加しています.
そして for ループの中に加えられた HotdeployUtil#start()stop() が HOT deploy のためのおまじないです.
HOT deploy は,この start() から stop() までが一つのコンテキストとなります.
このコンテキスト内では HotdeployClassLoader がスレッドのコンテキストクラスローダーに設定され,新たなクラスはこのクラスローダーにロードされます.
このクラスローダーは HotdeployUtil#stop() で破棄されます.
次の繰り返しになると,また新たにクラスローダーが作成され,クラスも改めてロードされます.
簡単に言うと,これが HOT deploy の全てです.


Web アプリの場合は HotdeployServletFilterHotdeployUtil#start()stop() をお任せできます.
つまり,一つのリクエストが一つのコンテキストになります.


修正するファイルがもうひとつ.

s2container.dicon
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.4//EN" 
    "http://www.seasar.org/dtd/components24.dtd">
<components>
    <include path="hotdeploy.dicon"/>
</components>

warmdeploy.dicon に代えて,hotdeploy.dicon をインクルードしています.


それでは実行してみます.
その前に,Test#main() のループの中にブレークポイントを設定してください.
そしてデバッグ実行します.
ループに入ったところで止まるので,まずはゴー!! (Eclipse だと F8).
次のように出力されます.

DEBUG 2006-11-19 01:06:55,750 [main] BEGIN org.example.service.impl.PrintServiceImpl#print()
DEBUG 2006-11-19 01:06:55,750 [main] BEGIN org.example.service.impl.MessageServiceImpl#getMessage()
DEBUG 2006-11-19 01:06:55,765 [main] END org.example.service.impl.MessageServiceImpl#getMessage() : 新大陸へようこそ
新大陸へようこそ
DEBUG 2006-11-19 01:06:55,765 [main] END org.example.service.impl.PrintServiceImpl#print() : null


2 回目のループで止まっているはずなので,ここで MessageServiceImpl を修正します.
もちろん実行したまま.

MessageServiceImpl.java
package org.example.service.impl;

import org.example.service.MessageService;

public class MessageServiceImpl implements MessageService {
    public String getMessage() {
        return ("エビちゃん最高!!");
    }
}

そしてゴー (F8)!!

DEBUG 2006-11-19 01:08:49,421 [main] BEGIN org.example.service.impl.PrintServiceImpl#print()
DEBUG 2006-11-19 01:08:49,421 [main] BEGIN org.example.service.impl.MessageServiceImpl#getMessage()
DEBUG 2006-11-19 01:08:49,421 [main] END org.example.service.impl.MessageServiceImpl#getMessage() : エビちゃん最高!!
エビちゃん最高!!
DEBUG 2006-11-19 01:08:49,421 [main] END org.example.service.impl.PrintServiceImpl#print() : null

ちゃんとメッセージが変わりました.


まだ繰り返しの中で止まっているはずなので,今度は新たなクラスを追加して MessageService のインタフェースごと変えてみましょう.
次のパッケージを作成します.

  • org.example.dto

そして新たなクラス,MessageDto を作成します.

package org.example.dto;

public class MessageDto {
    private String text;

    public MessageDto(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}


MessageServiceDTO を返すように修正します.

MessageService.java
package org.example.service;

import org.example.dto.MessageDto;

public interface MessageService {
    public MessageDto getMessage();
}


実装クラスも修正します.

MessageServiceImpl.java
package org.example.service.impl;

import org.example.dto.MessageDto;
import org.example.service.MessageService;

public class MessageServiceImpl implements MessageService {
    public MessageDto getMessage() {
        return new MessageDto("大好き! ぷよぷよ!!");
    }
}


最後に MessageService を利用している PringServiceImpl を修正します.

PrintServiceImpl.java
package org.example.service.impl;

import org.example.service.MessageService;
import org.example.service.PrintService;

public class PrintServiceImpl implements PrintService {
    private MessageService messageService;

    public void setMessageService(MessageService messageService) {
        this.messageService = messageService;
    }

    public void print() {
        System.out.println(messageService.getMessage().getText());
    }
}


そしてゴー (F8)!!

DEBUG 2006-11-19 01:14:09,015 [main] BEGIN org.example.service.impl.PrintServiceImpl#print()
DEBUG 2006-11-19 01:14:09,015 [main] BEGIN org.example.service.impl.MessageServiceImpl#getMessage()
DEBUG 2006-11-19 01:14:09,015 [main] END org.example.service.impl.MessageServiceImpl#getMessage() : org.example.dto.MessageDto@c8570c
大好き! ぷよぷよ!!
DEBUG 2006-11-19 01:14:09,015 [main] END org.example.service.impl.PrintServiceImpl#print() : null

そんなわけで (どんなわけで?),実行途中でも HOT deploy できていることが確認できました♪


さて.
このサンプルでは,4 つのクラス (インタフェース 含む) が登場していました.

  • MessageService
  • MessageServiceImpl
  • PrintService
  • PrintServiceImpl

途中で追加したクラスもあります.

  • MessageDto

これらの中で,HOT deploy できないものが一つだけあります.
それは

  • PrintService

です.
なぜなら,このインタフェースは HOT deploy なコンテキストの外側にいる,Test クラスから参照されるからです.
もし HotdeployClassLoaderPrintService をロードしてしまうと,Test#main()

            PrintService printer = (PrintService) container
                    .getComponent("printService");

という部分で ClassCastException が発生します.
Test からは HotdeployClassLoader は見えないため,本来のクラスローダー (このサンプルではシステムクラスローダー) から PrintService をロードします.
しかし,S2 コンテナから返されるのは HotdeployClassLoader からロードされた PrintService (を実装したクラス) のインスタンスです.
Java では異なるクラスローダーからロードされたクラスは別のクラスとして扱われ,代入やキャストができません.
そのため ClassCastException が発生します.


このような事態を極力避けるため,HotdeployClassLoader はオリジナルのクラスローダー (このサンプルではシステムクラスローダー) にロード済みのクラスはロードしないようになっています.
ところが,Test#main() を実行する場合に PrintService がシステムクラスローダーにロードされるタイミングは,予想外に遅いです.
もし Test#main()

        Class.forName(PrintService.class.getName());

という行がないと,コンテナから返された PrintService (を実装したクラス) のインスタンスが返された後,それをフィールドに代入する直前までロードされません.
そのため,上記の Class#forName() を削除すると,コンテナから PrintService を取得する時点ではシステムクラスローダーに PrintService がロードされていないため,HotdeployClassLoader にロードされてしまいます.
その後システムクラスローダーが PrintService をロードして,めでたく ClassCastException になるという次第.
この挙動は結構悩ましいです〜.


ともあれ (JW),通常の Web アプリなどでは,HOT deploy のコンテキストの外側にいるのはフレームワーク (Teeda とか S2Struts) で,それらが HOT deploy 対象となるアプリケーションのクラスをロード/リンクすることはまずないと思います.
convention.dicon に記述するルートパッケージ配下のクラスを dicon に明示的に定義したり,ルートパッケージ配下にないクラスから直接参照したりといったことを避ければ ClassCastException に悩まされることはそれほどないと思います.
現在開発中の案件ではこれに悩まされているのは内緒だ.


そんなわけで (どんなわけで?),HOT deploy 編をお送りしました.
Seasar2.4 をダウンロードして,是非ともお試しください♪