多线程之HardCodedTarget(type=OssFileClient, name=file, url=http://file)异常

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

相关推荐
WhoisXMLAPI5 小时前
WhoisXML API再次荣登2025年美国Inc. 5000快速成长企业榜单
网络·安全
阿sir1986 小时前
ZYNQ 自定义IP
服务器·网络·tcp/ip
星马梦缘7 小时前
计算机网络4 第四章 网络层——网络间的通信问题(省际之间如何规划信件运输路线)
网络·计算机网络·路由·ip地址·子网掩码·icmp·ipv4/ipv6
hsjkdhs8 小时前
万字详解网络编程之socket
网络·tcp/ip·udp·socket
想睡hhh10 小时前
HTTPS协议——对于HTTP的协议的加密
http·https
@是你太难忘10 小时前
计算机网络IP协议
网络·tcp/ip·计算机网络
attitude.x11 小时前
Swift 协议扩展与泛型:构建灵活、可维护的代码的艺术
运维·服务器·网络
RTC老炮12 小时前
webrtc弱网-LossBasedBandwidthEstimation类源码分析与算法原理
网络·算法·webrtc
想成为大佬的每一天12 小时前
Linux:NTP服务
linux·网络