MVC de printemps: comment utiliser un bean request-scoped à l'intérieur d'un thread spawned?

dans une application MVC de printemps, j'ai un haricot request-scoped. J'injecte ce haricot quelque part. Là, le thread de serving HTTP-request pourrait éventuellement générer un nouveau thread.

mais chaque fois que j'essaie d'accéder à la demande-scoped bean à partir du nouveau thread, j'obtiens un org.springframework.beans.factory.BeanCreationException (voir trace de la pile ci-dessous).

L'accès au bean request-scoped à partir du thread de requête HTTP fonctionne très bien.

Comment puis-je mettre un haricot sur demande à la disposition des threads généré par le thread de requête HTTP?


installation facile

lancez les extraits de code suivants. Puis lancez un serveur, par exemple à http://example.com:8080.

Lors de l'accès à http://example.com:8080/scopetestnormal, chaque fois qu'une demande est faite à l'adresse counter est incrémenté de 1 (perceptible via la sortie logger). :) Super!

lors de l'accès http://example.com:8080/scopetestthread, chaque fois qu'une demande est faite à cette adresse, les exceptions mentionnées sont jetés. : (. Peu importe ce que choisi ScopedProxyMode, cela se produit pour les deux CGLIB-basé et JDK-dynamique-proxy-interface basée sur la demande d'étendue de haricots

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 du 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 la pile pour la fève à la demande scopée à base de CGLIB (proxyMode = ScopedProxyMode.TARGET_CLASS)

SLF4J: Failed toString() invocation on an object of type [$java.lang.Number$$EnhancerByCGLIB$ffcde7]
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$ffcde7.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 la Pile pour JDK-dynamique-proxy-interface basée sur la demande d'étendue de haricot (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
25
demandé sur Abdull 2013-02-20 21:47:31

5 réponses

OK, en lisant le code dans SimpleThreadScope qui vient avec le printemps je pense que vous pouvez créer un simple Heritablethreadscope en utilisant un InheritableThreadLocal à la place.

alors utilisez juste un peu de xml pour enregistrer votre champ d'application personnalisé:

<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 haricot avec un thread-inherited scope, vous aurez accès à cette fève avec une copie par thread et cette copie sera disponible dans les threads générés par votre thread i.e. une fève scoped de requête qui peut être utilisée dans les threads générés dans votre thread de requête.

8
répondu ElderMael 2013-02-20 18:37:54

la configuration ci-dessous propagera le contexte de la requête à 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>

avertissement: Je ne l'ai pas testé spécifiquement avec des haricots request-scoped que je n'utilise pas. J'ai testé que RequestContextHolder renvoie un contexte valide dans les threads enfant.

Disclaimer 2: Il y a une raison pour laquelle ce paramètre est par défaut à false. Il peut y avoir des effets secondaires, surtout si vous réutilisez vos threads (comme dans threadpools).

8
répondu rootkit 2013-02-20 19:50:17

Inspiré par @mael réponse, voici mon "custom-portée-de-la-case" solution. J'utilise une configuration à ressort entièrement annotée.

Pour mon cas particulier, le Printemps est propre org.springframework.context.support.SimpleThreadScope fournit déjà le comportement que la question cherche (à droite, c'est bizarre, parce que SimpleThreadScope n'utilise pas un InheritableThreadLocal, mais effectivement un ThreadLocal. Mais comme cela fonctionne, je suis déjà heureux).

le comportement Correct lors de l'interaction avec l'utilisateur n'a pas été testé encore.

Mesures

Enregistrer le SimpleThreadScope type:

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 request-scope et qui doit être utilisable à partir de n'importe quel thread généré par le thread HTTP request, définissez la nouvelle portée 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
répondu Abdull 2013-02-20 23:32:20

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

Pour Ce Problème, vérifiez Ma réponse ci-dessus étant donné l'url

utilisation d'une requête scoped bean en dehors d'une requête Web réelle. Si vous utilisez un conteneur Web Servlet 2.5, avec des requêtes traitées en dehors de la répartiteur de Spring (par exemple, en utilisant JSF ou Struts), vous devez enregistrer l'org.springframework.Web.cadre.demande.RequestContextListener ServletRequestListener. Pour Servlet 3.0+, cela peut fait par programmation via L'interface WebApplicationInitializer. Alternativement, ou pour les conteneurs plus anciens, ajouter la déclaration suivante au web de votre application web.fichier xml:

3
répondu Deepak 2017-05-23 12:26:14

Si vous avez un coup d'oeil à AbstractRequestAttributesScope vous allez voir que c'est à l'aide de RequestAttributes afin d'obtenir le haricot désiré.

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();  
2
répondu Emanuel Hategan 2016-06-24 11:24:24