多线程之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…