web-dev-qa-db-ja.com

Spring MVC:生成されたスレッド内で要求スコープのBeanを使用する方法

Spring MVCアプリケーションには、要求スコープのBeanがあります。このBeanをどこかに注入します。そこで、HTTPリクエストサービングスレッドが新しいスレッドを生成する可能性があります。

しかし、新しく生成されたスレッドからリクエストスコープのBeanにアクセスしようとすると、org.springframework.beans.factory.BeanCreationException(以下のスタックトレースを参照)。
HTTP要求スレッドから要求スコープBeanへのアクセスは正常に機能します。

HTTPリクエストスレッドによって生成されたスレッドがリクエストスコープのBeanを利用できるようにするにはどうすればよいですか?


簡単なセットアップ

次のコードスニペットを実行します。次に、たとえば http://example.com:808 でサーバーを起動します。
http://example.com:8080/scopetestnormal にアクセスすると、このアドレスにリクエストが送信されるたびに、counterが1ずつ増加します(ロガー出力で認識可能)。 :) 素晴らしい!

http://example.com:8080/scopetestthread にアクセスすると、このアドレスに対してリクエストが行われるたびに、前述の例外がスローされます。 :(。ScopedProxyModeの選択に関係なく、これはCGLIBベースJDK-dynamic-proxy-interface-based要求スコープBeanの両方で発生

構成ファイル

package com.example.config

@Configuration
@ComponentScan(basePackages = { "com.example.scopetest" })
public class ScopeConfig {

    private Integer counter = new Integer(0);

    @Bean
    @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public Number counter() {
        counter = new Integer(counter.intValue() + 1);
        return counter;
    }


    /* Adding a org.springframework.social.facebook.api.Facebook request-scoped bean as a real-world example why all this matters
    @Bean
    @Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
    public Facebook facebook() {
    Connection<Facebook> facebook = connectionRepository()
            .findPrimaryConnection(Facebook.class);
    return facebook != null ? facebook.getApi() : new FacebookTemplate();
    }
    */

    ...................

}

コントローラーファイル

package com.example.scopetest;

import javax.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.social.facebook.api.Facebook;
import org.springframework.social.facebook.api.FacebookProfile;
import org.springframework.stereotype.Controller;

import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class ScopeTestController {

    //@Inject
    //private Facebook facebook;

    @Inject
    private Number counter;

    private static final Logger logger = LoggerFactory
            .getLogger(ScopeTestController.class);

    @RequestMapping(value = "/scopetestnormal") 
    public void scopetestnormal() {
        logger.debug("About to interact with a request-scoped bean from HTTP request thread");
        logger.debug("counter is: {}", counter);

        /* 
         * The following also works
         * FacebookProfile profile = facebook.userOperations().getUserProfile();
         * logger.debug("Facebook user ID is: {}", profile.getId());    
         */
    }



    @RequestMapping(value = "/scopetestthread")
    public void scopetestthread() {
        logger.debug("About to spawn a new thread");
        new Thread(new RequestScopedBeanAccessingThread()).start();
        logger.debug("Spawned a new thread");
    }


    private class RequestScopedBeanAccessingThread implements Runnable {

        @Override
        public void run() {
            logger.debug("About to interact with a request-scoped bean from another thread. Doomed to fail.");          
            logger.debug("counter is: {}", counter);

            /*
             * The following is also doomed to fail
             * FacebookProfile profile = facebook.userOperations().getUserProfile();
             * logger.debug("Facebook user ID is: {}", profile.getId());        
             */
        }

    }

}

CGLIBベースの要求スコープBean(proxyMode = ScopedProxyMode.TARGET_CLASS

SLF4J: Failed toString() invocation on an object of type [$Java.lang.Number$$EnhancerByCGLIB$$45ffcde7]
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.counter': 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:342)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.Java:193)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.Java:33)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.getTarget(Cglib2AopProxy.Java:654)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.Java:605)
    at $Java.lang.Number$$EnhancerByCGLIB$$45ffcde7.toString(<generated>)
    at org.slf4j.helpers.MessageFormatter.safeObjectAppend(MessageFormatter.Java:304)
    at org.slf4j.helpers.MessageFormatter.deeplyAppendParameter(MessageFormatter.Java:276)
    at org.slf4j.helpers.MessageFormatter.arrayFormat(MessageFormatter.Java:230)
    at ch.qos.logback.classic.spi.LoggingEvent.<init>(LoggingEvent.Java:114)
    at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.Java:447)18:09:48.276 container [Thread-16] DEBUG c.g.s.c.c.god.ScopeTestController - counter is: [FAILED toString()]

    at ch.qos.logback.classic.Logger.filterAndLog_1(Logger.Java:421)
    at ch.qos.logback.classic.Logger.debug(Logger.Java:514)
    at com.example.scopetest.ScopeTestController$RequestScopedBeanAccessingThread.run(ScopeTestController.Java:58)
    at Java.lang.Thread.run(Thread.Java:722)
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:131)
    at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.Java:40)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.Java:328)
    ... 14 more

JDK-dynamic-proxy-interface-based request-scoped bean(proxyMode = ScopedProxyMode.INTERFACES

Exception in thread "Thread-16" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.facebook': 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:342)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.Java:193)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.Java:33)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.Java:182)
    at $Proxy28.userOperations(Unknown Source)
    at com.example.scopetest.ScopeTestController$PrintingThread.run(ScopeTestController.Java:61)
    at Java.lang.Thread.run(Thread.Java:722)
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:131)
    at org.springframework.web.context.request.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.Java:40)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.Java:328)
    ... 6 more
26
Abdull

OK、Springに付属の SimpleThreadScope のコードを読むことで、代わりに InheritableThreadLocal を使用してSimpleInheritableThreadScopeを作成できると思います。

次に、XMLを少し使用して、カスタムスコープを登録します。

<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
      <property name="scopes">
          <map>
              <entry key="thread-inherited">
                  <bean class="org.mael.spring.context.support.SimpleInheritableThreadScope"/>
              </entry>
          </map>
      </property>
  </bean>

つまり、thread-inheritedスコープでBeanを作成すると、スレッドごとのコピーでこのBeanにアクセスでき、そのコピーは、スレッドによって生成されたスレッドで使用可能になります。要求スレッドで生成されたスレッド内。

9
ElderMael

以下の設定は、HTTPリクエスト内から起動されたスレッドにリクエストコンテキストを伝播します。

<servlet>
    <servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>threadContextInheritable</param-name>
      <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

免責事項:要求スコープのBeanを使用してこれをテストしたことはありません。 RequestContextHolderが子スレッドで有効なコンテキストを返すことをテストしました。

免責事項2:この設定のデフォルトがfalseである理由があります。特にスレッドを再利用する場合(スレッドプールなど)、副作用が生じる可能性があります。

8
rootkit

AbstractRequestAttributesScopeを見ると、目的のBeanを取得するために現在のRequestAttributesを使用していることがわかります。

あなたのスレッドでは、おそらく次のようなことをしたいと思うでしょう:

final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
final SecurityContext securityContext = SecurityContextHolder.getContext();

new Thread(
    () -> {

      boolean hasContext = RequestContextHolder.getRequestAttributes() == requestAttributes
          && SecurityContextHolder.getContext() == securityContext;

      if (!hasContext) {
        RequestContextHolder.setRequestAttributes(requestAttributes);
        SecurityContextHolder.setContext(securityContext);
      }

      try {

        // useful stuff goes here

      } finally {
        if (!hasContext) {
          RequestContextHolder.resetRequestAttributes();
          SecurityContextHolder.clearContext();
        }
      }
    }
).start();  

@maelの答えに触発されて、ここに私の「カスタムスコープですぐに使える」ソリューションがあります。私は完全に注釈駆動型のSpring構成を使用しています。

私の特定のケースでは、Spring自身のorg.springframework.context.support.SimpleThreadScopeは、質問が探している動作を既に提供しています(それは奇妙です。SimpleThreadScopeInheritableThreadLocalを使用せず、事実上ThreadLocalを使用します。 、私はすでに幸せです)。

同時ユーザーインタラクションでの正しい動作はまだテストされていません。

手順

SimpleThreadScopeタイプを登録します。

package com.example.config

public class MainConfig implements BeanFactoryAware {

    private static final Logger logger = LoggerFactory.getLogger(MainConfig.class);

    .......

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        if (beanFactory instanceof ConfigurableBeanFactory) {

            logger.info("MainConfig is backed by a ConfigurableBeanFactory");
            ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory;

            /*Notice:
             *org.springframework.beans.factory.config.Scope
             * !=
             *org.springframework.context.annotation.Scope
             */
            org.springframework.beans.factory.config.Scope simpleThreadScope = new SimpleThreadScope();
            cbf.registerScope("simpleThreadScope", simpleThreadScope);

            /*why the following? Because "Spring Social" gets the HTTP request's username from
             *SecurityContextHolder.getContext().getAuthentication() ... and this 
             *by default only has a ThreadLocal strategy...
             *also see http://stackoverflow.com/a/3468965/923560 
             */
            SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

        }
        else {
            logger.info("MainConfig is not backed by a ConfigurableBeanFactory");
        } 
    }
}

ここで、要求スコープを持ち、HTTP要求スレッドによって生成されたスレッドから使​​用できるBeanについて、それに応じて新しく定義されたスコープを設定します。

package com.example.config

@Configuration
@ComponentScan(basePackages = { "com.example.scopetest" })
public class ScopeConfig {

    private Integer counter = new Integer(0);

    @Bean
    @Scope(value = "simpleThreadScope", proxyMode = ScopedProxyMode.TARGET_CLASS)
    public Number counter() {
        counter = new Integer(counter.intValue() + 1);
        return counter;
    }


    @Bean
    @Scope(value = "simpleThreadScope", proxyMode = ScopedProxyMode.INTERFACES)
    public ConnectionRepository connectionRepository() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null) {
            throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
        }
        return usersConnectionRepository().createConnectionRepository(authentication.getName());
    }


    @Bean
    @Scope(value = "simpleThreadScope", proxyMode = ScopedProxyMode.INTERFACES)
    public Facebook facebook() {
    Connection<Facebook> facebook = connectionRepository().findPrimaryConnection(Facebook.class);
    return facebook != null ? facebook.getApi() : new FacebookTemplate();
    }


    ...................

}
4
Abdull

https://stackoverflow.com/a/30640097/2569475

この問題については、上記のURLで回答を確認してください

実際のWeb要求以外で要求スコープBeanを使用する。 Servlet 2.5 Webコンテナを使用し、SpringのDispatcherServletの外部でリクエストを処理する場合(たとえば、JSFまたはStrutsを使用する場合)、org.springframework.web.context.request.RequestContextListener ServletRequestListenerを登録する必要があります。 Servlet 3.0+の場合、これはWebApplicationInitializerインターフェースを介してプログラムで実行できます。または、古いコンテナの場合、次の宣言をWebアプリケーションのweb.xmlファイルに追加します。

4
Deepak