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

Spring WebFlux中WebClient捕获SSLException仍打印堆栈跟踪问题

WebClient捕获SSLHandshakeException后仍打印Netty堆栈跟踪的解决方案

问题场景

在Spring WebFlux响应式应用(底层基于Netty)中,使用WebClient发起请求时触发SSLHandshakeException:

javax.net.ssl.SSLHandshakeException: The server selected protocol version TLS10 is not accepted by client preferences [TLS13, TLS12]

已通过onErrorResume捕获异常并输出自定义日志,但控制台仍会打印Netty的完整堆栈跟踪。尝试修改Log4j配置、全局异常处理器均无效,且不希望启用TLS 1.0(存在安全风险)。

相关WebClient代码:

public Mono<Token> getToken(String url, String sellerId) {

    return this.webClient.post()
            .uri(url)
            .contentType(MediaType.APPLICATION_FORM_URLENCODED)
            .accept(MediaType.APPLICATION_JSON)
            .body(BodyInserters.fromFormData("grant_type", "password")
                    .with("username", "XXX")
                    .with("password", "XXXXXX"))
            .retrieve()
            .bodyToMono(Token.class)
            .onErrorResume(e -> {
                log.error("Error getting token for Seller {}. URL {}: {}", sellerId, url, e.getMessage());
                return Mono.empty();
            });

}

控制台打印的冗余堆栈(来自Netty的HttpClientConnect):

2024-06-14T09:30:43.879-03:00 ERROR 7852 --- [ctor-http-nio-8] c.a.m.requester.Requester                : Error getting token for Seller poctokin0005. URL https://goodsweet.arcordis.com.ar:14551/api/token: The server selected protocol version TLS10 is not accepted by client preferences [TLS13, TLS12]
2024-06-14T09:30:43.879-03:00  WARN 7852 --- [ctor-http-nio-5] r.netty.http.client.HttpClientConnect    : [fd0dcf4a, L:/10.133.26.164:57760 - R:proxyintegraciones.arcorgroup.com/10.140.7.125:9090] The connection observed an error

javax.net.ssl.SSLHandshakeException: The server selected protocol version TLS10 is not accepted by client preferences [TLS13, TLS12]
    at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:131) ~[na:na]
    at java.base/sun.security.ssl.Alert.createSSLException(Alert.java:117) ~[na:na]
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:365) ~[na:na]
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:321) ~[na:na]
    at java.base/sun.security.ssl.TransportContext.fatal(TransportContext.java:312) ~[na:na]
    at java.base/sun.security.ssl.ServerHello$ServerHelloConsumer.onServerHello(ServerHello.java:972) ~[na:na]
    at java.base/sun.security.ssl.ServerHello$ServerHelloConsumer.consume(ServerHello.java:894) ~[na:na]
    at java.base/sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:396) ~[na:na]
    at java.base/sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:480) ~[na:na]
    at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1277) ~[na:na]
    at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask$DelegatedAction.run(SSLEngineImpl.java:1264) ~[na:na]
    at java.base/java.security.AccessController.doPrivileged(AccessController.java:712) ~[na:na]
    at java.base/sun.security.ssl.SSLEngineImpl$DelegatedTask.run(SSLEngineImpl.java:1209) ~[na:na]
    at io.netty.handler.ssl.SslHandler.runDelegatedTasks(SslHandler.java:1651) ~[netty-handler-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.handler.ssl.SslHandler.unwrap(SslHandler.java:1497) ~[netty-handler-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.handler.ssl.SslHandler.decodeJdkCompatible(SslHandler.java:1338) ~[netty-handler-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.handler.ssl.SslHandler.decode(SslHandler.java:1387) ~[netty-handler-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:529) ~[netty-codec-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:468) ~[netty-codec-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:290) ~[netty-codec-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.handler.proxy.ProxyHandler.channelRead(ProxyHandler.java:255) ~[netty-handler-proxy-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:442) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.CombinedChannelDuplexHandler$DelegatingChannelHandlerContext.fireChannelRead(CombinedChannelDuplexHandler.java:436) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.CombinedChannelDuplexHandler.channelRead(CombinedChannelDuplexHandler.java:253) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.handler.proxy.HttpProxyHandler$HttpClientCodecWrapper.channelRead(HttpProxyHandler.java:284) ~[netty-handler-proxy-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:444) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:412) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:440) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:420) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:788) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:724) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:650) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:562) ~[netty-transport-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:997) ~[netty-common-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) ~[netty-common-4.1.105.Final.jar:4.1.105.Final]
    at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) ~[netty-common-4.1.105.Final.jar:4.1.105.Final]
    at java.base/java.lang.Thread.run(Thread.java:842) ~[na:na]

原因分析

这个冗余的堆栈跟踪是Netty的reactor.netty.http.client.HttpClientConnect类在IO线程中直接打印的WARN日志,不属于WebClient的Reactor响应式链抛出的异常,因此onErrorResume和全局异常处理器都无法拦截。

解决方法

方法1:通过日志配置屏蔽Netty的WARN日志

直接修改Log4j配置,将reactor.netty.http.client.HttpClientConnect的日志级别调整为ERROR或OFF,阻止其打印堆栈:

Log4j2.xml配置示例:

<Logger name="reactor.netty.http.client.HttpClientConnect" level="ERROR" additivity="false">
    <AppenderRef ref="Console"/>
</Logger>

application.properties配置示例:

logging.level.reactor.netty.http.client.HttpClientConnect=ERROR

方法2:自定义WebClient的Netty HttpClient配置

通过自定义Netty的SslContext或添加异常处理Handler,在Netty层面拦截SSL握手异常,避免日志输出:

@Bean
public WebClient webClient() {
    SslContext sslContext = SslContextBuilder.forClient()
            .protocols("TLSv1.2", "TLSv1.3") // 保持客户端协议偏好
            .build();

    HttpClient httpClient = HttpClient.create()
            .secure(sslSpec -> sslSpec.sslContext(sslContext))
            .doOnConnected(conn -> conn.addHandlerLast(new ChannelDuplexHandler() {
                @Override
                public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                    // 拦截SSLHandshakeException,不传递给Netty的默认日志处理器
                    if (cause instanceof SSLHandshakeException) {
                        ctx.close(); // 关闭连接
                    } else {
                        super.exceptionCaught(ctx, cause);
                    }
                }
            }));

    return WebClient.builder()
            .clientConnector(new ReactorClientHttpConnector(httpClient))
            .build();
}

说明

  • 方法1简单快捷,适合快速解决日志冗余问题;
  • 方法2更灵活,可在Netty层面自定义异常处理逻辑,适合需要对SSL异常做额外处理的场景。

内容的提问来源于stack exchange,提问作者Nicolas Zaeta

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 02:27:04