多线程之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,主业务代码
c
@Override
@Transactional(rollbackFor = Exception.class)
public Long inserTemergencyProcessingMessage(TemergencyProcessingDto dto) {
// 1.获取当前登录人的信息
// 2.紧急处理演练文档保存...
...
// 5.异步添加索引到ES
CompletableFuture.runAsync(() -> {
log.info("紧急处理演练文档开始异步:{} , {}",temergencyPlanEntity , flIds);
temergencyProcessingEsService.indextemergencyProcessing(temergencyPlanEntity, flIds);
}, threadCustomPoolExecutor);
return planEntity.getId();
}
保存ES的代码如下
c
@Override
public 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:
c
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 {
@Override
public @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,手动传递上下文到异步线程
c
@Resource
private ThreadPoolExecutor threadCustomPoolExecutor;
@Override
@Transactional(rollbackFor = Exception.class)
public Long inserTemergencyProcessingMessage(ContingencyPlanDto dto) {
// ... [原有代码] ...
// 捕获当前请求上下文和安全上下文
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
SecurityContext securityContext = SecurityContextHolder.getContext();
// 5.异步添加索引到ES
CompletableFuture.runAsync(() -> {
try {
// 恢复上下文到异步线程
RequestContextHolder.setRequestAttributes(requestAttributes);
SecurityContextHolder.setContext(securityContext);
log.info("x x x开始异步:{} , {}", planEntity, flIds);
temergencyProcessingEsService.indextemergencyProcessing(temergencyPlanEntity, flIds);
} finally {
// 清理上下文避免内存泄漏
RequestContextHolder.resetRequestAttributes();
SecurityContextHolder.clearContext();
}
}, threadCustomPoolExecutor);
return planEntity.getId();
}
最终代码调试到重点:
1,手动传递并恢复上下文(RequestContext + SecurityContext)
2,配置线程池的TaskDecorator确保上下文传播。
3,确认Feign拦截器正确配置用于 token 传递。
喜欢我的文章记得点个在看,或者点赞,持续更新中ing...