DIコンテナ30分クッキング

相変わらずJDK1.2で自転車走行していますが、やはりDIコンテナバブルを経験した我々(?)にとって、コンテナが無いのは何かとめんどくさいものです。

   / ̄ ̄\ 
 /   _ノ  \ 
 |    ( ●)(●) 
. |     (__人__)   コンテナ欲しいだろ…
  |     ` ⌒´ノ    常識的に考えて…
.  |         } 
.  ヽ        } 
   ヽ     ノ        \ 
   /    く  \        \ 
   |     \   \         \ 
    |    |ヽ、二⌒)、          \ 


ということで、1.2でも使えそうなコンテナをネットを駆使して当たってみましたが、小さなコンテナでもProxyやバイトコードエンジニアリングを多用していたり、アノテーションバリバリだったり、ジェネリック上等だったりと、「1.2世代の産物ではない」という現実をまざまざと見せ付けられました。


仕方が無いので、機能的には簡単でもいいので、コンテナチックなものを作ることにしました。
あまり手の込んだことはしてもしょうがないので、

・設定ファイルはxml
・ListとMapぐらいはプロパティに使いたい
・型変換はそれなりに

というぐらいの要件で着手です。


まず設定ファイルのパースですが、Digesterを利用しました。
名前が暗号用ライブラリっぽいとか、エラー時にデバックがし難いとかを除けば、個人的にはかなり好きなプロダクトです。もちろん、Strutsに付随しているというのが大きなポイントですが。


次に型変換ですが、BeanUtilを調べた所、Converterがすごいことになっていて、そいつに全部任せることにしました。

BeanUtils.setProperty()を使うと、内部でこれだけのコンバーターが動くようです。

BigDecimalConverter
BigIntegerConverter
BooleanArrayConverter
BooleanConverter
ByteArrayConverter
ByteConverter
CharacterArrayConverter
CharacterConverter
ClassConverter
DoubleArrayConverter
DoubleConverter
FileConverter
FloatArrayConverter
FloatConverter
IntegerArrayConverter
IntegerConverter
LongArrayConverter
LongConverter
ShortArrayConverter
ShortConverter
SqlDateConverter
SqlTimeConverter
SqlTimestampConverter
StringArrayConverter
StringConverter
URLConverter

昔からこんなにあったかな?


それを踏まえて、以下が材料です。

・JDK1.2
・commons-digester-1.4.1
・commons-beanutils-1.7.0
・commond-logging-1.0.4
・XMLパーサ(crimson-1.1.3)


普通にBeanを組み立てるだけなら簡単ですが、一応DIコンテナということで、参照の解決をしないといけません。
色々考えましたが、とりあえずすべてのBeanをシングルトンにしてしまえばうまく行きそうなので、それで作成することにしました。


設定ファイルは、Spring先生を参考にしました。全部属性で記述するのは、そのほうがDigesterに都合がいいからです。

<?xml version="1.0" encoding="UTF-8"?>
<beans>
 <bean id="bean1" class="test.container.TestBean1">
  <property name="p1" value="testp1"/>  
  <property name="p2">
   <list>
    <entry ref="bean2"/>
    <entry value="p2list"/>
   </list>
  </property>
  <property name="p3">
   <map>
    <entry key="foo" ref="bean2"/>
    <entry key="bar" value="p2map"/>
   </map>
  </property>
  <property name="p4" ref="bean2"/>  
 </bean>
 
 <bean id="bean2" class="test.container.TestBean2">
  <property name="p1" value="12"/>
  <property name="p4" ref="bean1"/>
 </bean>
</beans>

これを解決できれば、要件は満たせそうです。


ということで、Spring先生のインターフェース名をパクりつつ、以下のコードを実装しました。

package container;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.digester.Digester;
import org.apache.commons.digester.Rule;
import org.xml.sax.Attributes;

/**
 * @author c9katayama
 */
public class BeanFactory {

  private Map beanMap = new HashMap();

  public BeanFactory(InputStream is) {
    init(is);
  }

  private void init(final InputStream is) {

    // 2度読むので、一旦ロード
    final byte[] config = BeanFactoryHelper.readConfig(is);

    // 一旦全beanを登録
    Digester digester = new Digester();
    digester.setValidating(false);
    digester.addRule("beans/bean", new BeanRegisterRule());
    try {
      digester.parse(new ByteArrayInputStream(config));
    } catch (Exception e) {
      throw new NestedRuntimeException(e);
    }

    // プロパティ解決
    digester = new Digester();
    digester.setValidating(false);
    digester.addRule("beans/bean", new BeanStackRule());
    digester.addRule("beans/bean/property", new PropertyRule());
    digester.addRule("beans/bean/property/list", new CollectionRule());
    digester.addRule("beans/bean/property/list/entry", new EntryRule());
    digester.addRule("beans/bean/property/map", new CollectionRule());
    digester.addRule("beans/bean/property/map/entry", new EntryRule());
    try {
      digester.parse(new ByteArrayInputStream(config));
    } catch (Exception e) {
      throw new NestedRuntimeException(e);
    }
  }

  public Object getBean(String name) {
    Object bean = beanMap.get(name);
    if (bean == null) {
      throw new RuntimeException("Bean not found. name=" + name);
    }
    return bean;
  }
}

はじめに設定ファイルを読み込んで、1回パースします。初回パース時は、beanタグだけを読み、class属性からbeanをインスタンス化して、id属性もしくはname属性をキーにしてbeanMapにputします。
2回目のパース時には一通りのbeanがbeanMapに入っていることになるので、propertyタグを読み、value属性ならBeanUtilsで型変換セット、ref属性ならbeanMapからbeanを取得してセット、という流れで、一通りのセットアップをします。
あとはgetBean()でbeanを取得します。

    BeanFactory factory = new BeanFactory(BeanFactoryTest.class.getResourceAsStream("beans.xml"));
    TestBean1 bean1 = (TestBean1) factory.getBean("bean1");
    assertEquals("testp1",bea1.getP1());

機能的にはコンテナというにはおこがましいですが、エラー処理をもうちゃんとしてやれば、今回の要件はこれで満たせそうです。


ソースはこちらからどうぞ。
http://hatena.souko105.net/20080409/container.zip