読者です 読者をやめる 読者になる 読者になる

Taste of Tech Topics

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

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

Spring/SpringMVC


阪本です。

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

今回は、以下のように記述したときに、プロパティの値がセットされた後にメソッドが実行されるようにする方法を説明します。
(これで、ようやくSeasar2と同じ動きになる!)

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

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

では、具体的に見ていきましょう。

デコレータの修正と後処理クラスの作成

前回では、デコレータ(InitMethodBeanDefinitionDecorator)のメソッドが呼ばれたときにFactoryクラスにパラメータを渡してオブジェクトを生成していましたが、今回はこのメソッドではBeanオブジェクトを変更せず、一旦後処理クラス(InitMethodBeanPostProcessor)にOGNL式を渡します。

InitMethodBeanDefinitionDecorator.java

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

        // ここではBeanを変更しない
        return definitionHolder;
    }
}

で、後処理クラス(InitMethodBeanPostProcessor)の中身は以下になります。BeanPostProcessorをimplementsするのがポイント。

InitMethodBeanPostProcessor

public class InitMethodBeanPostProcessor implements BeanPostProcessor {
    private static Map<String, List<String>> initMethodsMap__ = new ConcurrentHashMap<String, List<String>>();

    public static synchronized void addInitMethod(String beanName,
            String initMethodExpr) {
        // bean名をキーにして、初期化メソッドを登録する
        List<String> initMethods = initMethodsMap__.get(beanName);
        if (initMethods == null) {
            initMethods = new ArrayList<String>();
            initMethodsMap__.put(beanName, initMethods);
        }
        initMethods.add(initMethodExpr);
    }

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName)
            throws BeansException {
        // 何も変更しない
        return bean;
    }

    @Override
    public synchronized Object postProcessAfterInitialization(Object bean,
            String beanName) throws BeansException {
        if (initMethodsMap__.containsKey(beanName)) {
            // 初期化メソッドが登録されていれば、順に実行する
            List<String> initMethodExprs = initMethodsMap__.get(beanName);
            initMethodsMap__.remove(beanName);
            try {
                for (String initMethodExpr : initMethodExprs) {
                    Object ognl = Ognl.parseExpression(initMethodExpr);
                    bean = Ognl.getValue(ognl,
                            Collections.singletonMap("self", bean));
                }
            } catch (OgnlException ex) {
                throw new BeanInitializationException("OGNL expression error.", ex);
            }
        }
        return bean;
    }
}

各Beanの初期化時に、このクラスのpostProcessAfterInitializationメソッドが呼ばれます。
init-methodを指定したBeanは、initMethodsMap__に値を持っているため、それを元に初期化メソッドを実行します。

あとは、context.xmlに以下を追加して、Bean初期化後に後処理クラスのメソッドが呼ばれるようにします。

<bean class="jp.co.acroquest.webpetition.common.xml.InitMethodBeanPostProcessor" />

これで、Bean定義に

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

と書くことにより、Tableクラスのインスタンス生成時に、先にプロパティの値が設定された後、初期化メソッド(init-method)が実行されるようになります。

今度こそ、Seasar2と同じように、Bean初期化時に引数ありのメソッドを複数個呼べるようになりました!
応用としては、OGNLで指定する式の中に、SpringのBean名を記述できるようにすれば、さらに便利ですね。

では。