Docker部署Spring WebClient遇java.net.ConnectException连接拒绝求助
Spring WebFlux + CompletableFuture 部署Docker后连接外部API失败问题
本地运行正常,Docker部署后调用外部API抛出连接拒绝异常,但容器内用curl可正常访问外部服务。
错误日志
2024-02-19T14:23:00.328Z ERROR 1 --- [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.RuntimeException: completableFuture completionException occured] with root cause 2024-02-19 23:23:00 2024-02-19 23:23:00 java.net.ConnectException: finishConnect(..) failed: Connection refused 2024-02-19 23:23:00 at io.netty.channel.unix.Errors.newConnectException0(Errors.java:166) ~[netty-transport-native-unix-common-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.channel.unix.Errors.handleConnectErrno(Errors.java:131) ~[netty-transport-native-unix-common-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.channel.unix.Socket.finishConnect(Socket.java:359) ~[netty-transport-native-unix-common-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.doFinishConnect(AbstractEpollChannel.java:711) ~[netty-transport-classes-epoll-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.finishConnect(AbstractEpollChannel.java:688) ~[netty-transport-classes-epoll-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.channel.epoll.AbstractEpollChannel$AbstractEpollUnsafe.epollOutReady(AbstractEpollChannel.java:567) ~[netty-transport-classes-epoll-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:499) ~[netty-transport-classes-epoll-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:407) ~[netty-transport-classes-epoll-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.104.Final.jar!/:4.1.104.Final] 2024-02-19 23:23:00 at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]
触发错误的核心代码
public List<List<BaseResponseItem>> concurrentPreGetFromApi(Address address) { // Create CompletableFutures for each API call CompletableFuture<List<BaseResponseItem>> baseFuture = CompletableFuture.supplyAsync(() -> baseApiService.fetchAllBaseData(address)); // Execute the other three tasks in the background after baseFuture is complete CompletableFuture<Void> sideEffectTasks = baseFuture.thenRunAsync(() -> { log.info("running start sideEffectTasks"); log.info("running start sideEffectTasks exposInfo"); CompletableFuture.supplyAsync(() -> { try { exposedInfoApiService.fetchAllExposedInfoData(address); } catch (Exception e) { log.error("Error in fetchAllExposedInfoData", e); } return null; }); log.info("running start sideEffectTasks recaptitle"); CompletableFuture.supplyAsync(() -> { recapTitleService.fetchAllRecapTitleData(address); return null; // dummy result }); CompletableFuture.supplyAsync(() -> { titleApiService.fetchAllTitleData(address); return null; // dummy result }); }); try { // Retrieve the results from the completed CompletableFutures List<BaseResponseItem> baseResponseItemList = baseFuture.join(); // Your business logic with the retrieved lists List<BaseResponseItem> baseResponseTitleItemList = baseApiService.fetchAllTitleBaseData(baseResponseItemList); List<BaseResponseItem> baseResponseExposItemList = baseApiService.fetchAllExposInfoBaseData(baseResponseItemList); if(baseResponseTitleItemList.isEmpty() && baseResponseExposItemList.isEmpty()) { return new LinkedList<>(List.of(baseResponseItemList)); } return new LinkedList<>(List.of(baseResponseTitleItemList,baseResponseExposItemList)); } catch (CompletionException e) { throw new RuntimeException("completableFuture completionException occured",e); } }
API调用逻辑示例(各CompletableFuture执行逻辑类似)
@Cacheable(value= "fetchAllBaseData") public List<BaseResponseItem> fetchAllBaseData(Address address) { // Check if the data is already in the cache Cache.ValueWrapper valueWrapper = Objects.requireNonNull(cacheManager.getCache("fetchAllBaseData")).get(address); // Use the address as a key for the lock ReentrantLock fetchLock = addressLocks.computeIfAbsent(address, k -> new ReentrantLock()); // Acquire the lock fetchLock.lock(); try { // Double-check if the value is inside the cache if (valueWrapper != null) { // Data is updated in cache return (List<BaseResponseItem>) valueWrapper.get(); } // Fetch data from the API return fetchDataFromApi(address); } finally { // Release the lock fetchLock.unlock(); addressLocks.remove(address, fetchLock); } } public List<BaseResponseItem> fetchDataFromApi(Address address) { int total = totalPages(address); log.warn("totalPages : {}", total); return Flux.range(1, total) .flatMap(page -> { log.info("currpage {}", page); return fetchPageData(address, page); }, CONCURRENCYLIMIT) // Fetch data for each page .collectList() .block(); // Block and wait for the result } private Flux<BaseResponseItem> fetchPageData(Address address, int pageNo) { WebClient.RequestHeadersSpec<?> request = apiService.getRequestHeadersSpec(address, BaseApiServiceAsync.URI, pageNo); Mono<BaseApiResponse> apiResponseMono = request.retrieve().bodyToMono(BaseApiResponse.class); return apiResponseMono // .doOnNext(apiResponse -> log.info("Received API response: {}", apiResponse)) .flatMapMany(apiResponse -> Flux.fromIterable(apiResponse.getResponse().getBody().getItems().getItem())) .doOnError(error -> { log.error("Error fetching BASE data for page {}", pageNo, error); if (error instanceof WebClientResponseException) { String responseBody = ((WebClientResponseException) error).getResponseBodyAsString(); String errorCode = ((WebClientResponseException) error).getStatusCode().toString(); log.error("Response body: {} . errorcode: {}", responseBody, errorCode); } }).retryWhen(Retry.backoff(3, Duration.ofSeconds(5)) .filter(throwable -> throwable instanceof WebClientException) ); } private Integer totalPages(Address address) { WebClient.RequestHeadersSpec<?> request = apiService.getRequestHeadersSpec(address, URI, 1); String responseString = request.retrieve().bodyToMono(String.class).block(); log.info("totalPages at {}, ", responseString); BaseApiResponse apiResponse = request.retrieve().bodyToMono(BaseApiResponse.class).block(); assert apiResponse != null; int totalCount = apiResponse.getResponse().getBody().getTotalCount(); int repeats = (int) Math.ceil((double) totalCount / WebClientApiService.SAFE_QUERY); log.warn("totalcount {}, repeats : {}", totalCount, repeats); return repeats; }
Docker配置
Dockerfile
# Stage 1: Build JRE FROM eclipse-temurin:21 as jre-build #RUN $JAVA_HOME/bin/jlink \ # --add-modules ALL-MODULE-PATH \ # --strip-debug \ # --no-man-pages \ # --no-header-files \ # --compress=2 \ # --output /javaruntime # Stage 2: Final Image FROM ubuntu:latest # Install required packages in a single RUN command RUN apt-get update \ && apt-get upgrade -y \ && DEBIAN_FRONTEND=noninteractive apt-get install -y curl \ && apt-get clean && apt-get autoclean && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* # Set Java environment variables ENV JAVA_HOME=/opt/java/openjdk ENV PATH "${JAVA_HOME}/bin:${PATH}" # Copy JRE from the build stage # COPY --from=jre-build /javaruntime $JAVA_HOME COPY --from=jre-build /opt/java/openjdk $JAVA_HOME # Create app directory RUN mkdir /opt/app # Copy the JAR file to the app directory ARG JAR_FILE=*.jar COPY ${JAR_FILE} /opt/app/firefacilauto.jar # Copy entrypoint script COPY entrypoint.sh /entrypoint.sh # Give execute permission to the entrypoint script RUN chmod +x /entrypoint.sh # Set entry point for the application ENTRYPOINT ["/entrypoint.sh"] # Expose ports EXPOSE 8080 80 # Add metadata to the image LABEL authors="user"
entrypoint.sh
#!/bin/bash echo "nameserver 8.8.8.8" >> /etc/resolv.conf exec java -jar /opt/app/firefacilauto.jar
容器网络信息
"Networks": { "RedisForSpringBoot": { "IPAMConfig": null, "Links": null, "Aliases": [ "78c9aa26a371" ], "MacAddress": "02:42:ac:12:00:03", "NetworkID": "17bc7512075a0643987824388acb2da06e0b81fe18f5614df3f5f4c24fe22167", "EndpointID": "a9d6d250881aa521379a9c66a5bee1596daf5b27480313a4bd1f2eb30e661803", "Gateway": "172.18.0.1", "IPAddress": "172.18.0.3", "IPPrefixLen": 16, "IPv6Gateway": "", "GlobalIPv6Address": "", "GlobalIPv6PrefixLen": 0, "DriverOpts": null, "DNSNames": [ "firefacilauto", "78c9aa26a371" ] } }
已尝试的排查步骤
- 注释Dockerfile部分内容重新构建镜像
- 容器内用curl测试外部服务连通性,确认网络可达
- 开启netty.io调试日志(日志量过大,可按需提供)
关键疑点
容器内curl可正常访问外部API,但WebClient抛出连接拒绝异常,未配置代理,不确定是否存在隐式代理问题。
内容的提问来源于stack exchange,提问作者김승현
相关产品推荐
相关产品推荐

