Как включить область запроса в async task executor

В моем приложении у меня есть несколько асинхронных веб-сервисов. Сервер принимает запрос, возвращает ответ 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" />

Ответ 1

Мы столкнулись с одной и той же проблемой - для выполнения кода в фоновом режиме с помощью @Async, поэтому он не смог использовать какой-либо Session- или RequestScope beans. Мы решили это следующим образом:

  • Создайте собственный TaskPoolExecutor, который сохраняет информацию с областями с задачами
  • Создайте специальный Callable (или Runnable), который использует эту информацию для установки и очистки контекста для фонового потока
  • Создайте конфигурацию переопределения для использования пользовательского исполнителя

Примечание: это будет работать только для Session и Request scoped beans, а не для контекста безопасности (как в Spring Безопасность). Вам нужно будет использовать другой метод для настройки контекста безопасности, если это то, что вам нужно.

Примечание2. Для краткости отображается только функция 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()));
    }
}

Callable:

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

Ответ 2

Самый простой способ - использовать декоратор задач, например так:

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

Нет необходимости в дополнительном держателе или исполнителе пользовательского пула потоков.

Ответ 3

Невозможно получить объект области запроса в дочернем асинхронном потоке, поскольку исходный родительский поток обработки запросов, возможно, уже передал ответ клиенту, и все объекты запроса уничтожены. Одним из способов обработки таких сценариев является использование настраиваемой области видимости, например SimpleThreadScope.

одна проблема с SimpleThreadScope состоит в том, что дочерние потоки не будут наследовать переменные области видимости родителей, поскольку он использует простой ThreadLocal для внутреннего использования. Чтобы преодолеть это, реализуйте пользовательскую область, которая в точности похожа на SimpleThreadScope, но использует InheritableThreadLocal для внутреннего использования. За дополнительной информацией обращайтесь к Spring MVC: Как использовать bean-объект в области запросов внутри порожденного потока?

Ответ 4

Решения, упомянутые ранее, не помогли мне. Причина, по которой решение не работает, как упоминалось в посте @Thilak, заключается в том, что как только исходный родительский поток передал ответ клиенту, объекты запроса могут быть собраны сборщиком мусора. Но с некоторыми изменениями в решении @Armadillo я смог заставить его работать. Я использую весеннюю загрузку 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()));
    }
}

Callable:

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("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.
    }

Ответ 5

Ответ @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()));
    }
}

Пользовательская реализация для Runnable:

/**
 * 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 answer
        this.context = context;
    }

    @Override
    public void run() {
        if (context != null) {
            RequestContextHolder.setRequestAttributes(context);
        }

        try {
            task.run();
        } finally {
            RequestContextHolder.resetRequestAttributes();
        }
    }
}