弊社のプロダクトであるR42フレームワークでは、Spring2.5で追加された機能である
これにより、開発者がApplicationContext.xmlを一切書く必要がない開発を実現しています。
現在、Actionクラスのスコープはprototypeとしているのですが、試しにrequestにしてみようとしたら多少手間取ることになり、結局時間切れになったので諦めました。現状、とくにprototypeスコープで困っているわけでもないですし。(←いいわけ)
まあとりあえず、以下に調査結果の備忘録として残しておきます。
修正前
ApplicationContext.xml(抜粋)
<context:component-scan base-package="foo.bar.web.action" scope-resolver="foo.bar.util.PrototypeScopeMetadataResolver" use-default-filters="false"> <context:include-filter type="assignable" expression="com.opensymphony.xwork2.Action" /> </context:component-scan>
PrototypeScopeMetadataResolver.java
public class PrototypeScopeMetadataResolver implements ScopeMetadataResolver { public ScopeMetadata resolveScopeMetadata(final BeanDefinition definition) { final ScopeMetadata scopeMetadata = new ScopeMetadata(); scopeMetadata.setScopeName("prototype"); return scopeMetadata; } }
修正後
ApplicationContext.xml(抜粋)
<context:component-scan base-package="foo.bar.web.action" scope-resolver="foo.bar.util.RequestScopeMetadataResolver" use-default-filters="false"> <context:include-filter type="assignable" expression="com.opensymphony.xwork2.Action" /> </context:component-scan>
RequestScopeMetadataResolver.java
public class RequestScopeMetadataResolver implements ScopeMetadataResolver { public ScopeMetadata resolveScopeMetadata(final BeanDefinition definition) { final ScopeMetadata scopeMetadata = new ScopeMetadata(); scopeMetadata.setScopeName("request"); return scopeMetadata; } }
で、起動してみるとエラー。
Tomcatの起動ログ(抜粋)
致命的: フィルタ struts2 の起動中の例外です org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'foo.bar.web.action.xxx.AbcAction': Scope 'request' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:312) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:185) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:164) at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:880) at com.opensymphony.xwork2.spring.SpringObjectFactory.getClassInstance(SpringObjectFactory.java:204) at org.apache.struts2.convention.PackageBasedActionConfigBuilder.buildConfiguration(PackageBasedActionConfigBuilder.java:429) at org.apache.struts2.convention.PackageBasedActionConfigBuilder.buildActionConfigs(PackageBasedActionConfigBuilder.java:278) at org.apache.struts2.convention.ClasspathPackageProvider.loadPackages(ClasspathPackageProvider.java:52) at com.opensymphony.xwork2.config.impl.DefaultConfiguration.reloadContainer(DefaultConfiguration.java:200) at com.opensymphony.xwork2.config.ConfigurationManager.getConfiguration(ConfigurationManager.java:55) at org.apache.struts2.dispatcher.Dispatcher.init_PreloadConfiguration(Dispatcher.java:360) at org.apache.struts2.dispatcher.Dispatcher.init(Dispatcher.java:403) at org.apache.struts2.dispatcher.FilterDispatcher.init(FilterDispatcher.java:190) at org.apache.catalina.core.ApplicationFilterConfig.getFilter(ApplicationFilterConfig.java:275) at org.apache.catalina.core.ApplicationFilterConfig.setFilterDef(ApplicationFilterConfig.java:397) at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:108) at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:3800) at org.apache.catalina.core.StandardContext.start(StandardContext.java:4450) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1045) at org.apache.catalina.core.StandardHost.start(StandardHost.java:722) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1045) at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:443) at org.apache.catalina.core.StandardService.start(StandardService.java:516) at org.apache.catalina.core.StandardServer.start(StandardServer.java:710) at org.apache.catalina.startup.Catalina.start(Catalina.java:583) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:288) at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:413) Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:122) at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:40) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:298) ... 30 more
どうやら「スレッドにHTTPリクエストがバインドされてねーのに、requestスコープも何もないだろ?ああ?」
と言っているようです。起動前にそんなこと言われても… ρ(-ε- )
調べると、scoped-proxy="targetClass"
を付けるとよさそうです。
scoped-proxyって何やねんと思い調べてみると、依存関係がBeanリポジトリの初期化時に確定しないオブジェクト
(例えばHTTPリクエストをActionにInjectionするとか)を、無理やり初期化時に確定しているオブジェクトへ見せかけるための力技のようです。詳しくはここの3.4.4.5を参照。
<context:component-scan base-package="foo.bar.web.action" scope-resolver="foo.bar.util.RequestScopeMetadataResolver" scoped-proxy="targetClass" ← 追加 use-default-filters="false"> <context:include-filter type="assignable" expression="com.opensymphony.xwork2.Action" /> </context:component-scan>
で、Tomcatを再起動すると次はこんなエラーが。
scope-resolverとscoped-proxyを一緒には指定できないとのこと。
Caused by: org.springframework.beans.factory.parsing.BeanDefinitionParsingException: Configuration problem: Cannot define both 'scope-resolver' and 'scoped-proxy' on <component-scan> tag Offending resource: class path resource [applicationContext.xml] at org.springframework.beans.factory.parsing.FailFastProblemReporter.error(FailFastProblemReporter.java:68) at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85) at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:76) at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.configureScanner(ComponentScanBeanDefinitionParser.java:119) at org.springframework.context.annotation.ComponentScanBeanDefinitionParser.parse(ComponentScanBeanDefinitionParser.java:83) at org.springframework.beans.factory.xml.NamespaceHandlerSupport.parse(NamespaceHandlerSupport.java:69) at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1297) at org.springframework.beans.factory.xml.BeanDefinitionParserDelegate.parseCustomElement(BeanDefinitionParserDelegate.java:1287) at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.parseBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:135) at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.registerBeanDefinitions(DefaultBeanDefinitionDocumentReader.java:92) at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.registerBeanDefinitions(XmlBeanDefinitionReader.java:507) at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.doLoadBeanDefinitions(XmlBeanDefinitionReader.java:398) at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:342) at org.springframework.beans.factory.xml.XmlBeanDefinitionReader.loadBeanDefinitions(XmlBeanDefinitionReader.java:310) at org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader.importBeanDefinitionResource(DefaultBeanDefinitionDocumentReader.java:190) ... 33 more
それではこれはどうだ。(さっきのscoped-proxyは消しておく)
public class RequestScopeMetadataResolver implements ScopeMetadataResolver { public ScopeMetadata resolveScopeMetadata(final BeanDefinition definition) { final ScopeMetadata scopeMetadata = new ScopeMetadata(); scopeMetadata.setScopedProxyMode(ScopedProxyMode.TARGET_CLASS); scopeMetadata.setScopeName("request"); return scopeMetadata; } }
これでやっと起動が完了するようになったのですが、次は必要なリソースがInjectionされていない模様。
( ´∀`)< ぬるぽ
になってしまいます。
public class AbcAction implements Action { @Resource private AbcService service; //←これがInjectionされずnullのまま public String execute() { ... //どっかこの辺でぬるぽ } }
scoped-proxyをONにすることで、requestスコープであるActionのサブクラスが自動的に作成される(by CGLIB)ようです。
ということは、privateフィールドに直接Injectionしているのが原因なのでは思い、
public class AbcAction implements Action { private AbcService service; @Resource public void setAbcService(AbcService service) { this.service = service; } public String execute() { ... //やっぱりぬるぽ } }
としましたがこれもダメ。
うーむ。もう少しでゴールだとは思うのですが、今日はこれ以上時間をかけられないので、ここで調査は終了。
Springは確かに強力ではあるものの、その力を誤用すると、とんでもないことになりかねません。
開発者がSpringを意識せずに済むようにフレームワークを構成するのは大事だな、と再確認しつつビールを飲むのでした。まる。