多線程之HardCodedTarget(type=OssFileClient, name=file, url=http://file)異常
摘要: 文檔描述了多線程環境下調用Feign客戶端OssFileClient時出現的HardCodedTarget異常。異常發生在異步保存文件到ES時,Feign調用未返回預期結果而直接打印了客戶端對象。問題分析指出可能原因:1)Feign調用失敗但未被捕獲;2)異步線程缺少Spring上下文導致認證失敗。解決方案包括:1)配置支持上下文傳播的線程池TaskDecorator;2)手動傳遞請求和Security上下文到異步線程。文中提供了ThreadPoolConfig配置類和ContextCopyingTaskDecorator實現,確保主線程上下文能正確傳遞到異步任務中。
前言
1,異常場景如下,文件上傳使用多線程調用微服務OssFile異步保存文件,日志報多線程之HardCodedTarget(type=OssFileClient, name=file, url=http://file)異常
原代碼如下
1,主業務代碼
@Override@Transactional(rollbackFor = Exception.class)public Long inserTemergencyProcessingMessage(TemergencyProcessingDto dto) {// 1.獲取當前登錄人的信息// 2.緊急處理演練文檔保存...... // 5.異步添加索引到ESCompletableFuture.runAsync(() -> {log.info("緊急處理演練文檔開始異步:{} , {}",temergencyPlanEntity , flIds);temergencyProcessingEsService.indextemergencyProcessing(temergencyPlanEntity, flIds);}, threadCustomPoolExecutor);return planEntity.getId();}
保存ES的代碼如下
@Overridepublic void indextemergencyProcessing(TemergencyPlanEntity planEntity, Set<Long> flIds) {try {// ... 業務代碼查詢文件信息 // 3.構建ES文檔TemergencyPlanEsDocument esDocument = buildEsDocument(planEntity, fileListEntities, fileContentsMap);// 4.索引到ES// 新增文檔 - 請求對象IndexRequest indexRequest = new IndexRequest("temergencn_plans").id(planEntity.getId().toString());// 添加文檔數據,數據轉換為Json...} catch (IOException e) {log.error("...的ES失敗,...的id是:{} ", planEntity.getId(), e);}}
2,錯誤日志如下
… … fiIds:[54]2025-08-26 18:52:52.905 [pool-2-thread-1] INFO com.xx.xxxefileapi.service.impl.TemergencyProcessingEsService - smartFileClient 獲取的數據是:HardCodedTarget(type=OssFileClient, name=file, url=http://file)
2025-08-26 18:52:52.910 [http-nio-16710-exec-3] INFO
3,問題排查思路
1,發現異步線程中調用了smartOssFileClient.queryFileListByIds(flIds),但是在日志中并沒有打印出調用該Feign客戶端后的結果(即沒有打印fileListEntitiesr 返回的數據是:),而是直接打印了ossFileClient對象,顯示為HardCodedTarget(type=OssFileClient, name=file, url=http://file)。
1.1 Feign客戶端調用失敗,但是沒有異常捕獲,但是查看代碼,在indextemergencyProcessing方法中捕獲的是IOException,而Feign調用可能拋出的是FeignException,屬于RuntimeException,所以沒有被捕獲,但奇怪的是也沒有看到異常日志。
1.2 線程上下文問題:Feign調用通常依賴于Spring的上下文(如請求攔截器、負載均衡等),而在異步線程中,可能無法獲取到正確的上下文,導致Feign調用失敗。
但是,從日志中看到,在異步線程中打印了ossFileClient對象,說明該對象不是null,而且Feign客戶端已經正常創建。
另外,注意到在異步線程打印日志的同時,主線程(http-nio-16710-exec-3)打印了AuthInterceptor的后置處理日志。這提示我們可能異步線程中缺少了某些上下文,例如安全上下文、請求頭等,導致Feign調用時沒有正確的認證信息。
解決方案:
1,確保Feign調用能夠傳遞必要的請求頭(如認證信息)。可以使用Feign的攔截器,或者自定義請求攔截器,在異步線程中手動設置請求頭。
2,檢查異步線程的線程池配置,是否支持上下文傳播。如果你使用的是Spring Boot 3.x,可以考慮使用Spring Boot的異步支持并配置任務裝飾器(TaskDecorator)來傳遞上下文。如果是較低版本,可以考慮使用其他方式(如InheritableThreadLocal)或者手動傳遞上下文。
4,手動傳遞上下文到異步線程
1,配置線程支持上下文傳遞
如果使用自定義線程池(threadCustomPoolExecutor),需確保其支持上下文傳播。推薦使用 TaskDecorator:
import lombok.Data;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/*** @author psd*/
@Data
@Configuration
public class ThreadPoolConfig {@Bean("threadCustomPoolExecutorAsync")public ThreadPoolTaskExecutor threadCustomPoolExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(coreSize);executor.setMaxPoolSize(maxSize);executor.setQueueCapacity(blockQueueSize);executor.setTaskDecorator(new ContextCopyingTaskDecorator());executor.setThreadNamePrefix("Async-Executor-");executor.initialize();return executor;}public static class ContextCopyingTaskDecorator implements TaskDecorator {@Overridepublic @NotNull Runnable decorate(@NotNull Runnable runnable) {// 捕獲主線程上下文RequestAttributes requestContext = RequestContextHolder.currentRequestAttributes();SecurityContext securityContext = SecurityContextHolder.getContext();return () -> {try {// 將主線程上下文設置到異步線程RequestContextHolder.setRequestAttributes(requestContext, true);SecurityContextHolder.setContext(securityContext);runnable.run();} finally {// 清理異步線程上下文RequestContextHolder.resetRequestAttributes();SecurityContextHolder.clearContext();}};}}
}
2,手動傳遞上下文到異步線程
@Resource
private ThreadPoolExecutor threadCustomPoolExecutor;@Override
@Transactional(rollbackFor = Exception.class)
public Long inserTemergencyProcessingMessage(ContingencyPlanDto dto) {// ... [原有代碼] ...// 捕獲當前請求上下文和安全上下文RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();SecurityContext securityContext = SecurityContextHolder.getContext();// 5.異步添加索引到ESCompletableFuture.runAsync(() -> {try {// 恢復上下文到異步線程RequestContextHolder.setRequestAttributes(requestAttributes);SecurityContextHolder.setContext(securityContext);log.info("應急預案開始異步:{} , {}", planEntity, flIds);temergencyProcessingEsService.indextemergencyProcessing(temergencyPlanEntity, flIds);} finally {// 清理上下文避免內存泄漏RequestContextHolder.resetRequestAttributes();SecurityContextHolder.clearContext();}}, threadCustomPoolExecutor);return planEntity.getId();
}
最終代碼調試到重點:
1,手動傳遞并恢復上下文(RequestContext + SecurityContext)
2,配置線程池的TaskDecorator確保上下文傳播。
3,確認Feign攔截器正確配置用于 token 傳遞。
喜歡我的文章記得點個在看,或者點贊,持續更新中ing…