web-dev-qa-db-fra.com

Spring MVC: Comment utiliser un bean à portée de requête à l'intérieur d'un thread généré?

Dans une application Spring MVC, j'ai un bean à portée de requête. J'injecte ce haricot quelque part. Là, le thread de service de requête HTTP pourrait éventuellement générer un nouveau thread.

Mais chaque fois que j'essaie d'accéder au bean à portée de requête à partir du thread nouvellement généré, j'obtiens un org.springframework.beans.factory.BeanCreationException (voir trace de pile ci-dessous).
L'accès au bean à portée de requête à partir du thread de requête HTTP fonctionne correctement.

Comment puis-je mettre un bean à portée de requête à la disposition des threads générés par le thread de requête HTTP?


Configuration simple

Exécutez les extraits de code suivants. Démarrez ensuite un serveur, par exemple à http://example.com:808 .
Lors de l'accès à http://example.com:8080/scopetestnormal , chaque fois qu'une demande est faite à cette adresse, counter est incrémenté de 1 (visible via l'enregistreur production). :) Super!

Lors de l'accès http://example.com:8080/scopetestthread , chaque fois qu'une demande est adressée à cette adresse, les exceptions mentionnées sont levées. :(. Peu importe ce que vous avez choisi ScopedProxyMode, cela se produit pour les beans basés sur CGLIB et JDK-dynamic-proxy-interface-scope-scope

Fichier de configuration

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();
    }
    */

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

}

Fichier contrôleur

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());        
             */
        }

    }

}

Trace de pile pour le bean à portée de requête basé sur CGLIB (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

Trace de pile pour le bean JDK-dynamic-proxy-interface-based scope-scope (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, en lisant le code dans SimpleThreadScope fourni avec Spring, je pense que vous pouvez créer un SimpleInheritableThreadScope en utilisant un InheritableThreadLocal à la place.

Ensuite, utilisez simplement un peu de xml pour enregistrer votre portée personnalisée:

<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>

Cela signifie que lorsque vous créez un bean avec un thread-inherited scope, vous aurez accès à ce bean avec une copie par thread et cette copie sera disponible dans les threads générés par votre thread, c'est-à-dire un bean de portée de requête qui peut être utilisé dans les threads générés dans votre thread de requête.

9
ElderMael

La configuration ci-dessous propage le contexte de la requête vers vos threads lancés à partir de la requête 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>

Avis de non-responsabilité: je n'ai pas testé cela spécifiquement avec des beans à portée de demande, car je n'en utilise aucun. J'ai testé que RequestContextHolder renvoie un contexte valide dans les threads enfants.

Avertissement 2: il y a une raison pour laquelle ce paramètre par défaut est faux. Il peut y avoir des effets secondaires, surtout si vous réutilisez vos threads (comme dans les pools de threads).

8
rootkit

Si vous regardez AbstractRequestAttributesScope, vous verrez qu'il utilise le RequestAttributes actuel pour obtenir le bean souhaité.

Dans votre fil, vous voudrez probablement faire quelque chose comme ceci:

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

Inspirée par la réponse de @ mael, voici ma solution "custom-scope-out-of-the-box". J'utilise une configuration Spring entièrement basée sur des annotations.

Pour mon cas particulier, Spring's propre org.springframework.context.support.SimpleThreadScope fournit déjà le comportement recherché (à droite, c'est bizarre, car SimpleThreadScope n'utilise pas un InheritableThreadLocal, mais en fait un ThreadLocal. Mais comme cela fonctionne , Je suis déjà content).

Le comportement correct lors d'une interaction utilisateur simultanée n'a pas encore été testé.

Pas

Enregistrez le type 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");
        } 
    }
}

Maintenant, pour tout bean qui doit avoir la portée de la demande et qui doit être utilisable à partir de tout thread généré par le thread de requête HTTP, définissez la portée nouvellement définie en conséquence:

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

Pour ce problème, vérifiez ma réponse à l'adresse URL ci-dessus

Utilisation d'un bean de portée de requête en dehors d'une requête Web réelle. Si vous utilisez un conteneur Web Servlet 2.5, avec des demandes traitées en dehors de DispatcherServlet de Spring (par exemple, lorsque vous utilisez JSF ou Struts), vous devez enregistrer org.springframework.web.context.request.RequestContextListener ServletRequestListener. Pour Servlet 3.0+, cela peut être fait par programme via l'interface WebApplicationInitializer. Vous pouvez également, ou pour les conteneurs plus anciens, ajouter la déclaration suivante au fichier web.xml de votre application Web:

4
Deepak