Подтвердить что ты не робот

Spring MVC: Как использовать bean с запросом в пределах порожденной нити?

В приложении Spring MVC у меня есть bean с запросом. Я вставляю этот bean где-нибудь. Там поток обслуживания HTTP-запросов может порождать новый поток.

Но всякий раз, когда я пытаюсь получить доступ к областью bean с учетом запроса из недавно созданного потока, я получаю org.springframework.beans.factory.BeanCreationException (см. ниже трассировку стека).
Доступ к области запросов bean, зависящей от запроса, из потока запросов HTTP отлично работает.

Как сделать доступным для запросов bean для потоков, созданных потоком запросов HTTP?


Простая настройка

Получите следующие фрагменты кода. Затем запустите сервер, например, http://example.com:8080.
При обращении к http://example.com:8080/scopetestnormal каждый раз, когда запрос отправляется по этому адресу, counter увеличивается на 1 (заметный через выход журнала).:) Супер!

При обращении к http://example.com:8080/scopetestthread каждый раз, когда запрос отправляется по этому адресу, вызывается указанные исключения.: (Независимо от того, что выбрано ScopedProxyMode, это происходит как для CGLIB-based, так и для JDK-динамический-прокси-интерфейс на основе запросов beans

Файл конфигурации

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-динамического-прокси-интерфейса на основе запроса 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
4b9b3361

Ответ 1

ОК, прочитав код SimpleThreadScope, который поставляется с Spring Я думаю, вы можете создать SimpleInheritableThreadScope, используя InheritableThreadLocal.

Затем просто используйте бит 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>

Это означает, что при создании bean с областью thread-inherited у вас будет доступ к этому bean с копией на поток, и эта копия будет доступна в потоках, порожденных вашим потоком, т.е. bean, который может использоваться в потоках, созданных в потоке запроса.

Ответ 2

Конфигурация ниже будет распространять контекст запроса на ваши потоки, запущенные из 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>

Отказ от ответственности: я не тестировал это специально с помощью beans с запросом, поскольку я его не использую. Я проверил, что RequestContextHolder возвращает допустимый контекст в дочерних потоках.

Отказ 2: есть причина, по которой этот параметр по умолчанию равен false. Возможны побочные эффекты, особенно если вы повторно используете свои потоки (как в пулах потоков).

Ответ 3

Вдохновленный ответом @mael, вот мое решение "custom-scope-out-of-the-box". Я использую полностью зависящую от аннотации конфигурацию Spring.

В моем конкретном случае Spring own org.springframework.context.support.SimpleThreadScope уже предоставляет поведение, которое ищет этот вопрос (правда, это странно, потому что SimpleThreadScope не использует InheritableThreadLocal, но эффективно 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 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");
        } 
    }
}

Теперь для любого bean, у которого должна быть область запроса, и которая должна использоваться из любого потока, порожденного потоком HTTP-запроса, соответствующим образом установите вновь определенную область:

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

fooobar.com/questions/136879/...

Для этого вопроса проверьте мой ответ выше, указанный url

Использование области запроса bean вне фактического веб-запроса. Если вы используете веб-контейнер Servlet 2.5, с запросами, обработанными за пределами Spring s DispatcherServlet (например, при использовании JSF или Struts), вам необходимо зарегистрировать org.springframework.web.context.request.RequestContextListener ServletRequestListener. Для Servlet 3.0+ это можно сделать программно через интерфейс WebApplicationInitializer. В качестве альтернативы или для старых контейнеров добавьте следующее выражение в свой веб-приложение: web.xml файл:

Ответ 5

Если вы посмотрите на AbstractRequestAttributesScope, вы увидите, что он использует текущий RequestAttributes, чтобы получить желаемый bean.

В вашем потоке вы, вероятно, захотите сделать что-то вроде этого:

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();