You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring请求作用域Bean传递至子线程的问题求助

问题:异步线程中无法访问请求作用域Bean

我在微服务应用中处理请求作用域Java Bean与异步逻辑时遇到了问题。服务间通过REST API通信,使用当前登录用户的JWT令牌完成服务间授权。我希望异步处理耗时较长的流程,完成后向用户发送通知,但调用NotificationApi时总是抛出以下错误:

Error creating bean with name ''scopedTarget.notificationServiceProxy'': 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

相关代码结构如下:

异步任务服务

// async service
@Service
@RequiredArgsConstructor
public class AsyncTaskService {
    private final AsyncTaskRepository asyncTaskRepository;
    private final ThreadPoolExecutor executorService = (ThreadPoolExecutor) Executors.newFixedThreadPool(10);
    
    public UUID startAsyncTask (Supplier<?> task) {
        ClassLoader contentClassLoader = this.getClass().getClassLoader();
        UUID someId = UUID.randomUUID();
        
        CompletableFuture<?> future = CompletableFuture.supplyAsync(() -> {
            Thread.currentThread().setContextClassLoader(contentClassLoader);
            return task.get();
        }, executorService);
        
        future.whenComplete((result, throwable) -> {
            if (throwable != null) {
                log.error("Task with id {} finished unsuccessfully. ", someId, throwable);
            } else {
                log.info("Task with id {} finished successfully.", someId);
            }
        });
        return someId;
    }
}

长流程服务

// long-lasting processes service
@Service
@RequiredArgsConstructor
public class LongLastingProcessesService {
    private final AsyncTaskService asyncTaskService;
    private final NotificationApi notificationApi;
    
    public UUID handleLongLastingProcess() {
        RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
        
        return asyncTaskService.startAsyncTask(() -> {
            RequestContextHolder.setRequestAttributes(requestAttributes, true);
            
            var result = someService.someMethod(someData);
            
            notificationApi.createNotification(someDataToBuildNotification);
            
            RequestContextHolder.resetRequestAttributes();
            return result;
        });
    }
}

NotificationApi定义

// notificationApi
@HttpExchange(url = "someUrlForNotificationMicroservice")
public interface NotificationApi {
    @PostExchange
    Notification createNotification(@RequestBody NotificationBody notificationBody);
}

请求作用域Bean配置

// proxy configuration
@Configuration
class ProxyConfiguration {
    
    @Bean
    @RequestScope
    RestClient notificationRestClient () {
        if (SecurityContextHolder.getContext().getAuthentication() != null) {
            return createRestClient("notification");
        }
        return RestClient.builder().baseUrl(retrieveInstanceUrl("notification")).defaultHeaders(httpHeaders -> {
            httpHeaders.setBearerAuth(((JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication()).getToken().getTokenValue());
        }).build();
    }

    @Bean
    @RequestScope
    NotificationApi notificationServiceProxy () {
        RestClientAdapter adapter = RestClientAdapter.create(notificationRestClient());
        HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
        return factory.createClient(NotificationApi.class);
    }
    
    private RestClient createRestClient (String appName) {
        return RestClient.builder().baseUrl(retrieveInstanceUrl(appName)).defaultStatusHandler(
            HttpStatusCode::isError,
            (request, response) -> log.error(
                "Error occurred when communicating between services: {} {}",
                request, response
            )
        ).defaultHeaders(httpHeaders -> {
            httpHeaders.setBearerAuth(((JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication()).getToken().getTokenValue());
        }).build();
    }
}

我尝试通过RequestContextHolder将请求属性传递到子线程,但并未生效,请问如何在异步线程中访问这些请求作用域的Bean?


解决方案

问题核心是请求作用域Bean依赖的SecurityContext和RequestContext在异步线程中未正确传递,且Spring默认的请求作用域代理在跨线程场景下无法复用原有上下文。以下是具体解决步骤:

1. 显式传递JWT令牌,脱离请求作用域依赖

请求作用域的NotificationApi本质是为了获取当前用户的JWT令牌,不如在主线程中提前提取令牌,传递给异步任务,彻底避免依赖请求上下文:

修改LongLastingProcessesService:

@Service
@RequiredArgsConstructor
public class LongLastingProcessesService {
    private final AsyncTaskService asyncTaskService;
    private final ProxyConfiguration proxyConfiguration;
    
    public UUID handleLongLastingProcess() {
        // 主线程中提取JWT令牌
        String jwtToken = ((JwtAuthenticationToken) SecurityContextHolder.getContext().getAuthentication()).getToken().getTokenValue();
        
        return asyncTaskService.startAsyncTask(() -> {
            // 异步线程中手动构建NotificationApi
            RestClient restClient = proxyConfiguration.createRestClientWithToken("notification", jwtToken);
            RestClientAdapter adapter = RestClientAdapter.create(restClient);
            HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
            NotificationApi notificationApi = factory.createClient(NotificationApi.class);
            
            var result = someService.someMethod(someData);
            notificationApi.createNotification(someDataToBuildNotification);
            
            return result;
        });
    }
}

在ProxyConfiguration中新增令牌传递方法:

@Configuration
class ProxyConfiguration {
    // ... 原有代码
    
    // 根据传入的令牌创建RestClient
    public RestClient createRestClientWithToken(String appName, String jwtToken) {
        return RestClient.builder().baseUrl(retrieveInstanceUrl(appName)).defaultStatusHandler(
            HttpStatusCode::isError,
            (request, response) -> log.error(
                "Error occurred when communicating between services: {} {}",
                request, response
            )
        ).defaultHeaders(httpHeaders -> {
            httpHeaders.setBearerAuth(jwtToken);
        }).build();
    }
}

2. 配置线程池支持Spring上下文传递

如果必须保留请求作用域Bean的使用,需要让自定义线程池支持Spring上下文的传递:

修改AsyncTaskService,使用Spring管理的TaskExecutor:

@Service
@RequiredArgsConstructor
public class AsyncTaskService {
    private final AsyncTaskRepository asyncTaskRepository;
    private final TaskExecutor taskExecutor;
    
    public UUID startAsyncTask (Supplier<?> task) {
        UUID someId = UUID.randomUUID();
        
        CompletableFuture<?> future = CompletableFuture.supplyAsync(task, taskExecutor);
        
        future.whenComplete((result, throwable) -> {
            if (throwable != null) {
                log.error("Task with id {} finished unsuccessfully. ", someId, throwable);
            } else {
                log.info("Task with id {} finished successfully.", someId);
            }
        });
        return someId;
    }
}

配置支持上下文传递的TaskExecutor:

@Configuration
public class AsyncConfig {
    @Bean
    public TaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(20);
        executor.setThreadNamePrefix("async-task-");
        // 关键:启用上下文传递,让异步线程继承主线程的请求与安全上下文
        executor.setTaskDecorator(new ContextDecorator());
        executor.initialize();
        return executor;
    }
    
    // 自定义装饰器,传递Request和Security上下文
    static class ContextDecorator implements TaskDecorator {
        @Override
        public Runnable decorate(Runnable runnable) {
            RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            
            return () -> {
                try {
                    RequestContextHolder.setRequestAttributes(requestAttributes);
                    SecurityContextHolder.getContext().setAuthentication(authentication);
                    runnable.run();
                } finally {
                    RequestContextHolder.resetRequestAttributes();
                    SecurityContextHolder.clearContext();
                }
            };
        }
    }
}

3. 确保请求作用域Bean的代理模式正确

修改NotificationApi的Bean配置,指定代理模式为TARGET_CLASS:

@Bean
@RequestScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
NotificationApi notificationServiceProxy () {
    RestClientAdapter adapter = RestClientAdapter.create(notificationRestClient());
    HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
    return factory.createClient(NotificationApi.class);
}

原有方法失效原因

  • 手动创建的ThreadPoolExecutor未处理Spring上下文传递,即使通过RequestContextHolder设置属性,Spring的请求作用域代理仍无法识别异步线程中的上下文。
  • 请求作用域Bean的初始化依赖活跃的请求上下文,异步线程默认无此上下文,导致Bean创建失败。

内容的提问来源于stack exchange,提问作者rh3h

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 05:34:54