James君!〜Phoenix を使ったEchoサーバの実装

サービス実装の基礎知識〜ライフサイクル

さて、では Echo サーバ自体の実装だ。実際には org.apache.james.remotemanager.RemoteManager あたりをカンニングして書いたものである。まず大前提として、「Avalonコンポーネントとか何か?」あたりから始めるのが筋だろう。

まあ、「Avalonコンポーネント」は、POJO(Plain Old Java Object) 風のコンポーネントだ。言い替えると、「何か特定の抽象クラスを継承しなくてはならない」フレームワークではない、ということだ。とはいえ、JSF みたいな JavaBeans ベースの POJO ではなくて、「必要に応じて、必要な interface を implements する」というかたちになっている。で、「Avalon フレームワーク」で、コンポーネントの「ライフサイクル」が決められていて、そのコンポーネントが「○○ interface を implements していれば」で、ライフサイクルに応じたメソッドが呼ばれる...という寸法だ。そういったライフサイクルに応じた interface が開始時、開始後のサスペンド&レジューム、停止時の各フェーズごとに次のように定められている。だからそのようなコールバックの引数で与えられる、フレームワークのプロパティに相当するオブジェクトを使って、いろいろと仕事をしていけばいい。

以下のリストは起動順で()内はインターフェイスクラスである。また、太字は実際に Echo サーバで implements しているものだ。

開始時
  1. コンストラクタ
  2. contextualize(org.apache.avalon.framework.context.Contextualizable)
  3. compose(org.apache.avalon.framework.component.Composable)
  4. configure(org.apache.avalon.framework.configuration.Configurable)
  5. parameterize(org.apache.avalon.framework.parameters.Parameterizable)
  6. initialize(org.apache.avalon.framework.activity.Initializable)
  7. start(org.apache.avalon.framework.activity.Startable)
開始後(suspend&resume)
  1. suspend(org.apache.avalon.framework.activity.Suspendable)
  2. recontextualize(org.apache.avalon.framework.context.Recontextualizable)
  3. recompose(org.apache.avalon.framework.component.Recomposable)
  4. reconfigure(org.apache.avalon.framework.configuration.Reconfigurable)
  5. reparameterize(org.apache.avalon.framework.parameters.Reparameterizable)
  6. resume(org.apache.avalon.framework.activity.Suspendable)
停止時
  1. stop(org.apache.avalon.framework.activity.Startable)
  2. dispose(org.apache.avalon.framework.activity.Disposable)
  3. finalize(ガベージコレクタからの呼び出し/java.lang.Object)

というわけで、こういった interface を implemts して、クラスを書いてやればいい。とはいえ、Echo はインターネット・サーバである....ということは、ポートをリスンして、接続があれば、そのソケットに応じてスレッドを立ち上げて、接続ごとに個別の処理をさせる必要がある。

これを解決する抽象化が「ハンドラ」である。言い替えると、「接続ごとに(サービスを実装した)ハンドラが起動される」ように、あらかじめ設定をしておく...というのが、assembly.xml で「サービスの実装クラス」として定義した、jp.or.nurs.sug.phoenix.echo.EchoServer の仕事になる。で、個別のEcho サービスのハンドラは jp.or.nurs.sug.phoenix.echo.EchoHandler というクラスになる。

とはいえ、このハンドラ自体も、フレームワークによって「プール」される。無闇にハンドラを生成せず、一度生成されたハンドラを使いまわすわけだな。同様にそのハンドラと結び付いたスレッドも、「プール」による使いまわしがなされる(ここらへんが Thread-Manager)。だから、自分でハンドラとかスレッドとか生成することなしに、そういう生成はすべて「フレームワークにお任せ!」してしまうわけだ。ここらへんがフレームワークの利点なんだな。

ただし、このプール機能(org.apache.avalon.excalibur.pool)を使うためには、「どういうオブジェクトを生成するか?」を汎用的なプール・メカニズムに対して伝えてやらなくてはならない。まあ、この方法論は「Factory」でやるに決まってる。というわけで、sug.EchoHandlerFactory という Factory クラスを作ってやる必要がある...で、sug.EchoSerivce は、そういう Handler たちを生成するクラスなので、あらかじめ cornerstone で作られている、org.apache.avalon.cornerstone.services.connection.AbstractHandlerFactory クラスを継承することにしよう。要するにこいつは「インターネット・サーバ用」にうまく実装された基底クラスなわけである。だからロガーとかも基底クラスの側であらかじめ用意していてくれるので楽ちんだ。

だからソースは次の3ファイルだ。

  1. jp.or.nurs.sug.phoenix.echo.EchoService: サービス本体。AbstractHandlerFactory を継承して、Avalon フレームワークで定められたライフサイクルの interface をいろいろ implements する。
  2. jp.or.nurs.sug.phoenix.echo.EchoHandlerFactory: ハンドラの Pool 機能を使うための、Factory クラスである。そのため、org.apache.avalon.excalibur.pool.ObjectFactory クラスを implements する。
  3. jp.or.nurs.sug.phoenix.echo.EchoHandler: 接続ごとに呼び出されるハンドラ。ログを取れるように、org.apache.avalon.framework.logger.AbstractLogEnabled を継承し、ハンドラとして呼び出されるように org.apache.avalon.cornerstone.services.connection.ConnectionHandler を implements する。あと、Pool されるようにマーカー・インターフェイスとして、org.apache.avalon.excalibur.pool.Poolable を implements しておく。

....Excalibur はこういう風に大活躍しているわけである。

EchoService の実装

じゃあ、まずサービス本体である、jp.or.nurs.sug.phoenix.echo.EchoService だ。フレームワークなので、大量に Avalon 関連クラスを import する。

ちなみに、このクラスは org.apache.avalon.framework.component.Component interface も implements している。まあ、これは Composable の compose() メソッドで返る Manager が、lookup() する時のマーカーインターフェイスみたいなもののようだ。とはいえ最近では、deprecated 扱いのようなので、そのうち要らなくなるようである。

package jp.or.nurs.sug.phoenix.echo;

import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.context.Contextualizable;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.component.Composable;
import org.apache.avalon.framework.component.Component;
import org.apache.avalon.framework.component.DefaultComponentManager;
import org.apache.avalon.framework.component.ComponentManager;
import org.apache.avalon.framework.context.Context;
import org.apache.avalon.framework.logger.Logger;
import org.apache.avalon.framework.logger.LogEnabled;

import org.apache.avalon.excalibur.pool.ObjectFactory;
import org.apache.avalon.excalibur.pool.Pool;
import org.apache.avalon.excalibur.pool.Poolable;
import org.apache.avalon.excalibur.pool.HardResourceLimitingPool;
import org.apache.avalon.excalibur.thread.ThreadPool;

import org.apache.avalon.cornerstone.services.sockets.SocketManager;
import org.apache.avalon.cornerstone.services.sockets.ServerSocketFactory;
import org.apache.avalon.cornerstone.services.threads.ThreadManager;
import org.apache.avalon.cornerstone.services.connection.ConnectionManager;
import org.apache.avalon.cornerstone.services.connection.AbstractHandlerFactory;
import org.apache.avalon.cornerstone.services.connection.ConnectionHandlerFactory;
import org.apache.avalon.cornerstone.services.connection.ConnectionHandler;

import java.net.ServerSocket;

public class EchoService extends AbstractHandlerFactory
    implements Contextualizable, Composable, Configurable, 
               Initializable, Component, ConnectionHandlerFactory {
    private DefaultComponentManager compMgr; // 自分自身のManager。使うサブサービスを拾い出す
    private Context context;     // コンテキスト
    private Configuration conf;  // config.xml での設定内容
    private Logger logger = null;  // ロガー

    // config.xml での設定内容
    private int port = -1;
    private int maxconnect = 10;
    private int minconnect = 1;

    // 内部使用のオブジェクトたち
    // ハンドラの Factory
    private ObjectFactory theHandlerFactory = new EchoHandlerFactory();
    private Pool theHandlerPool = null;
    // マネージャたち
    private SocketManager socketManager;
    private ThreadManager threadManager;
    private ConnectionManager connectionManager;

グローバルな定義はこれだけでOK。あとは implements された interface に沿って、メソッドを作っていく。まず単純なものから。単に引数で渡されたオブジェクトを内部に保存するだけでOKである。

    /**
     * Contextualizable interface が要求する
     */
    public void contextualize(final Context context) {
        this.context = context;
    }

    /**
     * Composable interface が要求する
     */
    public void compose(ComponentManager comp) {
        compMgr = new DefaultComponentManager(comp);
    }

    /**
     * Configurable interface が要求する
     */
    public void configure(Configuration conf) {
        this.conf = conf;
    }

さて、ライフサイクルの初期化フェーズで最後に呼び出されるのが initialize() である。ここでそれまでに渡されたコンテキスト変数を使って、実際の設定をする。

    /**
     * Initalizable interface が要求する。
     */
    public void initialize() throws Exception {
        getLogger().info("SugTest initialize...");
        // config.xml から設定項目を抽出する
        port = conf.getChild( "port" ).getValueAsInteger( 2531 );
        maxconnect = conf.getChild( "max-connect" ).getValueAsInteger( 10 );
        minconnect = conf.getChild( "min-connect" ).getValueAsInteger( 3 );
        getLogger().info("SugTest port=" + port + " connect " + minconnect 
                         + "-" + maxconnect );

        // 使う Manager たちを検索する
        socketManager = (SocketManager) compMgr.lookup(SocketManager.ROLE);
        threadManager = (ThreadManager) compMgr.lookup(ThreadManager.ROLE);
        connectionManager = (ConnectionManager) compMgr.lookup(ConnectionManager.ROLE);

        // ServerSoket を作って、リスンを開始する
        // "plain" は SSL ではないことの指定にすぎない。
        ServerSocketFactory factory = socketManager.getServerSocketFactory("plain");
        // 5 == 生成ソケット数, null == 特定のIPに固定するなら
        ServerSocket serverSocket = factory.createServerSocket(port, 5, null);
        // スレッドに結び付ける
        ThreadPool threadPool = threadManager.getDefaultThreadPool();
        connectionManager.connect("", serverSocket, this, threadPool);

        // ハンドラのプールを作成する
        theHandlerPool = new HardResourceLimitingPool(theHandlerFactory, 
                                                      minconnect, maxconnect );
        if (theHandlerPool instanceof LogEnabled) {
            ((LogEnabled)theHandlerPool).enableLogging(getLogger());
        }
        if (theHandlerPool instanceof Initializable) {
            ((Initializable)theHandlerPool).initialize();
        }

        System.out.println( "Echo Service started at port " + port );
    }

まあ、ビビるほど難しいものではない。以前も少し説明したが、

        // 使う Manager たちを検索する
        socketManager = (SocketManager) compMgr.lookup(SocketManager.ROLE);
        threadManager = (ThreadManager) compMgr.lookup(ThreadManager.ROLE);
        connectionManager = (ConnectionManager) compMgr.lookup(ConnectionManager.ROLE);

        // ServerSoket を作って、リスンを開始する
        // "plain" は SSL ではないことの指定にすぎない。
        ServerSocketFactory factory = socketManager.getServerSocketFactory("plain");
        // 5 == 生成ソケット数, null == 特定のIPに固定するなら
        ServerSocket serverSocket = factory.createServerSocket(port, 5, null);
        // スレッドに結び付ける
        ThreadPool threadPool = threadManager.getDefaultThreadPool();
        connectionManager.connect("", serverSocket, this, threadPool);

のあたりは、「Avalonコンポーネントの集合体」である Phoenix アプリの、それらの「コンポーネントの間の参照」をしている部分である。「バラバラなコンポーネントの寄せ集め」なのだが、それらの相互作用は、compose() で渡された ComponentManager を介して行う。言い替えると、compMgr.lookup( キー名 ) で「必要とする他のコンポーネント」を参照できるようになるのである。だから、他のコンポーネント(ここでは Cornerstone で作られた各種サービスのコンポーネント)をこれでルックアップし、得られた他のコンポーネントに対していろいろな設定をしているわけである。

ただし、この ComponentManager のルックアップのためには、ちょいとした追加情報として「*.xinfo」というファイルが必要だったりするが、これについてはすぐ後で触れる

では EchoServer の最後は、ConnectionHandlerFactory な部分だ。接続ごとに newHandler() が呼び出されるので、ここで実際に作業をするハンドラを返す。勿論プールを使っているから、theHandlerPool から取り出してそれを返せばOKだ。

実際にはこのクラスの抽象基底クラスである AbstractHandlerFactory で、抽象メソッドになっているのは「作る」側の newHandler() だけなので、これだけ実装してもイイのだが、実際にはプールに「返す」側の releaseConnectionHandler() の上書きも必要だ。まあ、implements している ConnectionHandlerFactory interface も、どうせここらへんのインターフェイス定義(AbstarctHandlerFactory で実装されている createConnectionHandler() と、releaseConnectionHandler() のinterface)に過ぎない。

    /* AbstractHandlerFactory が要求する。 
         接続されたのでハンドラを返す */
    protected ConnectionHandler newHandler()
            throws Exception {
        EchoHandler theHandler = (EchoHandler)theHandlerPool.get();
        theHandler.enableLogging(getLogger());
        return theHandler;
    }

    /* 切断されたのでハンドラをプールに戻す */
    public void releaseConnectionHandler( ConnectionHandler connectionHandler ) {
        if (!(connectionHandler instanceof EchoHandler)) {
            throw new IllegalArgumentException("Attempted to return non-EchoHandler to pool.");
        }
        theHandlerPool.put((Poolable)connectionHandler);
    }

いかがかな?そう難しいものではないと御理解頂けると思うのだが。

EchoService.xinfo

とはいえ、規約上、この jp.or.nurs.sug.phoenix.echo.EchoService に対して、同じディレクトリに「ブロックとしての設定ファイル」 EchoService.xinfo が必要である。実際にはサブサービスである socketManager たちは、この EchoService.xinfo の記述から、そのサブサービスのマネージャを取得している。言い替えると、

        // 使う Manager たちを検索する
        socketManager = (SocketManager) compMgr.lookup(SocketManager.ROLE);
        threadManager = (ThreadManager) compMgr.lookup(ThreadManager.ROLE);
        connectionManager = (ConnectionManager) compMgr.lookup(ConnectionManager.ROLE);

は、この EchoService.xinfo の記述

<?xml version="1.0"?>
<blockinfo>
  <block>
    <version>1.0</version>
  </block>

  <dependencies>
    <dependency>
      <service name="org.apache.avalon.cornerstone.services.sockets.SocketManager" 
          version="1.0"/>
    </dependency>
    <dependency>
      <service name="org.apache.avalon.cornerstone.services.threads.ThreadManager" 
          version="1.0"/>
    </dependency>
    <dependency>
      <service name="org.apache.avalon.cornerstone.services.connection.ConnectionManager" 
          version="1.0"/>
    </dependency>
  </dependencies>

</blockinfo>

から、各Manager として assembly.xml を介して実装クラスのオブジェクトを取得することになる。まあ、模式的に書けば、

  1. compMgr.lookup(マネージャのID) の呼び出し
  2. 自分のクラスの xinfo ファイル(EchoService.xinfo)から、role を取得する。
  3. assembly.xml の role → name から、block で定義された実装クラスを取得する。

という流れのようである。まあここらへん、「設定で動的に実装を差し替える」という機能を実現しているのが判ると思う。



copyright by K.Sugiura, 1996-2006