How to enable request scope in async task executor
在我的应用程序中,我有一些异步Web服务。 服务器接受请求,返回OK响应,并开始使用AsyncTaskExecutor处理请求。 我的问题是如何在此处启用请求范围,因为在此处理中,我需要获取注释为的类:
1 | @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) |
现在我得到异常:
1 | org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.requestContextImpl': 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. |
因为它在
我对请求的异步处理
1 2 3 4 5 6 7 |
taskExecutor在哪里:
1 | <bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor" /> |
我们遇到了同样的问题-需要使用@Async在后台执行代码,因此它无法使用任何Session-或RequestScope bean。我们通过以下方式解决了它:
- 创建一个自定义TaskPoolExecutor,用于将范围信息与任务一起存储
- 创建一个特殊的Callable(或Runnable),该信息使用该信息来设置和清除后台线程的上下文
- 创建替代配置以使用自定义执行程序
注意:这仅适用于Session和Request作用域的bean,不适用于安全上下文(如Spring Security中一样)。如果您要这样做,则必须使用另一种方法来设置安全上下文。
注意2:为简便起见,仅显示Callable和Submit()实现。您可以对Runnable和execute()执行相同的操作。
这是代码:
执行者:
1 2 3 4 5 6 7 8 9 10 11 | public class ContextAwarePoolExecutor extends ThreadPoolTaskExecutor { @Override public < T > Future< T > submit(Callable< T > task) { return super.submit(new ContextAwareCallable(task, RequestContextHolder.currentRequestAttributes())); } @Override public < T > ListenableFuture< T > submitListenable(Callable< T > task) { return super.submitListenable(new ContextAwareCallable(task, RequestContextHolder.currentRequestAttributes())); } } |
可致电:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | public class ContextAwareCallable< T > implements Callable< T > { private Callable< T > task; private RequestAttributes context; public ContextAwareCallable(Callable< T > task, RequestAttributes context) { this.task = task; this.context = context; } @Override public T call() throws Exception { if (context != null) { RequestContextHolder.setRequestAttributes(context); } try { return task.call(); } finally { RequestContextHolder.resetRequestAttributes(); } } } |
组态:
1 2 3 4 5 6 7 8 | @Configuration public class ExecutorConfig extends AsyncConfigurerSupport { @Override @Bean public Executor getAsyncExecutor() { return new ContextAwarePoolExecutor(); } } |
最简单的方法是使用任务装饰器,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | static class ContextCopyingDecorator implements TaskDecorator { @Nonnull @Override public Runnable decorate(@Nonnull Runnable runnable) { RequestAttributes context = RequestContextHolder.currentRequestAttributes(); Map<String, String> contextMap = MDC.getCopyOfContextMap(); return () -> { try { RequestContextHolder.setRequestAttributes(context); MDC.setContextMap(contextMap); runnable.run(); } finally { MDC.clear(); RequestContextHolder.resetRequestAttributes(); } }; } } |
要将这个装饰器添加到任务执行器中,您所需要做的就是将其添加到配置例程中:
1 2 3 4 5 6 7 8 | @Override @Bean public Executor getAsyncExecutor() { ThreadPoolTaskExecutor poolExecutor = new ThreadPoolTaskExecutor(); poolExecutor.setTaskDecorator(new ContextCopyingDecorator()); poolExecutor.initialize(); return poolExecutor; } |
不需要额外的持有人或自定义线程池任务执行器。
由于原始的父请求处理线程可能已经将响应提交给客户端,并且所有请求对象都被破坏,因此无法在子异步线程中获取请求范围的对象。处理此类情况的一种方法是使用自定义范围,例如SimpleThreadScope。
SimpleThreadScope的一个问题是子线程将不会继承父范围变量,因为它在内部使用了简单的ThreadLocal。为了克服这一点,请实现一个与SimpleThreadScope完全相似但内部使用InheritableThreadLocal的自定义范围。有关更多信息,请参见
Spring MVC:如何在生成的线程中使用请求范围的bean?
前面提到的解决方案不适用于我。
如@Thilak的帖子所述,该解决方案不起作用的原因是,一旦原始父线程对客户端提交了响应,则可能会垃圾收集请求对象。
但是,通过对@Armadillo提供的解决方案进行了一些调整,我得以使其工作。我正在使用Spring Boot 2.2
这是我遵循的。
-
创建一个自定义TaskPoolExecutor,用于存储(克隆后)作用域
有关任务的信息。 -
创建一个特殊的Callable(或Runnable)
使用克隆的信息来设置当前上下文值
并清除异步线程的上下文。
执行者(与@Armadillo的帖子相同):
1 2 3 4 5 6 7 8 9 10 11 | public class ContextAwarePoolExecutor extends ThreadPoolTaskExecutor { @Override public < T > Future< T > submit(Callable< T > task) { return super.submit(new ContextAwareCallable(task, RequestContextHolder.currentRequestAttributes())); } @Override public < T > ListenableFuture< T > submitListenable(Callable< T > task) { return super.submitListenable(new ContextAwareCallable(task, RequestContextHolder.currentRequestAttributes())); } } |
可致电:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | public class ContextAwareCallable< T > implements Callable< T > { private Callable< T > task; private final RequestAttributes requestAttributes; public ContextAwareCallable(Callable< T > task, RequestAttributes requestAttributes) { this.task = task; this.requestAttributes = cloneRequestAttributes(requestAttributes); } @Override public T call() throws Exception { try { RequestContextHolder.setRequestAttributes(requestAttributes); return task.call(); } finally { RequestContextHolder.resetRequestAttributes(); } } private RequestAttributes cloneRequestAttributes(RequestAttributes requestAttributes){ RequestAttributes clonedRequestAttribute = null; try{ clonedRequestAttribute = new ServletRequestAttributes(((ServletRequestAttributes) requestAttributes).getRequest(), ((ServletRequestAttributes) requestAttributes).getResponse()); if(requestAttributes.getAttributeNames(RequestAttributes.SCOPE_REQUEST).length>0){ for(String name: requestAttributes.getAttributeNames(RequestAttributes.SCOPE_REQUEST)){ clonedRequestAttribute.setAttribute(name,requestAttributes.getAttribute(name,RequestAttributes.SCOPE_REQUEST),RequestAttributes.SCOPE_REQUEST); } } if(requestAttributes.getAttributeNames(RequestAttributes.SCOPE_SESSION).length>0){ for(String name: requestAttributes.getAttributeNames(RequestAttributes.SCOPE_SESSION)){ clonedRequestAttribute.setAttribute(name,requestAttributes.getAttribute(name,RequestAttributes.SCOPE_SESSION),RequestAttributes.SCOPE_SESSION); } } if(requestAttributes.getAttributeNames(RequestAttributes.SCOPE_GLOBAL_SESSION).length>0){ for(String name: requestAttributes.getAttributeNames(RequestAttributes.SCOPE_GLOBAL_SESSION)){ clonedRequestAttribute.setAttribute(name,requestAttributes.getAttribute(name,RequestAttributes.SCOPE_GLOBAL_SESSION),RequestAttributes.SCOPE_GLOBAL_SESSION); } } return clonedRequestAttribute; }catch(Exception e){ return requestAttributes; } } } |
我所做的更改是引入cloneRequestAttributes()来复制和设置RequestAttribute,以便即使在原始父线程将响应提交给客户端之后,这些值仍然可用。
组态:
由于存在其他异步配置,并且我不希望该行为适用于其他异步执行器,因此我创建了自己的任务执行器配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Configuration @EnableAsync public class TaskExecutorConfig { @Bean(name ="contextAwareTaskExecutor") public TaskExecutor getContextAwareTaskExecutor() { ContextAwarePoolExecutor taskExecutor = new ConAwarePoolExecutor(); taskExecutor.setMaxPoolSize(20); taskExecutor.setCorePoolSize(5); taskExecutor.setQueueCapacity(100); taskExecutor.setThreadNamePrefix("ContextAwareExecutor-"); return taskExecutor; } } |
最后,在异步方法上,我使用执行程序名称。
1 2 3 4 | @Async("contextAwareTaskExecutor") public void asyncMethod() { } |
替代解决方案:
通过尝试重用现有的组件类,我们最终陷入了麻烦。尽管该解决方案使它看起来很方便。如果我们可以将相关请求范围的值称为方法参数,那么麻烦就更少了(克隆对象和保留线程池)。在我们的例子中,我们计划以某种方式重构代码,使得使用请求范围Bean并从async方法中重用的组件类接受这些值作为方法参数。将请求范围的Bean从可重用组件中删除,并移至调用其方法的组件类中。
将我刚才描述的内容放入代码中:
我们目前的状态是:
1 2 3 4 | @Async("contextAwareTaskExecutor") public void asyncMethod() { reUsableCompoment.executeLogic() //This component uses the request scoped bean. } |
重构代码:
1 2 3 4 | @Async("taskExecutor") public void asyncMethod(Object requestObject) { reUsableCompoment.executeLogic(requestObject); //Request scoped bean is removed from the component and moved to the component class which invokes it menthod. } |
上面的解决方案都不适合我,因为在我的情况下,父线程将请求返回给客户端,并且在任何工作线程中都不能引用请求范围的对象。
我只是进行了一些工作,以使上述工作正常进行。我正在使用Spring Boot 2.2,并将customTaskExecutor与上面指定的ContextAwareCallable一起使用。
异步配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 | @Bean(name ="cachedThreadPoolExecutor") public Executor cachedThreadPoolExecutor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ContextAwarePoolExecutor(); threadPoolTaskExecutor.setCorePoolSize(corePoolSize); threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize); threadPoolTaskExecutor.setQueueCapacity(queueCapacity); threadPoolTaskExecutor.setAllowCoreThreadTimeOut(true); threadPoolTaskExecutor.setThreadNamePrefix("ThreadName-"); threadPoolTaskExecutor.initialize(); return threadPoolTaskExecutor; } |
ContextAwarePoolExecutor:
1 2 3 4 5 6 7 8 9 10 11 12 13 | public class ContextAwarePoolExecutor extends ThreadPoolTaskExecutor { @Override public < T > Future< T > submit(Callable< T > task) { return super.submit(new ContextAwareCallable(task, RequestContextHolder.currentRequestAttributes())); } @Override public < T > ListenableFuture< T > submitListenable(Callable< T > task) { return super.submitListenable(new ContextAwareCallable(task, RequestContextHolder.currentRequestAttributes())); } |
}
创建的自定义上下文感知可调用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | public class ContextAwareCallable< T > implements Callable< T > { private Callable< T > task; private CustomRequestScopeAttributes customRequestScopeAttributes; private static final String requestScopedBean = "scopedTarget.requestScopeBeanName"; public ContextAwareCallable(Callable< T > task, RequestAttributes context) { this.task = task; if (context != null) { //This is Custom class implements RequestAttributes class this.customRequestScopeAttributes = new CustomRequestScopeAttributes(); //Add the request scoped bean to Custom class customRequestScopeAttributes.setAttribute (requestScopedBean,context.getAttribute(requestScopedBean,0),0); //Set that in RequestContextHolder and set as Inheritable as true //Inheritable is used for setting the attributes in diffrent ThreadLocal objects. RequestContextHolder.setRequestAttributes (customRequestScopeAttributes,true); } } @Override public T call() throws Exception { try { return task.call(); } finally { customRequestScopeAttributes.removeAttribute(requestScopedBean,0); } } } |
自定义类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | public class CustomRequestScopeAttributes implements RequestAttributes { private Map<String, Object> requestAttributeMap = new HashMap<>(); @Override public Object getAttribute(String name, int scope) { if(scope== RequestAttributes.SCOPE_REQUEST) { return this.requestAttributeMap.get(name); } return null; } @Override public void setAttribute(String name, Object value, int scope) { if(scope== RequestAttributes.SCOPE_REQUEST){ this.requestAttributeMap.put(name, value); } } @Override public void removeAttribute(String name, int scope) { if(scope== RequestAttributes.SCOPE_REQUEST) { this.requestAttributeMap.remove(name); } } @Override public String[] getAttributeNames(int scope) { if(scope== RequestAttributes.SCOPE_REQUEST) { return this.requestAttributeMap.keySet().toArray(new String[0]); } return new String[0]; } //Override all methods in the RequestAttributes Interface. } |
最后,在所需的方法中添加Async注释。
1 2 3 4 | @Async("cachedThreadPoolExecutor") public void asyncMethod() { anyService.execute() //This Service execution uses request scoped bean } |
@Armadillo
为我工作,非常感谢。
至于Spring Security Context,还有更多现成的解决方案,它对我也都有效(请参阅此处如何设置Spring Security SecurityContextHolder策略?)
为了在子线程中使用SecurityContextHolder:
1 2 3 4 5 6 7 8 | @Bean public MethodInvokingFactoryBean methodInvokingFactoryBean() { MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean(); methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class); methodInvokingFactoryBean.setTargetMethod("setStrategyName"); methodInvokingFactoryBean.setArguments(new String[]{SecurityContextHolder.MODE_INHERITABLETHREADLOCAL}); return methodInvokingFactoryBean; } |
我通过添加以下bean配置解决了这个问题
1 2 3 4 5 6 7 8 9 | <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="request"> <bean class="org.springframework.context.support.SimpleThreadScope"/> </entry> </map> </property> </bean> |
更新:上面的解决方案没有清除与线程相关的任何对象,如spring文档中所述。此替代方法对我有效:https://www.springbyexample.org/examples/custom-thread-scope-module.html
使用Spring-boot-2.0.3.REALEASE / spring-web-5.0.7,我想出了以下适用于@Async的代码
包含ThreadLocal上下文的类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import java.util.Map; public class ThreadContextHolder { private ThreadContextHolder() {} private static final ThreadLocal<Map<String, Object>> ctx = new ThreadLocal<>(); public static Map<String, Object> getContext() { return ctx.get(); } public static void setContext(Map<String, Object> attrs) { ctx.set(attrs); } public static void removeContext() { ctx.remove(); } } |
异步配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @Bean public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); ... ... executor.setTaskDecorator( runnable -> { RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); // or currentRequestAttributes() if you want to fall back to JSF context. Map<String, Object> map = Arrays.stream(requestAttributes.getAttributeNames(0)) .collect(Collectors.toMap(r -> r, r -> requestAttributes.getAttribute(r, 0))); return () -> { try { ThreadContextHolder.setContext(map); runnable.run(); } finally { ThreadContextHolder.removeContext(); } }; }); executor.initialize(); return executor; } |
从异步方法:
1 2 3 4 | @Async public void asyncMethod() { logger.info("{}", ThreadContextHolder.getContext().get("key")); } |
@Armadillo的回答促使我为Runnable编写实现。
TaskExecutor的自定义实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * This custom ThreadPoolExecutor stores scoped/context information with the tasks. */ public class ContextAwareThreadPoolExecutor extends ThreadPoolTaskExecutor { @Override public Future< ? > submit(Runnable task) { return super.submit(new ContextAwareRunnable(task, RequestContextHolder.currentRequestAttributes())); } @Override public ListenableFuture< ? > submitListenable(Runnable task) { return super.submitListenable(new ContextAwareRunnable(task, RequestContextHolder.currentRequestAttributes())); } } |
可运行的自定义实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** * This custom Runnable class can use to make background threads context aware. * It store and clear the context for the background threads. */ public class ContextAwareRunnable implements Runnable { private Runnable task; private RequestAttributes context; public ContextAwareRunnable(Runnable task, RequestAttributes context) { this.task = task; // Keeps a reference to scoped/context information of parent thread. // So original parent thread should wait for the background threads. // Otherwise you should clone context as @Arun A's answer this.context = context; } @Override public void run() { if (context != null) { RequestContextHolder.setRequestAttributes(context); } try { task.run(); } finally { RequestContextHolder.resetRequestAttributes(); } } } |