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
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.
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).
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();
}
...................
}
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:
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();