如何在异步任务执行程序中启用请求范围

时间:2014-05-19 07:32:11

标签: java spring

在我的应用程序中,我有一些异步Web服务。服务器接受请求,返回OK响应并使用AsyncTaskExecutor启动处理请求。我的问题是如何在此处启用请求范围,因为在此处理中我需要获取由以下内容注释的类:

@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)

现在我得到例外:

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

中运行

我的异步处理请求

taskExecutor.execute(new Runnable() {

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

其中taskExecutor是:

<bean id="taskExecutor" class="org.springframework.core.task.SimpleAsyncTaskExecutor" />

10 个答案:

答案 0 :(得分:59)

我们遇到了同样的问题 - 需要使用@Async在后台执行代码,因此它无法使用任何Session或RequestScope bean。我们通过以下方式解决了这个问题:

  • 创建一个自定义TaskPoolExecutor,用于存储带有任务的范围信息
  • 创建一个特殊的Callable(或Runnable),使用该信息设置和清除后台线程的上下文
  • 创建覆盖配置以使用自定义执行程序

注意:这只适用于Session和Request范围的bean,而不适用于安全上下文(如Spring Security)。您必须使用其他方法来设置安全上下文,如果这是您之后的事情。

Note2 :为简洁起见,仅显示了Callable和submit()实现。您可以对Runnable和execute()执行相同的操作。

以下是代码:

执行人:

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

可赎回:

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

配置:

@Configuration
public class ExecutorConfig extends AsyncConfigurerSupport {
    @Override
    @Bean
    public Executor getAsyncExecutor() {
        return new ContextAwarePoolExecutor();
    }
}

答案 1 :(得分:15)

最简单的方法是使用这样的任务装饰器:

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

要将此装饰器添加到任务执行器,您只需将其添加到配置例程中:

@Override
@Bean
public Executor getAsyncExecutor() {
    ThreadPoolTaskExecutor poolExecutor = new ThreadPoolTaskExecutor();
    poolExecutor.setTaskDecorator(new ContextCopyingDecorator());
    poolExecutor.initialize();
    return poolExecutor;
}

不需要额外的持有者或自定义线程池任务执行程序。

答案 2 :(得分:11)

无法在子异步线程中获取请求范围对象,因为原始父请求处理线程可能已将响应提交给客户端,并且所有请求对象都已销毁。处理此类方案的一种方法是使用自定义范围,例如SimpleThreadScope

SimpleThreadScope的一个问题是子线程不会继承父范围变量,因为它在内部使用简单的ThreadLocal。要克服该实现自定义范围,它与SimpleThreadScope完全相似,但在内部使用InheritableThreadLocal。有关更多信息,请注册 Spring MVC: How to use a request-scoped bean inside a spawned thread?

答案 3 :(得分:5)

前面提到的解决方案不适用于我。 如@Thilak的帖子所述,解决方案不起作用的原因是,一旦原始父线程向客户端提交了响应,就可以对请求对象进行垃圾收集。 但是通过对@Armadillo提供的解决方案进行了一些调整,我得以使其工作。我正在使用Spring Boot 2.2

这是我关注的。

  • 创建一个自定义TaskPoolExecutor,用于存储(克隆后)作用域 有关任务的信息。
  • 创建特殊的Callable(或Runnable) 使用克隆的信息来设置当前上下文值 并清除异步线程的上下文。

执行者(与@Armadillo的帖子相同):

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

可致电:

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,以便即使在原始父线程将响应提交给客户端之后,这些值仍然可用。

配置: 由于存在其他异步配置,并且我不希望该行为适用于其他异步执行器,因此我创建了自己的任务执行器配置。

@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;
    }
}

最后,在异步方法上,我使用执行程序名称。

    @Async("contextAwareTaskExecutor")
    public void asyncMethod() {

    }

替代解决方案:

通过尝试重用现有的组件类,我们最终陷入了麻烦。尽管该解决方案使它看起来很方便。如果我们可以将相关请求范围的值称为方法参数,那么它的麻烦就更少了(克隆对象和保留线程池)。在我们的案例中,我们计划以某种方式重构代码,使得使用请求范围Bean并从async方法重用的组件类接受这些值作为方法参数。请求范围的Bean从可重用组件中删除,并移至调用其方法的组件类中。 将我刚才描述的内容写在代码中:

我们当前的状态是:

@Async("contextAwareTaskExecutor")
    public void asyncMethod() {
       reUsableCompoment.executeLogic() //This component uses the request scoped bean.
    }

重构代码:

    @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.
    }

答案 4 :(得分:2)

上述解决方案都不对我有用,因为在我的情况下,父线程将请求响应返回给客户端,并且在任何工作线程中都不能引用请求范围的对象。

我只是想办法使上述事情起作用。我正在使用Spring Boot 2.2,并将customTaskExecutor与上面指定的ContextAwareCallable一起使用。

异步配置:

@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:

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

   }

}

创建的可自定义自定义上下文可调用:

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

自定义类别:

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("cachedThreadPoolExecutor")    
  public void asyncMethod() {     
     anyService.execute() //This Service execution uses request scoped bean
  }

答案 5 :(得分:0)

@Armadillo的回答促使我为Runnable编写实现。

TaskExecutor的自定义实现:

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

可运行的自定义实现:

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

答案 6 :(得分:0)

对于Spring-boot-2.0.3.REALEASE / spring-web-5.0.7,我想出了以下适用于@Async的代码

包含ThreadLocal上下文的类。

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

异步配置:

      @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;
      }

然后通过async方法:

@Async
  public void asyncMethod() {
    logger.info("{}", ThreadContextHolder.getContext().get("key"));
  }

答案 7 :(得分:0)

我通过添加以下bean配置解决了这个问题

<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

答案 8 :(得分:0)

@Armadillo

  1. 为我工作,非常感谢。

  2. 对于Spring Security Context,有更多现成的解决方案,并且对我也都有效(在How to set up Spring Security SecurityContextHolder strategy?处找到)

为了在子线程中使用SecurityContextHolder:

@Bean
public MethodInvokingFactoryBean methodInvokingFactoryBean() {
    MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
    methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class);
    methodInvokingFactoryBean.setTargetMethod("setStrategyName");
    methodInvokingFactoryBean.setArguments(new String[]{SecurityContextHolder.MODE_INHERITABLETHREADLOCAL});
    return methodInvokingFactoryBean;
}

答案 9 :(得分:0)

对于任何想通过API中的非阻塞I / O命令使用RequestScope的人,这都是一个相关的答案,而不是分解生活在原始HTTP请求之后的子线程。

目标

大多数人的目标目标是

  • 避免在I / O期间阻塞Java API中的线程,以提高效率
  • 技术上简单的代码

NODEJS / .NET行为

在其他API技术堆栈中编写这样的代码是很标准的,这样API线程可以在等待I / O完成的同时返回线程池。我花了一段时间才弄清楚如何在Java中做同样的事情:

// Start on thread 3
const data = await database.getInfo(sql);

// Then run this on thread 9
await downstreamService.postInfo(data);

// Finish on thread 7

NodeJS和.Net Core都将请求范围的对象存储在HttpRequest对象上,以便数据可以在线程之间移动。他们不使用ThreadLocal数据,因为这显然不适合这种情况。

春季同步等待请求范围

可以在Spring中实现一个自定义范围,该范围将请求范围的对象存储在当前HttpServletRequest对象中,以便可以在“ await”语句之前和之后访问这些对象:

使用和更多信息

希望下面的细节对读者有所帮助。我博客的目标之一是在不同技术堆栈之间一致地实施这种类型的非功能性编码模式,而我不想使用Java WebFlux: