2017-07-13追記
struts-2.5.12で解消された模様です。
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にしておきました。