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

この日記は私的なものであり、所属会社の見解ではありません。 GitHub: takahashikzn

requestスコープとcomponent-scanの併用

Java Spring

弊社のプロダクトであるR42フレームワークでは、Spring2.5で追加された機能であるを使用することで、特定のクラスを自動的にコンポーネントとしてBeanリポジトリに取り込んでいます。

これにより、開発者が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を意識せずに済むようにフレームワークを構成するのは大事だな、と再確認しつつビールを飲むのでした。まる。