James君!〜Avalonから見たJames

クラス関連の「地図」

さて、今からは「James を使う!」というよりも、今までの説明の中で「何でこういう設定体系になっているのか??」という疑問に答えていく...というノリの解説になってくる。これは言うまでもなく、James が Phoenix(Avalon) ベースのサーバであり、「James では」というよりも「Phoenix(Avalon)では」で答えた方が適切な疑問について、答えていく...ということである。

まあ、この Avalon はデカ過ぎて謎なフレームワークなので、筆者と言えども完全な理解をしているとウヌぼれているわけではない。とりあえず James を手引きとして、Phoenix(Avalon)を理解する、という風なノリであることをあらかじめお断りしておく。

少し各パッケージの役割分担についてまとめておこう。これが皆さんにとっての一種の「地図」になる。

James(org.apache.jamesパッケージ)
メールサーバである James の実装を担当する部分(既出)
Mailet(org.apache.Mailetパッケージ)
James を特徴づける Mailet&Matcher のベース実装とインターフェイス(既出)
Phoenix(org.apache.avalon.phoenixパッケージ)
Avalonベースのサーバ・コンポーネントなら何でも起動するランチャー(主として bin/phoenix-loader.jar など)。実際には「ランチャー自体と起動環境」の担当なので、ここではほとんど解説しない。
Avalon Framework(org.apache.avalon.frameworkパッケージ)
実際の Avalon のコンポーネントを定義する。言い替えると「Avalonベースの開発」とは、ここで定義されるコンポーネント規約に従った「コンポーネント」を作ることである。今から見ると意外に先進的なプロジェクトであったようで、現在流行中の「POJOベースの開発」とか「IoCコンテナ」とか、そういうのを先取りしているフレームワークである。
Avalon Excalibur(org.apache.avalon.excaliburパッケージ)
Avalon Framework で使うさまざまなライブラリ集。雰囲気は jakarta-commons に似ている。結構把握しきれない程のサブパッケージがあるのも commons みたいだな。例えば excalibur-colletions, excalibur-pool, excalibur-thread といったパッケージを James では使っている。ちょいとここらへんが Avalon 分裂のアオリを食らって、コンポーネント構成を見直している感じがある。JDK1.5 とか jakarta-commons と統合するような流れがあるみたいだな...どうなるか、今一つ良く判らん...
Cornerstone(org.apache.avalon.cornerstone)
Avalon Framework 規約に則った、James が使うさまざまなサブコンポーネントの実装。要するに Phoenix ベースのサーバを使う場合には、この Cornerstone 所属のコンポーネントをいろいろなサーバで使える「デフォルト実装」として使うのである。たとえば、SocketManager であるとか、ThreadManager, ConnectionManager といったものを James は使っている。ただし、このパッケージは james.sar の中に入っているので、後で解凍したのをコピーして使うことにする。

という感じである。ややこしいな。

Echoサーバの構成

では今から James をカンニングして、Phoenix ベースの適当なサーバを作ってみせる。まあ、どんなサーバでもイイのだが、ごく簡単なところで Echo サーバくらいにしようか。要するに TCP 接続して、リクエストをそのままレスポンスとして返すアレである。としてみると、その Echo サーバの構成はこんな感じになる。

phoenix-echo -+- apps --- sugecho.sar
              +- bin  -+- run.sh      起動スクリプト1
              |        +- phoenix.sh  起動スクリプト2
              |        +- phoenix-loader.jar  Phoenix のローダ
              |        +- lib --- phoenix-engine.jar など。
              +- conf -+- kernel.xml Phoenix 自体の設定(いじらない)
              |        +- wrapper.xml Phoenix の起動環境設定(いじっても良いが..)
              +- lib   avalon-framework-*.jar, excalibur-*.jar が大量にある。
              +- logs --- phoenix.log   Phoenix 自体のログファイル
              +- temp -+- phoenix.console  サーバモードで起動した場合のコンソールログ
              |        +- phoenix.pid  サーバモードで起動した場合の PID ファイル
              +- work --- sugecho-数値/  sugecho.sar を展開する作業ディレクトリ

上記部分は James で使ったものをそのまま使ってもイイくらいなものだ。Echo サーバの実装として開発する sugecho.sar の中身は...

sugecho.sar -+- SAR-INF -+- assembly.xml   アプリの構成を記述する 
             |           +- config.xml     サーバとしての設定ファイル
             |           +- environment.xml ロガー設定
             |           +- lib  -+- sugecho.jar 実装のクラスファイル
             |                    +- cornerstone.jar
             +- META-INF --- MANIFEST.MF

くらいになる。James の設定では config.xml ほぼだけをイジって「Jamesサーバの設定」をしたが、実際にはこの config.xml はアプリごとのフリーフォーマットな XML に過ぎない。別なサーバを開発するのならば、その構成自体は勝手に作ればよいことになる。James 設定ではまったくいじらなかった assembly.xml が Echo サーバではマジで書かなくてはならない部分になる。要するに assembly.xml が、「そのサーバの構成」を Phoenix に伝えるのである。

assembly.xml

じゃあ、assembly.xml の内容は...というと、Echo サーバではこんな感じだ。

<?xml version="1.0"?>
<assembly>
  <block name="EchoService" class="jp.or.nurs.sug.phoenix.echo.EchoService" >
    <provide name="sockets"
             role="org.apache.avalon.cornerstone.services.sockets.SocketManager"/>
    <provide name="connections"
             role="org.apache.avalon.cornerstone.services.connection.ConnectionManager"/>
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <block name="sockets"
         class="org.apache.avalon.cornerstone.blocks.sockets.DefaultSocketManager"/>

  <block name="connections"
         class="org.apache.avalon.cornerstone.blocks.connection.DefaultConnectionManager" >
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <block name="thread-manager"
         class="org.apache.avalon.cornerstone.blocks.threads.DefaultThreadManager" />

</assembly>

要するに構造は、

assembly -* block -* provide

という簡単なものだが、これがサービス自体を「Phoenix に伝える」重要な役割を果たす。ここでは EchoService というサービス(実装クラス: jp.or.nurs.sug.phoenix.echo.EchoService)をすることを定義し、それが3つのサブサービスを利用する(provideタグ)ことを伝えている。3つのサブサービス(sockets, connections, thread-manager)は、それぞれインターフェイスの宣言を EchoService ブロックの中に書き、独立したブロックとして sokets ブロックなどの中で、その実装クラス(org.apache.avalon.cornerstone.blocks.sockets.DefaultSocketManager)を指定している。まあ、これは見ると「なるほど...」という風に理解できると思う。

ここで一番重要なポイントは、assembly.xml で定義したブロック名「EchoService」が、他の設定でも「そのサービスの名前」を特定するのに使われる、ということである。なので、サービス名「EchoService」で以降で参照されることを念頭に置いて欲しい。

だったら、James の assembly.xml だって同様だ。

<?xml version="1.0"?>
<assembly>
  <!-- The James block  -->
  <block name="James" class="org.apache.james.James" >
    <!-- このブロックではどのコンポーネントがサービスを提供するか特定する。
      role はそのコードと .xinfo ファイルによって特定される。その名前は、
      この XML ファイルの記述と一致していなければならない。 -->
    <provide name="dnsserver" role="org.apache.james.services.DNSServer"/>
    <provide name="mailstore" role="org.apache.james.services.MailStore"/>
    <provide name="users-store" role="org.apache.james.services.UsersStore"/>
    <provide name="sockets"
             role="org.apache.avalon.cornerstone.services.sockets.SocketManager"/>
    <provide name="connections"
             role="org.apache.james.services.JamesConnectionManager"/>
    <provide name="scheduler"
             role="org.apache.avalon.cornerstone.services.scheduler.TimeScheduler"/>
    <provide name="database-connections"
             role="org.apache.avalon.cornerstone.services.datasource.DataSourceSelector" />
  </block>

  <!-- The James Spool Manager block  -->
  <block name="spoolmanager" class="org.apache.james.transport.JamesSpoolManager" >
    <provide name="James" role="org.apache.mailet.MailetContext"/>
    <provide name="mailstore" role="org.apache.james.services.MailStore"/>
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <block name="dnsserver" class="org.apache.james.dnsserver.DNSServer" />

  <block name="remotemanager" class="org.apache.james.remotemanager.RemoteManager" >
    <provide name="mailstore" role="org.apache.james.services.MailStore"/>
    <provide name="users-store" role="org.apache.james.services.UsersStore"/>
    <provide name="sockets"
             role="org.apache.avalon.cornerstone.services.sockets.SocketManager"/>
    <provide name="connections"
             role="org.apache.james.services.JamesConnectionManager"/>
    <provide name="James" role="org.apache.james.services.MailServer"/>
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <!-- POP3 Server -->
  <block name="pop3server" class="org.apache.james.pop3server.POP3Server" >
    <provide name="mailstore" role="org.apache.james.services.MailStore"/>
    <provide name="users-store" role="org.apache.james.services.UsersStore"/>
    <provide name="sockets"
             role="org.apache.avalon.cornerstone.services.sockets.SocketManager"/>
    <provide name="connections"
             role="org.apache.james.services.JamesConnectionManager"/>
    <provide name="James" role="org.apache.james.services.MailServer"/>
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <!-- SMTP Server -->
  <block name="smtpserver" class="org.apache.james.smtpserver.SMTPServer" >
    <provide name="James" role="org.apache.mailet.MailetContext"/>
    <provide name="mailstore" role="org.apache.james.services.MailStore"/>
    <provide name="users-store" role="org.apache.james.services.UsersStore"/>
    <provide name="sockets"
             role="org.apache.avalon.cornerstone.services.sockets.SocketManager"/>
    <provide name="connections"
             role="org.apache.james.services.JamesConnectionManager"/>
    <provide name="James" role="org.apache.james.services.MailServer"/>
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <!-- NNTP Server -->
  <block name="nntpserver" class="org.apache.james.nntpserver.NNTPServer" >
    <provide name="users-store" role="org.apache.james.services.UsersStore"/>
    <provide name="sockets"
             role="org.apache.avalon.cornerstone.services.sockets.SocketManager"/>
    <provide name="connections"
             role="org.apache.james.services.JamesConnectionManager"/>
    <provide name="nntp-repository"
             role="org.apache.james.nntpserver.repository.NNTPRepository"/>
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <!-- NNTP Repository -->
  <block name="nntp-repository" class="org.apache.james.nntpserver.repository.NNTPRepositoryImpl" />

  <!-- FetchPOP Service -->
  <block name="fetchpop" class="org.apache.james.fetchpop.FetchScheduler" >
    <provide name="scheduler"
             role="org.apache.avalon.cornerstone.services.scheduler.TimeScheduler"/> 
    <provide name="James" role="org.apache.james.services.MailServer"/>      
  </block>
  
  <!-- FetchMail Service -->
  <block name="fetchmail" class="org.apache.james.fetchmail.FetchScheduler" >
    <provide name="scheduler"
             role="org.apache.avalon.cornerstone.services.scheduler.TimeScheduler"/> 
    <provide name="James" role="org.apache.james.services.MailServer"/>
    <provide name="users-store" role="org.apache.james.services.UsersStore"/>          
  </block>   

  <!-- The High Level Storage block -->
  <block name="mailstore" class="org.apache.james.core.AvalonMailStore" >
    <provide name="objectstorage"
             role="org.apache.avalon.cornerstone.services.store.Store"/>
    <provide name="database-connections"
             role="org.apache.avalon.cornerstone.services.datasource.DataSourceSelector" />
  </block>

  <!-- The User Storage block -->
  <block name="users-store" class="org.apache.james.core.AvalonUsersStore" >
    <!-- Configure file based user store here, defaults should be fine -->
    <provide name="objectstorage"
             role="org.apache.avalon.cornerstone.services.store.Store"/>
    <provide name="database-connections"
             role="org.apache.avalon.cornerstone.services.datasource.DataSourceSelector" />
  </block>


  <!-- Cornerstone のブロックの設定。これ以降は変更してはならない。
      (セキュアなソケット(TLS)を使いたい場合は例外だが) -->

  <!-- The Storage block -->
  <block name="objectstorage"
         class="org.apache.avalon.cornerstone.blocks.masterstore.RepositoryManager" />

  <!-- The Connection Manager block -->
  <block name="connections"
         class="org.apache.james.util.connection.SimpleConnectionManager" >
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <!-- The Socket Manager block -->
  <block name="sockets"
         class="org.apache.avalon.cornerstone.blocks.sockets.DefaultSocketManager"/>

  <!-- The Time Scheduler block -->
  <block name="scheduler"
         class="org.apache.avalon.cornerstone.blocks.scheduler.DefaultTimeScheduler" >
    <provide name="thread-manager"
             role="org.apache.avalon.cornerstone.services.threads.ThreadManager" />
  </block>

  <!-- The DataSourceSelector block -->
  <block name="database-connections"
         class="org.apache.avalon.cornerstone.blocks.datasource.DefaultDataSourceSelector" />

  <!-- The ThreadManager block -->
  <block name="thread-manager"
         class="org.apache.james.util.thread.DefaultThreadManager" />

</assembly>

James はいくつものサービスを提供するので、その各サービスがここでのブロックとなっている。James で実装されているサービス(コンポーネント)と、Cornerstone で実装されているサブサービスのコンポーネントとがあるのが判るだろう。言い替えると、James はこういったコンポーネント化されたサービスの集合体にすぎない、というわけだ。「すべてはAvalonのコンポーネントである」というのが、この Avalon ベース開発の一番中心的な命題である。だから、以前「疑問?」とした James の config.xml の階層構造の謎もこれで解決されたのではないかと思う。sockets や thread-manager などの細かいサブユニットも...

すべて Avalon のコンポーネントだ!
(実際には Cornerstone で作られたモノだが...)

Echo サービスの config.xml

ではついでなので、Echo サービスの config.xml を示す。これはアプリが違えば設定内容を勝手に変えてイイものである(当り前だな)。ごくごく単純なものだが、これで十分だ。

<?xml version="1.0"?>
<config>
   <!-- EchoService 自体の設定 -->
   <EchoService>
      <!-- リスンするポート -->
      <port>2599</port>
      <!-- 最大接続数(最大のハンドラの数) -->
      <max-connect>30</max-connect>
      <!-- あらかじめ用意しておくハンドラの数 -->
      <min-connect>1</min-connect>
   </EchoService>

   <!-- 以降は使うサブサービスの設定項目。James のものと同様 -->
   <connections>
      <idle-timeout>300000</idle-timeout>
      <max-connections>30</max-connections>
   </connections>

   <sockets>
      <server-sockets>
         <factory name="plain" 
          class="org.apache.avalon.cornerstone.blocks.sockets.DefaultServerSocketFactory"/>
      </server-sockets>
      <client-sockets>
         <factory name="plain" 
          class="org.apache.avalon.cornerstone.blocks.sockets.DefaultSocketFactory"/>
      </client-sockets>
   </sockets>

   <thread-manager>
      <thread-group>
         <name>default</name>
         <priority>5</priority>
         <is-daemon>false</is-daemon>
         <max-threads>100</max-threads>
         <min-threads>20</min-threads>
         <min-spare-threads>20</min-spare-threads>
      </thread-group>
   </thread-manager>
</config>

勝手に <EchoService> なんてタグを使っているが、これは assembly.xml で定義されたブロック(=サービス名)だから、これが「EchoServiceブロック用の設定である」という風に理解できるわけだ。そういうかたちでサービスと設定項目を結び付けていることに注意されたい。

Avalon Logkit と environment.xml

実際、ログ設定ファイルである environment.xml も、こういう考え方で設定される。ブロック単位の識別名として、先ほどのブロック名が使われて、ブロック単位でロガーが設定される。これがログの「カテゴリー」となり、Avalon の Logkit ではそれぞれのカテゴリーごとにログファイルを割り当てる、という考え方だ。まあ、そういう面を別にすると、Log4j に慣れた向きにはあまり柔軟ではないように感じられるのではないだろうか。細かいサブカテゴリー設定をするには、アプリ上でのコードサポートが必要だったりする(James だと James.Mailet というサブカテゴリーをわざわざ生成している。org.apache.james.James#getMailetLogger を参照のこと)わけだ。

まあ、それでも出力フォーマットとか、ローテーション指定はできる。ここらへんはタグの名前を見ると、そこそこ見当がつくのではないか、と思う。より詳しくは「Avalon Logger」を見てくれたまえ。

<?xml version="1.0"?>
<server>
  <logs version="1.1">
    <factories>
      <factory type="file" 
         class="org.apache.avalon.excalibur.logger.factory.FileTargetFactory"/>
    </factories>

    <categories>
      <category name="" log-level="INFO">
        <log-target id-ref="default"/>
      </category>
      <category name="EchoService" log-level="INFO">
        <log-target id-ref="echoservice"/>
      </category>
      <category name="sockets" log-level="INFO">
        <log-target id-ref="sockets-target"/>
      </category>
      <category name="connections" log-level="INFO">
        <log-target id-ref="connections-target"/>
      </category>
    </categories>

    <targets>
      <file id="default">
        <filename>${app.home}/logs/default</filename>
        <format>%{time:dd/MM/yy HH:mm:ss} %5.5{priority} %{category}: %{message}\n%{throwable}</format>
        <append>true</append>
        <rotation type="unique" pattern="-yyyy-MM-dd-HH-mm" suffix=".log">
          <or>
            <date>dd</date>
            <size>10485760</size>
          </or>
        </rotation>
      </file>
      <file id="echoservice">
        <filename>${app.home}/logs/echoservice</filename>
        <format>%{time:dd/MM/yy HH:mm:ss} %5.5{priority} %{category}: %{message}\n%{throwable}</format>
        <append>true</append>
        <rotation type="unique" pattern="-yyyy-MM-dd-HH-mm" suffix=".log">
          <or>
            <date>dd</date>
            <size>10485760</size>
          </or>
        </rotation>
      </file>
      <file id="connections-target">
        <filename>${app.home}/logs/connections</filename>
        <format>%{time:dd/MM/yy HH:mm:ss} %5.5{priority} %{category}: %{message}\n%{throwable}</format>
        <append>true</append>
        <rotation type="unique" pattern="-yyyy-MM-dd-HH-mm" suffix=".log">
          <or>
            <date>dd</date>
            <size>10485760</size>
          </or>
        </rotation>
      </file>
      <file id="sockets-target">
        <filename>${app.home}/logs/sockets</filename>
        <format>%{time:dd/MM/yy HH:mm:ss} %5.5{priority} %{category}: %{message}\n%{throwable}</format>
        <append>true</append>
        <rotation type="unique" pattern="-yyyy-MM-dd-HH-mm" suffix=".log">
          <or>
            <date>dd</date>
            <size>10485760</size>
          </or>
        </rotation>
      </file>
    </targets>
  </logs>
</server>

まあ、James の方の environment.xml なんかは長くてウットオしいだけなので、自分で見てくれ。とはいえ、例えば「ソケットについてもっと細かいログを取りたい!」と思うんだったら、

      <category name="sockets" log-level="DEBUG">
        <log-target id-ref="sockets-target"/>
      </category>

という風にしてやればイイのは明白だな。ログレベルとしてはキビしい順に FATALERROR, ERROR, WARN, INFO, DEBUG が使える。



copyright by K.Sugiura, 1996-2006