James君!〜ハンドラの実装

Pool 用 Factory クラス jp.or.nurs.sug.phoenix.echo.EchoHandlerFactory

さて、今度は「実際のサービス」を担当する部分ということになる。とはいえ、サービス本体の jp.or.nurs.sug.phoenix.echo.EchoService で、「実際のサービス」を担当する「ハンドラ」を「プールして」使う、という仕様になっているために、その「プール用」の Factory クラスをまず紹介しておこう。これはかなり定型的なものだ。単に ObjectFactory インターフェイスで要求されるメソッドを実装すればイイだけだ。

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

import org.apache.avalon.excalibur.pool.ObjectFactory;

public class EchoHandlerFactory implements ObjectFactory {
    /* 
     * ObjectFactory interface が要求する。
    ハンドラのインスタンスを生成する
     */
    public Object newInstance() throws Exception {
	return new EchoHandler();
    }
    
    /**
     * ObjectFactory interface が要求する。
     ハンドラのクラスを返す
     */
    public Class getCreatedClass() {
	return EchoHandler.class;
    }
    
    /**
     * ObjectFactory interface が要求する。
     */
    public void decommission( Object object ) throws Exception {
	return;
    }
}

ハンドラクラス jp.or.nurs.sug.phoenix.echo.EchoHandler の実装

じゃあ最後はハンドラ自体の紹介だ。これは実際にプールされる対象になるので、Poolable マーカーインターフェイスを implements することになるし、ConnectionHandler なんだから、ConnectionHandler interface も implements する。

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

import org.apache.avalon.framework.logger.AbstractLogEnabled;
import org.apache.avalon.cornerstone.services.connection.ConnectionHandler;
import org.apache.avalon.excalibur.pool.Poolable;

import java.io.*;
import java.net.*;

public class EchoHandler extends AbstractLogEnabled
    implements ConnectionHandler, Poolable {

    /**
     * ConnectionHandler interface が要求する
     */
    public void handleConnection( final Socket connection )
        throws IOException {

	// ログ内容を作る
	String threadName = Thread.currentThread().getName();
        Socket socket = connection;
        String remoteIP = socket.getInetAddress().getHostAddress();
        String remoteHost = socket.getInetAddress().getHostName();
	System.out.println( threadName + "(" + this + "):EchoHandler: handleConnection ENTER" );
	System.out.println( "\tconnected by " + remoteHost + "(" + remoteIP + ")" );
        getLogger().info( threadName + "(" + this + "):EchoHandler: handleConnection ENTER\n" + 
			  "\tconnected by " + remoteHost + "(" + remoteIP + ")" );
        // リクエスト→レスポンス。かなり定型的
        try {
            BufferedReader in = new BufferedReader(
                  new InputStreamReader(socket.getInputStream(), "ASCII"), 512);
            PrintWriter out = new PrintWriter(
                  new BufferedWriter(
                      new OutputStreamWriter(socket.getOutputStream())
                                     , 512), false);
	    String mess;
	    do {
		mess = in.readLine().trim();  // 読む
		System.out.println( threadName + ":read " + mess );
		getLogger().debug( threadName + ":read " + mess );
		out.println( mess );  // 書く
		out.flush();  // flush() 要るぞ。
		System.out.println( threadName + ":write " + mess );
		getLogger().debug( threadName + ":write " + mess );
		if( mess.equals( "QUIT" ) ) {
		    break;
		}
	    } while( mess != null );
	} catch( Exception e ) {
	    // クライアントが勝手に切断した場合
	    System.out.println( threadName + ":disconnected" );
	    getLogger().debug( threadName + ":disconnected" );
	}
	if( socket != null ) {
	    try {
		socket.close();
	    } catch( IOException e ) { /* NOP */ }
	}
    }
}

まあ、ここでは org.apache.avalon.framework.logger.AbstractLogEnabled を継承しているので、ロギング用の getLogger() が使えちゃったりするのである。Java でサーバを書いたことがあれば、こんなん楽勝だよね。

ということは、実質上、サーバの本体として jp.or.nurs.sug.phoenix.echo.EchoService を書き、ハンドラとして jp.or.nurs.sug.phoenix.echo.EchoHandler を書くだけで、ソケットに関するややこしい設定なしに、接続を受け付けた後でスレッドを起動して...とか、特にコードのサポートをすることなしに、それらが「プールされた効率のよいかたち」で、書くことができちゃうわけだ。これはフレームワークの大きなメリットだわな。

まあ、「書く」とは言っても、jp.or.nurs.sug.phoenix.echo.EchoService の側は、かなり定型性が強いものだ。もし複数のサービスを実装するサーバを書くのならば、「設定項目とかは個別のクラスで処理させるにせよ、アプリ上の抽象基底クラスでまとめちゃいたいな!」と考えたくなるような、その程度のものである。実際 James だと、org.apache.james.core.AbstarctJamesService というサービスの基底クラスを用意して、NNTPServer, POP3Server, RemoteManager, SMTPServer がこれを継承するかたちで書かれていたりする。まあ、「一度書いておけば何度でも使え」そうなクラスに過ぎない。

というわけでほぼハンドラだけに専念して実装すればイイということになるわけだ。これってフレームワークらしいよね。

build.xml

じゃあ、最後にこのアプリで使う build.xml を紹介しておく。

今まで紹介した「Phoenix上のEchoサーバ」は、

から取得できる。こんなん PDS でいいぞ。テキトーに遊んでくれたまえ。とはいえ、ライブラリ類はまったくこれには入ってなくて、めちゃ軽量だ。要するに、こいつの build.xml で、James のディレクトリから必要なファイルを集めて環境を作るようにしてある。だから、まあこんな感じでやってくれ。

$ tar xvfz echo.tgz
$ cd echo
# build.xml の編集
$ ant setup
$ ant install
$ cd ../phoenix-echo
$ chown 755 bin/*.sh
$ bin/run.sh

じゃあ、build.xml の内容。あなたが James をインストールしたディレクトリを、james.dir に設定するか、環境変数 JAMES_HOME に設定してくれ。あと、cornerstone.jar が開発に要るのだが、これはひょっとしたら james インストールディレクトリの中に見当たらないかもしれない。まあ、これはどうせ james.sar の中に固まっているから、適当に解凍して「cornerstone.jar があるディレクトリ」を cornerstone.dir プロパティに設定してくれたまえ。

あとは自動で ant setup が

+- echo -- 開発ディレクトリ
+- phoenix-echo  -- James から集めたディレクトリ構成 + apps/sugecho.sar

という感じで phoenix-echo ディレクトリを構築してくれる。

<project name="phoenix" default="compile" basedir=".">
   <property name="debug.flag" value="yes" />
   <property environment="env" />

   <!-- export JAMES_HOME=/usr/local/james/james-2.2.0
       とか環境変数を設定しておいてくれたかな? -->
   <property name="james.dir" value="${env.JAMES_HOME}" />
   <property name="phoenix-echo.dir" value="../phoenix-echo" />
   <property name="phoenix.lib.dir"
      value="${phoenix-echo.dir}/lib" />
   <property name="cornerstone.dir" 
       value="cornerstone.jar があるディレクトリ" />
   <property name="lib.dir" value="./lib" />
   <property name="src" value="./src" />
   <property name="classes" value="./classes" />

  <path id="lib.classpath">
      <pathelement path="${classes}" />
      <pathelement path="${phoenix.lib.dir}/avalon-framework-4.1.3.jar" />
      <pathelement path="${phoenix.lib.dir}/excalibur-pool-1.0.jar" />
      <pathelement path="${phoenix.lib.dir}/excalibur-thread-1.0.jar" />
      <pathelement path="SAR-INF/lib/cornerstone.jar" />
  </path>

  <!-- 環境変数 JAMES_HOME を設定しないとバチが当たる -->
  <target name="init" unless="env.JAMES_HOME" >
     <echo>**********************************</echo>
     <echo>* Please set Env-var: JAMES_HOME *</echo>
     <echo>**********************************</echo>
     <fail message="see above"/>
  </target>

  <target name="setup" depends="init">
     <mkdir dir="SAR-INF/lib" />
     <mkdir dir="${phoenix-echo.dir}" />
     <copy todir="${phoenix-echo.dir}" >
        <fileset dir="${james.dir}" includes="bin/**/*" />
        <fileset dir="${james.dir}" includes="conf/**/*" />
        <fileset dir="${james.dir}" includes="ext/**/*" />
        <fileset dir="${james.dir}" includes="lib/**/*" />
     </copy>
     <!-- cornerstone.dir の設定が正しくないとコケる -->
     <copy file="${cornerstone.dir}/cornerstone.jar"
           todir="SAR-INF/lib" />
     <mkdir dir="${phoenix-echo.dir}/apps" />
  </target>

  <target name="compile" >
     <copy todir="${classes}">
        <fileset dir="${src}" includes="**/*.xml" />
	<fileset dir="${src}" includes="**/*.properties" />
	<fileset dir="${src}" includes="**/*.xinfo" />
     </copy>
     <javac srcdir="${src}" destdir="${classes}" debug="${debug.flag}" 
            classpathref="lib.classpath"/>
  </target>

  <target name="install" depends="compile" >
      <jar jarfile="SAR-INF/lib/sugecho.jar" basedir="${classes}" />
      <jar jarfile="${phoenix-echo.dir}/apps/sugecho.sar" basedir="." 
           includes="SAR-INF/**/*" />
  </target>

  <target name="clean">
     <delete>
         <fileset dir="SAR-INF/src" includes="**/*.class" />
     </delete>
     <delete file="SAR-INF/lib/sugecho.jar" />
  </target>

  <target name="distclean" depends="clean" >
      <delete file="SAR-INF/lib/cornerstone.jar" />
  </target>
</project>



copyright by K.Sugiura, 1996-2006