programing

비동기 작업 실행기에서 요청 범위를 활성화하는 방법

abcjava 2023. 7. 25. 20:25
반응형

비동기 작업 실행기에서 요청 범위를 활성화하는 방법

제 앱에는 비동기식 웹 서비스가 있습니다.서버가 요청을 수락하고 확인 응답을 반환한 후 AsyncTask로 요청 처리를 시작합니다.실행자.제 질문은 요청 범위를 활성화하는 방법입니다. 이 프로세스에서는 다음과 같이 주석이 달린 클래스를 얻어야 하기 때문입니다.

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

▁in로 운영되기 SimpleAsyncTaskExecutor 에없는이 DispatcherServlet

요청에 대한 나의 비동기 처리

taskExecutor.execute(new Runnable() {

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

어디서 하는 일실행자:

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

@Async를 사용하여 백그라운드에서 코드를 실행해야 하는 동일한 문제가 발생하여 Session- 또는 RequestScope 빈을 사용할 수 없었습니다.다음과 같은 방법으로 해결했습니다.

  • 작업과 함께 범위 정보를 저장하는 사용자 지정 TaskPool Executer 생성
  • 정보를 사용하여 백그라운드 스레드의 컨텍스트를 설정하고 지우는 특수 호출 가능(또는 실행 가능) 만들기
  • 사용자 지정 실행자를 사용하기 위한 재정의 구성 생성

참고: 이는 Session 및 Request 범위의 콩에만 적용되며 Spring Security와 같은 보안 컨텍스트에는 적용되지 않습니다.보안 컨텍스트를 설정하려면 다른 방법을 사용해야 합니다.

참고 2: 간략하게 설명하기 위해 통화 가능 및 제출() 구현만 보여줍니다.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();
    }
}

가장 쉬운 방법은 다음과 같은 작업 장식기를 사용하는 것입니다.

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

추가 홀더나 사용자 지정 스레드 풀 작업 실행자가 필요하지 않습니다.


: 부트를 하여 2021년형 만 확인할 수 .TaskDecorator충분할 것입니다.컨텍스트를 만들면 작업 장식기를 사용하여 Spring Boot에서 만든 실행자를 장식합니다.

이전에 언급된 해결책들은 저에게 효과가 없었습니다.솔루션이 작동하지 않는 이유는 @Thilak의 게시물에 언급된 바와 같이 원래 상위 스레드가 클라이언트에 응답을 커밋하는 즉시 요청 개체가 가비지 수집될 수 있기 때문입니다.하지만 @Armadillo가 제공하는 솔루션을 약간 수정하여 작동시킬 수 있었습니다.나는 스프링 부츠 2.2를 사용하고 있습니다.

제가 따라간 것은 다음과 같습니다.

  • 복제 후 작업과 함께 범위 정보를 저장하는 사용자 지정 TaskPool Executer를 생성합니다.
  • 복제된 정보를 사용하여 현재 컨텍스트 값을 설정하고 비동기 스레드의 컨텍스트를 지우는 특별한 호출 가능(또는 실행 가능)을 만듭니다.

실행자(@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()를 도입하여 RequestAttributes를 복사하고 설정하여 원래 상위 스레드가 클라이언트에 응답을 커밋한 후에도 값을 사용할 수 있도록 하는 것입니다.

구성:다른 비동기 구성이 있고 다른 비동기 실행자에서 해당 동작이 적용되는 것을 원하지 않았기 때문에 자체 작업 실행자 구성을 만들었습니다.

@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() {

    }

대체 솔루션:

기존 구성 요소 클래스를 재사용하려고 시도하여 이러한 문제가 발생했습니다.솔루션이 편리해 보이기는 했지만요.관련 요청 범위 값을 메서드 매개 변수로 참조할 수 있었다면 훨씬 덜 번거로웠습니다(개체 복제 및 스레드 풀 예약).우리의 경우, 요청 범위 빈을 사용하고 비동기 방식에서 재사용되는 구성 요소 클래스가 값을 메서드 매개 변수로 수락하도록 코드를 리팩터링할 계획입니다.요청 범위 빈이 재사용 가능한 구성 요소에서 제거되고 해당 메서드를 호출하는 구성 요소 클래스로 이동됩니다.제가 방금 설명한 것을 코드로 표현하자면:

현재 상태는 다음과 같습니다.

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

원래 부모 요청 처리 스레드가 이미 클라이언트에 응답을 커밋했으며 모든 요청 개체가 삭제되었을 수 있으므로 자식 비동기 스레드에서 요청 범위 개체를 가져올 수 없습니다.이러한 시나리오를 처리하는 한 가지 방법은 SimpleThreadScope와 같은 사용자 지정 범위를 사용하는 것입니다.

SimpleThreadScope의 한 가지 문제는 내부적으로 단순한 ThreadLocal을 사용하기 때문에 자식 스레드가 부모 범위 변수를 상속하지 않는다는 것입니다.이러한 문제를 해결하기 위해 SimpleThreadScope와 정확히 유사하지만 Inheritable을 사용하는 사용자 지정 범위를 구현합니다.내부적으로 로컬 스레드입니다.이번 봄 MVC에 대한 자세한 내용: 생성된 스레드 내에서 요청 범위 빈을 사용하는 방법은 무엇입니까?

이 경우 상위 스레드가 클라이언트에 대한 요청에 응답하고 요청 범위 개체를 작업자 스레드에서 참조할 수 없기 때문에 위의 솔루션 중 하나도 작동하지 않습니다.

저는 단지 위의 것들을 작동시키기 위해 작업을 했습니다.저는 Spring Boot 2.2를 사용하고 있으며 위에서 지정한 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;

}

컨텍스트 인식 풀 실행자:

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
  }

Spring-boot-2.0.3 포함.REALEASE / spring-web-5.0.7, @Async에서 작동하는 아래 코드를 생각해냈습니다.

스레드로컬 컨텍스트를 보유하는 클래스입니다.

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
  public void asyncMethod() {
    logger.info("{}", ThreadContextHolder.getContext().get("key"));
  }

@아르마딜로의 대답은 제가 Runnable의 구현체를 쓰도록 동기를 부여했습니다.

Task Executor에 대한 사용자 정의 구현:

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

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

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

다음 빈 구성을 추가하여 이 문제를 해결했습니다.

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

업데이트: 위 솔루션은 스프링 문서에 언급된 스레드와 관련된 개체를 정리하지 않습니다.이 대안은 저에게 효과가 있습니다. https://www.springbyexample.org/examples/custom-thread-scope-module.html

@아르마딜로

  1. 저를 위해 일했습니다, 감사합니다.

  2. Spring Security Context의 경우, 더 많은 즉시 사용 가능한 솔루션이 있으며 저에게도 효과가 있었습니다(Spring Security ContextHolder 전략 설정 방법은 여기 참조).

하위 스레드에서 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;
}

다음은 API에서 차단되지 않는 I/O 명령과 함께 RequestScope를 사용하려는 사용자를 위한 관련 답변으로, 원래 HTTP 요청을 통과한 하위 스레드를 회전시키는 것과는 반대입니다.

스프링 비동기 대기 요청 범위

봄에 요청 범위 개체를 현재 HttpServletRequest 개체에 저장하는 사용자 지정 범위를 구현하여 'wait' 문 앞과 뒤에 개체에 액세스할 수 있습니다.

언급URL : https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executor

반응형