modelandview - Spring MVC: ¿Cómo usar un bean con ámbito de solicitud dentro de un hilo engendrado?
spring mvc ejemplo (5)
En una aplicación Spring MVC, tengo un bean con ámbito de solicitud. Inyecto este frijol en alguna parte. Allí, el hilo de servicio de solicitud HTTP podría engendrar un nuevo hilo.
Pero cada vez que intento acceder al bean con ámbito de solicitud desde el nuevo hilo engendrado, obtengo una org.springframework.beans.factory.BeanCreationException
(ver el seguimiento de pila a continuación).
El acceso al bean con ámbito de solicitud desde el hilo de solicitud HTTP funciona bien.
¿Cómo puedo hacer que un bean con ámbito de solicitud esté disponible para los hilos generados por el hilo de solicitud HTTP?
Configuración simple
Obtenga los siguientes fragmentos de código en ejecución. Luego, inicie un servidor, por ejemplo en http://example.com:8080 .
Al acceder a http://example.com:8080/scopetestnormal , cada vez que se realiza una solicitud a esta dirección, el counter
se incrementa en 1 (notable a través de la salida del registrador). :) ¡Súper!
Al acceder a http://example.com:8080/scopetestthread , cada vez que se realiza una solicitud a esta dirección, se lanzan las excepciones mencionadas. :(. Sin importar qué ScopedProxyMode
elegido, esto sucede tanto para beans basados en CGLIB como JDK-dynamic-proxy-interface-based-based
Archivo de configuración
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();
}
*/
...................
}
Archivo controlador
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());
*/
}
}
}
Stack trace para bean con ámbito de solicitud basado en proxyMode = ScopedProxyMode.TARGET_CLASS
( 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
Stack trace para JDK-dynamic-proxy-interface-based bean con ámbito de solicitud ( 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
Inspirada por la respuesta de @mael, esta es mi solución de "alcance personalizado" desde el primer momento. Estoy usando una configuración de primavera totalmente impulsada por anotación.
Para mi caso particular, el propio org.springframework.context.support.SimpleThreadScope
Spring ya proporciona el comportamiento que la pregunta está buscando (correcto, eso es raro, porque SimpleThreadScope
no usa un InheritableThreadLocal
, sino efectivamente un ThreadLocal
. Pero a medida que funciona, Ya estoy feliz).
El comportamiento correcto en la interacción del usuario concurrente no ha sido probado todavía.
Pasos
Registre el tipo 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://.com/a/3468965/923560
*/
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
else {
logger.info("MainConfig is not backed by a ConfigurableBeanFactory");
}
}
}
Ahora, para cualquier bean que tenga request-scope y que se pueda utilizar a partir de cualquier hilo generado por el hilo de solicitud HTTP, configure el alcance recién definido en consecuencia:
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();
}
...................
}
La siguiente configuración propagará el contexto de la solicitud a los subprocesos iniciados desde la solicitud 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>
Descargo de responsabilidad: No he probado esto específicamente con los beans de ámbito de solicitud ya que no uso ninguno. Probé que RequestContextHolder devuelva un contexto válido en subprocesos hijo.
Descargo de responsabilidad 2: hay una razón por la que esta configuración se establece de manera predeterminada en falsa. Puede haber efectos secundarios, especialmente si reutiliza sus hilos (como en las piscinas de hilo).
OK, al leer el código en SimpleThreadScope que viene con Spring, creo que puedes crear un SimpleInheritableThreadScope usando un InheritableThreadLocal lugar.
Luego solo usa un poco de xml para registrar tu alcance personalizado:
<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>
Esto significa que cuando creas un bean con un alcance thread-inherited
, tendrás acceso a este bean con una copia por hilo y esa copia estará disponible en hilos generados por el hilo, es decir, un bean con ámbito de solicitud que se puede usar en hilos engendrado en su hilo de solicitud.
Si RequestAttributes
un vistazo a AbstractRequestAttributesScope
, verás que está usando los RequestAttributes
actuales para obtener el bean deseado.
En tu hilo probablemente quieras hacer algo como esto:
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();
https://.com/a/30640097/2569475
Para este problema, verifique Mi respuesta en la URL anterior
Usar un bean con ámbito de solicitud fuera de una solicitud web real. Si utiliza un contenedor web Servlet 2.5, con solicitudes procesadas fuera del DispatcherServlet de Spring (por ejemplo, cuando usa JSF o Struts), debe registrar org.springframework.web.context.request.RequestContextListener ServletRequestListener. Para Servlet 3.0+, esto puede hacerse mediante programación a través de la interfaz WebApplicationInitializer. Alternativamente, o para contenedores más antiguos, agregue la siguiente declaración al archivo web.xml de su aplicación web: