Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Springの気が利かない部分を少し改良してみる(2)

阪本です。

前回に続き、Springでのオブジェクト初期化時の処理をもっと柔軟にしてみます。

今回は、以下のように記述できるようにする方法を説明します。

<bean id="table" class="sample.Table">
  <init-method>self.addColumn("userId", "ユーザID")</init-method>
  <init-method>self.addColumn("userName", "ユーザ名")</init-method>
</bean>

追加・修正するファイルは以下の通り。

では、さっそく行ってみましょう。

XSDファイルの修正

init-methodタグを書けるようにするために、sample.xsdファイルを以下のように修正します。

sample.xsd

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<xsd:schema xmlns="http://www.foo.com/schema/sample"
    xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://www.foo.com/schema/sample">

  <xsd:element name="column">
    <xsd:complexType>
      <xsd:attribute name="id" type="xsd:string" use="required" />
      <xsd:attribute name="name" type="xsd:string" use="required" />
    </xsd:complexType>
  </xsd:element>

  <xsd:element name="init-method">
    <xsd:complexType mixed="true" />
  </xsd:element>

</xsd:schema>

を指定することにより、init-methodタグで囲まれた中に文字列を記述できるようになります。

ハンドラの修正とデコレータの作成

次に、XMLを解析してオブジェクトを生成するハンドラを修正します。
ここでは、init-methodタグを解析するパーサを登録します。

SampleBeanXmlHandler.java

public class SampleBeanXmlHandler extends NamespaceHandlerSupport {
    public void init() {
        // columnタグを解析するオブジェクトを登録する
        registerBeanDefinitionParser("column", new ColumnBeanDefinitionParser());

        // init-methodタグを解析するオブジェクトを登録する
        registerBeanDefinitionDecorator("init-method", new InitMethodBeanDefinitionDecorator());
    }
}

また、init-methodタグを解析するデコレータを作成します。

InitMethodBeanDefinitionDecorator.java

public class InitMethodBeanDefinitionDecorator implements BeanDefinitionDecorator {
    @Override
    public BeanDefinitionHolder decorate(Node node,
            BeanDefinitionHolder definitionHolder,
            ParserContext parserContext)
    {
        // init-methodタグで囲まれた文字列を取得する
        String initMethodValue = null;
        if (node instanceof Element)
        {
            Element element = (Element) node;
            Object value = parserContext.getDelegate().parseValueElement(element, null);
            initMethodValue = ((TypedStringValue) value).getValue();
        }

        // 各設定をコピーする
        BeanDefinition targetDefinition = definitionHolder.getBeanDefinition();
        RootBeanDefinition rootDefinition = new RootBeanDefinition();
        rootDefinition.setScope(targetDefinition.getScope());
        rootDefinition.setLazyInit(targetDefinition.isLazyInit());
        rootDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate());
        rootDefinition.setPrimary(targetDefinition.isPrimary());

        // init-methodの処理対象のBean定義を設定する
        rootDefinition.setDecoratedDefinition(definitionHolder);

        // 処理対象のBean定義に対してinit-methodの処理を行うクラスを登録する
        rootDefinition.setBeanClass(InitMethodFactoryBean.class);

        // InitMethodFactoryBeanオブジェクトに渡す値をセットする
        rootDefinition.getPropertyValues().add("beanClass", targetDefinition.getBeanClassName());
        rootDefinition.getPropertyValues().add("initMethod", initMethodValue);

        BeanDefinitionHolder holder = new BeanDefinitionHolder(rootDefinition, definitionHolder.getBeanName());
        return holder;
    }
}

前回のようにパーサ(ColumnBeanDefinitionParser)ではなく、デコレータ(InitMethodBeanDefinitionDecorator)を作成したのは、定義されたインスタンス(今回の例ではColumnオブジェクト)に対して変更を加えるからです。

init-methodの処理クラスの実装

処理対象のBeanに対して、init-methodで指定した処理を行うInitMethodFactoryBeanクラスは以下になります。

InitMethodFactoryBean

public class InitMethodFactoryBean implements FactoryBean<Object>
{
    private Class<?> beanClass_;

    private String initMethod_;

    public void setBeanClass(String beanClassName) throws Exception
    {
        this.beanClass_ = Class.forName(beanClassName);
    }

    public void setInitMethod(String initMethod)
    {
        this.initMethod_ = initMethod;
    }

    @Override
    public Object getObject() throws Exception
    {
        // init-methodで指定された処理を行ったオブジェクトを返す
        Object object = this.beanClass_.newInstance();
        Object ognl = Ognl.parseExpression(this.initMethod_);

        object = Ognl.getValue(ognl, Collections.singletonMap("self", object));
        return object;
    }

    @Override
    public Class<?> getObjectType()
    {
        return this.beanClass_;
    }

    @Override
    public boolean isSingleton()
    {
        return false;
    }
}

OGNL式でinit-methodの値を評価しています。
getObject() の中でオブジェクトを初期化しているので、OGNL式の評価に必要なキーワード(上記であればself)を追加することもできます。
これで、

<bean id="table" class="sample.Table">
  <sample:init-method>self.addColumn("userId", "ユーザID")</sample:init-method>
  <sample:init-method>self.addColumn("userName", "ユーザ名")</sample:init-method>
</bean>

と書くことにより、Tableクラスのインスタンス生成時に、TableクラスのaddColumnメソッドが2回呼ばれるようになります。

一見、「やりたかったことができた!」と思えそうなのですが、この方法には欠点があります。それは・・・

インスタンスにプロパティをセットする(beanタグを使用する例)前に、init-methodが実行される

こと!

プロパティの値がセットされてからinit-methodを実行するべきですので、これについてはまた次回に。。

では。