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

この日記は私的なものであり所属会社の見解とは無関係です。 GitHub: takahashikzn

struts-2.5.5でI18nInterceptorの初期化失敗を無理やり回避する

struts-2.5.5がリリースされました。

このリリースはかなり大きなインパクトのある変更がいくつか入っています。


最大のインパクトは、ActionContext#getParameters()の戻り値がjava.util.Mapからorg.apache.struts2.dispatcher.HttpParametersに変更されたこと。 これにより、これまで動作していたコードがコンパイルエラーになります。いわゆるbreaking changeというやつです。

詳しくはこちらの議論を参照。


いやお前、せめて前のAPIをDeprecatedで残すとかしろよ…コレはちょっとやり過ぎ。しかもHttpParametersクラス、APIが使いづらいし。

例えば値を追加する際、Mapのままだったら

ActionContext.getParameters().put("foo", new String[]{ "値" });

で済むところが、

Map<String, Parameter> params = new HashMap<>();
params.put("foo", new Parameter.Request("foo", "値"));

ActionContext.getParameters().appendAll(params);

としなければならなくなりました。単一の値をputする手段がないし、いちいちParameterに変換してやる必要があります。メンドクサイ。

起動すると謎のエラー発生

さて、上記API変更に伴い既存コードを修正して、いざ起動してみると今度は次のような例外が発生。

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.apache.struts2.interceptor.I18nInterceptor': Unsatisfied dependency expressed through bean property 'localeProvider'

localeProviderというBeanが未定義なのかと思ってstruts2-core-2.5.5.jar内のstruts-default.xmlをみてみると、きちんと

<bean type="com.opensymphony.xwork2.LocaleProvider" name="struts" class="com.opensymphony.xwork2.DefaultLocaleProvider" scope="singleton" />

は定義されている。なのに何故依存性解決に失敗するのだ。。。

ログをよく見てみると、次のようにも出力されています。

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type [com.opensymphony.xwork2.LocaleProvider] is defined: expected single matching bean but found 543: jp.root42.r42fw_web.export.web.action.error.ErrorAction,...

これで答えがわかりました。犯人はstruts2-spring-pluginでした。いや、struts2-coreかな?どちらが悪いとは決めづらい微妙な感じです。

Struts2のActionクラスの基底クラスであるcom.opensymphony.xwork2.ActionSupportの定義は次のようになっており、

public class ActionSupport implements Action, Validateable, ValidationAware, TextProvider, LocaleProvider, Serializable {
    ...
}

LocaleProviderインタフェースを実装しています。そして、ActionクラスはSpringのコンテキストが初期化されたときにロードされてしまいます。 従ってI18nInterceptorの依存性解決のときに、struts-default.xmlで定義されているDefaultLocaleProviderと競合してLocaleProviderのBeanを一意に特定できないため失敗するわけです。

ワークアラウンド

解決方法ですが、僕は次のようにしました。

import java.util.Map;

import javax.servlet.ServletContext;

import com.opensymphony.xwork2.DefaultLocaleProvider;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.interceptor.I18nInterceptor;
import org.apache.struts2.spring.StrutsSpringObjectFactory;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;


public class WorkaroundStrutsSpringObjectFactory
    extends StrutsSpringObjectFactory {

    // 親クラスからコピペ
    @Inject
    public WorkaroundStrutsSpringObjectFactory(
        @Inject(value = StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE, required = false) final String autoWire,
        @Inject(value = StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE_ALWAYS_RESPECT, required = false) final String alwaysAutoWire,
        @Inject(value = StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_USE_CLASS_CACHE, required = false) final String useClassCacheStr,
        @Inject(value = StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_ENABLE_AOP_SUPPORT, required = false) final String enableAopSupport,
        @Inject final ServletContext servletContext, @Inject(StrutsConstants.STRUTS_DEVMODE) final String devMode,
        @Inject final Container container) {

        super(autoWire, alwaysAutoWire, useClassCacheStr, enableAopSupport, servletContext, devMode, container);
    }

    @Override
    public Object buildBean(final Class clazz, final Map<String, Object> extraContext) throws Exception {

        try {
            return super.buildBean(clazz, extraContext);
        } catch (final Exception e) {

            // I18nInterceptorの依存性解決失敗の場合、手動でオブジェクトを作って返す
            if ((clazz == I18nInterceptor.class)
                && (0 <= ExceptionUtils.indexOfType(e, NoUniqueBeanDefinitionException.class))) {

                final I18nInterceptor ret = new I18nInterceptor();
                ret.setLocaleProvider(new DefaultLocaleProvider());
                return ret;
            } else {
                throw e;
            }
        }
    }
}

struts.xmlにこのように記述してデフォルトのBeanを置き換えます。

<constant name="struts.objectFactory" value="[YOUR PACKAGE HERE].WorkaroundStrutsSpringObjectFactory" />

全くエレガントでない手法ですが、そのうち対策されるまでのツナギなので一先ずはこれで。

ついでに、親クラス側でエラーログが出てウザいので

<logger name="com.opensymphony.xwork2.spring.SpringObjectFactory" level="off" />

でログをOFFにしておきました。