关于Java:如何在异步任务执行器中启用请求范围

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.

因为它在SimpleAsyncTaskExecutor中而不是在DispatcherServlet中运行

我对请求的异步处理

1
2
3
4
5
6
7
taskExecutor.execute(new Runnable() {

    @Override
    public void run() {
        asyncRequest(request);
    }
});

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