You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,提问作者김승현

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 00:05:54