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
相关产品推荐
相关产品推荐

