
1. 问题背景与核心症结拆解1.1 这个报错到底在说什么做过Java组件发布的人大概率都遇到过这个场景本地mvn release:perform跑得好好的编译、测试、打包全部通过结果到了最后一步往中央仓库推送构件的时候控制台突然甩出一行红字——SSL peer shut down incorrectly。整个构建流程戛然而止release标签已经打上了版本号也提交了但构件就是没上去留下一个尴尬的中间状态。这个报错从字面意思理解就是“SSL对端异常关闭了连接”。注意关键词是“incorrectly”也就是非正常关闭。正常的SSL挥手关闭是有close_notify通知的而对端直接断开了TCP连接没有走完TLS的关闭流程。在Maven发布构件到中央仓库的场景下这意味着你本地和远端仓库之间的HTTPS通道在传输过程中被中断了。很多人第一反应是“是不是网络问题”然后反复重试有时候重试能成功有时候一直失败。这种间歇性的表现恰恰说明问题不是简单的“通或不通”而是涉及TLS握手参数、传输数据量、超时设置、JDK版本等多个变量的综合作用。我见过不少团队在这个问题上卡了大半天最后靠换网络环境“碰运气”解决但其实根因是可以定位和修复的。1.2 为什么这个问题值得单独拿出来讲发布构件到中央仓库是开源Java项目维护者的高频操作但Maven的release流程本身涉及SCM操作、GPG签名、构件上传等多个环节任何一个环节出问题都会导致发布失败。而SSL peer shut down incorrectly这个报错恰好处于一个模糊地带——它既可能是网络层面的问题也可能是JVM参数配置的问题还可能是构件体积过大导致的传输超时。更麻烦的是这个报错的堆栈信息往往不够详细Maven默认的输出只会告诉你“SSL peer shut down incorrectly”不会告诉你具体是在哪个阶段断的、传输了多少数据、TLS握手用的是什么版本。这就导致排查方向很容易跑偏。我自己在维护几个开源组件的过程中前后遇到过三四次这个报错每次的具体原因都不太一样积累了一些排查经验这里系统性地整理一下。1.3 适用读者与前置知识这篇文章主要面向有Maven使用经验、需要将构件发布到中央仓库的Java开发者。如果你还没配置过distributionManagement、settings.xml中的server认证、GPG签名这些基础环节建议先把发布流程跑通再来看这个问题。文章会涉及一些TLS协议和JVM网络参数的知识但不需要你精通密码学只要能理解HTTPS的基本工作原理就够了。下面我会从根因分析入手然后给出完整的排查步骤和修复方案最后附上一些我在实际发布中总结的避坑经验。整个思路是“先定位再修复”而不是盲目重试。2. 根因分析为什么SSL连接会被异常关闭2.1 TLS握手阶段的常见断点要理解这个报错先要搞清楚Maven发布构件时的网络交互过程。简单来说mvn release:perform会调用maven-deploy-plugin后者通过HTTPS协议将构件jar、pom、sources、javadoc等上传到远端仓库。这个过程中涉及两次关键的TLS交互一次是建立连接时的握手另一次是数据传输完成后的关闭。握手阶段出问题通常表现为SSLHandshakeException而不是SSL peer shut down incorrectly。所以当你看到后者时大概率是握手已经完成了问题出在数据传输或连接关闭阶段。但这也不是绝对的因为有些代理或负载均衡设备会在握手过程中直接断开连接表现出来的错误信息可能被JVM包装成不同的异常类型。我在排查时习惯先用一个简单的curl命令测试目标仓库的连通性curl -v --tlsv1.2 -X GET https://oss.sonatype.org/ 21 | head -50如果curl能正常完成TLS握手并返回响应说明基础的网络通道没问题问题更可能出在Maven的JVM参数或构件传输环节。如果curl也报类似的SSL错误那就需要检查本地的JDK版本、系统代理设置、防火墙规则等。2.2 构件体积与传输超时的关系这是最容易被忽略的一个因素。中央仓库的构件上传接口对单次请求的传输时间是有隐性限制的虽然官方文档不会明确写“超过XX秒就断开”但实际测试下来如果一个构件包体积过大比如超过几十MB的fat jar或者sources/javadoc包特别大传输时间就会显著增加。当传输时间超过服务端或中间网络设备的超时阈值时对端会直接关闭连接此时JVM抛出的就是SSL peer shut down incorrectly。这种情况下重试可能会成功因为网络状况好的时候传输速度快能在超时前完成。但如果构件体积确实很大重试的成功率就很低。我遇到过一次典型的案例一个项目的javadoc包因为包含了大量生成的HTML文件体积达到了80多MB每次上传到一半就断。后来通过配置maven-javadoc-plugin排除了一些不需要生成文档的包把体积压到20MB以内问题就消失了。2.3 JDK版本与TLS协议兼容性不同JDK版本对TLS协议的支持是有差异的。JDK 8早期版本默认启用TLS 1.0/1.1/1.2而较新的JDK版本可能默认禁用了TLS 1.0和1.1。如果远端仓库的负载均衡设备对TLS版本有特定要求就可能出现协商失败或连接被异常关闭的情况。另外JDK 8u261之后的版本对TLS的默认配置做了一些调整包括禁用了部分弱加密套件。这些调整在大多数情况下是好事但在某些网络环境下可能导致兼容性问题。我一般会建议在发布构件时显式指定TLS版本export MAVEN_OPTS-Dhttps.protocolsTLSv1.2 -Djavax.net.ssl.trustStoreTypeJKS这里显式指定TLSv1.2是为了避免JVM自动协商到服务端不支持的版本。trustStoreType指定为JKS是为了兼容一些老旧的证书存储格式虽然现在很多环境已经默认用PKCS12了但显式指定可以避免一些奇怪的问题。2.4 网络中间设备的干扰企业内网环境往往有防火墙、代理、DDoS防护设备等中间层。这些设备可能会对HTTPS流量进行深度检测在检测到大量数据传输时主动断开连接。这种情况下SSL peer shut down incorrectly的“peer”其实不是远端仓库而是中间设备。判断方法很简单用手机热点或者其他非企业网络环境试一次。如果换了网络就能成功基本可以确认是中间设备的问题。这种情况下的解决方案要么是让运维加白名单要么是换网络环境发布。我一般建议在发布构件时尽量用干净的网络环境避免中间设备的干扰。3. 完整排查流程与修复方案3.1 第一步确认报错发生的具体阶段Maven的默认输出不够详细第一步是开启调试模式获取更多信息mvn -X release:perform 21 | tee release-debug.log在debug日志中搜索SSL、upload、Transfer等关键词定位到具体是哪个构件、在传输了多少字节之后断开的。如果日志显示某个jar包传输到一半就断了那基本可以确认是传输超时或体积问题。如果是在连接建立阶段就断了那更可能是TLS协商或网络策略问题。另外可以配合-Dmaven.wagon.http.verbosetrue参数让wagonMaven的传输组件输出更详细的HTTP交互日志mvn -Dmaven.wagon.http.verbosetrue release:perform这个参数会打印出HTTP请求头、响应码、重定向等信息对于判断问题出在哪个环节非常有帮助。3.2 第二步调整JVM网络参数确认了问题阶段之后可以针对性地调整JVM参数。以下是我常用的参数组合export MAVEN_OPTS-Xmx1024m -Xms512m \ -Dhttps.protocolsTLSv1.2 \ -Dmaven.wagon.httpconnectionManager.ttlSeconds120 \ -Dmaven.wagon.http.retryHandler.count3 \ -Dmaven.wagon.rto60000逐个解释这些参数的作用-Xmx1024m -Xms512m给Maven进程足够的内存避免因为GC导致的传输暂停。构件上传时如果JVM在频繁GC传输线程可能被阻塞进而触发超时。-Dhttps.protocolsTLSv1.2强制使用TLS 1.2避免协商到不兼容的版本。-Dmaven.wagon.httpconnectionManager.ttlSeconds120设置连接池中连接的存活时间为120秒避免复用已经失效的连接。-Dmaven.wagon.http.retryHandler.count3设置重试次数为3次遇到临时性网络抖动时自动重试。-Dmaven.wagon.rto60000设置读取超时为60秒给大文件传输留出足够的时间。这些参数不是万能的但能覆盖大部分因为超时和连接复用导致的问题。我实测下来加上这些参数之后间歇性的SSL报错频率明显降低。3.3 第三步优化构件体积如果确认是构件体积过大导致的问题可以从以下几个方面优化排除不必要的文件。在pom.xml中配置maven-jar-plugin和maven-source-plugin排除测试资源、临时文件等plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-jar-plugin/artifactId version3.3.0/version configuration excludes exclude**/*.properties/exclude exclude**/test/**/exclude /excludes /configuration /plugin精简javadoc内容。javadoc包往往是体积最大的可以通过配置只生成public API的文档plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-javadoc-plugin/artifactId version3.6.0/version configuration showpublic/show nohelptrue/nohelp quiettrue/quiet /configuration /plugin检查是否有大体积的依赖被意外打包。有时候项目里引入了某个包含大量资源的依赖打包时被一起打进了jar包。可以用mvn dependency:tree检查依赖树用jar tf target/xxx.jar | sort -k5 -n -r | head -20查看jar包中体积最大的文件。3.4 第四步更换传输协议或仓库端点如果以上方法都试过了还是不行可以尝试更换仓库的端点地址。中央仓库的发布端点有多个有时候某个端点的网络状况不好换一个就能成功。在pom.xml的distributionManagement中distributionManagement repository idossrh/id urlhttps://oss.sonatype.org/service/local/staging/deploy/maven2//url /repository /distributionManagement可以尝试将oss.sonatype.org替换为其他可用的端点。不过要注意不同端点可能对应不同的staging profile替换后需要确认构件是否进入了正确的staging仓库。另外如果HTTPS一直不稳定可以尝试在settings.xml中配置HTTP端点如果仓库支持的话。但出于安全考虑现在大多数仓库都强制要求HTTPS所以这个方案的实际可行性有限。3.5 第五步分步执行release流程mvn release:perform是一个复合目标它包含了checkout、build、deploy等多个步骤。如果整体执行容易失败可以拆分成多步执行降低单次操作的复杂度# 第一步准备release mvn release:prepare -Dresumefalse # 第二步单独执行deploy mvn release:perform -Dgoalsdeploy -Darguments-DskipTests通过-Dgoalsdeploy可以只执行部署步骤跳过前面的checkout和build。这样如果部署失败不需要重新走一遍完整的release流程节省时间。同时-Darguments-DskipTests可以跳过测试减少构建时间降低因为构建时间过长导致的连接超时风险。注意拆分执行时release标签和版本号已经在release:prepare阶段提交了如果release:perform失败需要手动清理标签和版本号或者用mvn release:rollback回滚。这个操作要谨慎避免把已经发布的版本号搞乱。4. 常见问题速查与避坑经验4.1 常见问题速查表问题现象可能原因排查方法解决方案每次都在同一个构件上报错该构件体积过大查看debug日志中传输字节数精简构件内容或分步上传间歇性报错重试有时成功网络抖动或超时用curl测试连通性增加重试次数和超时时间换网络环境后成功中间设备干扰对比不同网络下的表现更换网络或联系运维加白名单所有构件都报错TLS协商失败检查JDK版本和TLS配置显式指定TLS版本和加密套件报错后staging仓库有残留部分构件已上传登录仓库管理界面查看清理staging仓库后重新发布4.2 避坑经验一不要盲目重试很多人遇到这个报错的第一反应是反复执行mvn release:perform但每次重试都会在staging仓库中产生新的残留构件。如果重试次数多了staging仓库会变得很乱后续清理起来非常麻烦。正确的做法是先定位原因修复后再重新发布。如果确实需要重试建议先用mvn release:clean清理本地的release配置然后重新走完整的release流程。4.3 避坑经验二注意GPG签名的超时问题GPG签名本身不涉及网络传输但如果签名过程耗时过长比如密钥服务器响应慢会导致整个release流程超时。我遇到过因为GPG密钥服务器连接超时导致后续的构件上传也被中断的情况。解决方案是在settings.xml中配置GPG的keyname和passphrase避免每次签名时都去密钥服务器查询profiles profile idgpg/id properties gpg.keyname你的密钥ID/gpg.keyname gpg.passphrase你的密钥密码/gpg.passphrase /properties /profile /profiles4.4 避坑经验三检查settings.xml中的镜像配置有时候settings.xml中配置的镜像mirror会拦截所有请求包括发布请求。如果镜像配置了mirrorOf*/mirrorOf那么发布请求也会被转发到镜像地址而镜像地址可能不支持发布操作导致连接被异常关闭。检查方法是在settings.xml中确认镜像的mirrorOf是否包含了发布仓库的IDmirror idnexus-mirror/id mirrorOfcentral/mirrorOf urlhttps://your-nexus-repo/repository/maven-public//url /mirror这里mirrorOf只写了central表示只拦截中央仓库的下载请求不会拦截发布请求。如果写成了*就需要改成具体的仓库ID或者用external:*排除发布仓库。4.5 避坑经验四关注JDK的小版本号JDK 8的各个小版本之间在TLS行为上有细微差异。比如8u191之前的版本默认不启用TLS 1.3而8u261之后又对TLS 1.0/1.1做了限制。如果你的项目必须用某个特定的JDK小版本建议在CI/CD环境中固定这个版本避免因为环境差异导致发布失败。我一般会在项目的.mvn/jvm.config中显式指定TLS参数确保不同环境下行为一致-Dhttps.protocolsTLSv1.2 -Djdk.tls.client.protocolsTLSv1.24.6 避坑经验五用staging仓库的close操作验证构件上传完成后需要在staging仓库中执行close操作触发仓库的校验规则。如果构件上传不完整close操作会失败并给出具体的错误信息。所以如果你不确定构件是否完整上传了可以先尝试close根据报错信息判断哪些构件缺失或损坏。这个操作比反复重试发布要高效得多。mvn nexus-staging:close -DstagingDescription验证构件完整性如果close成功说明构件上传完整可以继续执行release操作。如果close失败根据报错信息补充上传缺失的构件即可。5. 长期稳定的发布环境配置建议5.1 固定构建环境发布构件最好在一个固定的环境中进行包括固定的JDK版本、固定的Maven版本、固定的网络环境。我见过太多因为环境不一致导致的发布失败案例。建议在CI/CD流水线中专门配置一个发布任务使用Docker镜像固定JDK和Maven版本避免因为本地环境差异导致的问题。FROM maven:3.9.6-eclipse-temurin-17 RUN apt-get update apt-get install -y gnupg COPY settings.xml /root/.m2/settings.xml这个Dockerfile固定了Maven 3.9.6和JDK 17并安装了GPG用于签名。在CI中执行发布任务时直接使用这个镜像可以保证每次发布的环境完全一致。5.2 配置合理的超时和重试策略在settings.xml中配置全局的wagon参数避免每次都要通过命令行传递settings profiles profile idwagon-tuning/id properties maven.wagon.rto120000/maven.wagon.rto maven.wagon.httpconnectionManager.ttlSeconds180/maven.wagon.httpconnectionManager.ttlSeconds maven.wagon.http.retryHandler.count5/maven.wagon.http.retryHandler.count /properties /profile /profiles activeProfiles activeProfilewagon-tuning/activeProfile /activeProfiles /settings这些参数会应用到所有的Maven构建中包括发布操作。超时时间设置为120秒连接存活时间180秒重试次数5次。这些值是我经过多次实践后总结出来的对于大多数项目来说够用了。5.3 监控发布过程发布构件时不要干等着可以开另一个终端监控网络连接状态watch -n 2 netstat -an | grep 443 | grep ESTABLISHED这个命令每2秒刷新一次显示当前与443端口的活跃连接。如果发现连接数突然减少说明有连接被断开了。配合iftop或nethogs可以查看实时的网络流量判断传输是否在正常进行。5.4 记录每次发布的参数和结果建议在项目的README或单独的发布文档中记录每次发布使用的JDK版本、Maven版本、关键参数和遇到的问题。这样下次发布时可以参考之前的成功经验避免重复踩坑。我自己的做法是在项目的docs/release-notes.md中记录每次发布的详细信息包括发布日期和版本号使用的JDK和Maven版本关键JVM参数遇到的问题和解决方案发布耗时这个记录看起来麻烦但长期来看非常值得。特别是当项目有多个维护者时这些记录可以帮助新人快速上手发布流程。5.5 考虑使用发布插件简化流程除了Maven原生的release插件还可以考虑使用nexus-staging-maven-plugin来简化发布流程。这个插件提供了deploy、close、release等目标可以一步到位完成构件上传和仓库关闭操作plugin groupIdorg.sonatype.plugins/groupId artifactIdnexus-staging-maven-plugin/artifactId version1.6.13/version extensionstrue/extensions configuration serverIdossrh/serverId nexusUrlhttps://oss.sonatype.org//nexusUrl autoReleaseAfterClosetrue/autoReleaseAfterClose /configuration /plugin配置了autoReleaseAfterClose为true之后构件上传完成并成功close后会自动执行release减少了手动操作步骤也降低了中间环节出错的概率。不过这个插件对网络稳定性的要求更高因为整个流程是自动化的中间没有人工干预的机会。建议在网络环境稳定的情况下使用。5.6 定期更新依赖和插件版本Maven的wagon组件和相关的HTTP客户端库在不断更新新版本通常会修复一些已知的SSL/TLS兼容性问题。建议定期检查并更新以下组件的版本maven-deploy-pluginmaven-release-pluginmaven-wagon-pluginnexus-staging-maven-plugin可以在Maven中央仓库的搜索页面查看这些插件的最新版本然后在pom.xml中显式指定版本号避免使用Maven默认绑定的老版本。我一般会在项目根pom.xml的pluginManagement中统一管理这些插件的版本确保所有模块使用一致的版本。pluginManagement plugins plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-deploy-plugin/artifactId version3.1.1/version /plugin plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-release-plugin/artifactId version3.0.1/version /plugin /plugins /pluginManagement版本号不是越新越好建议选择稳定版本避免使用刚发布的版本因为新版本可能引入未知的问题。我一般会选择发布至少三个月以上、没有严重bug报告的版本。5.7 最后的经验分享这个SSL peer shut down incorrectly报错我前前后后遇到过五六次每次的具体原因都不太一样。有一次是因为公司网络换了新的防火墙设备有一次是因为JDK从8u252升级到了8u302还有一次纯粹是因为构件包太大了。我的体会是遇到这个报错不要慌按照“先定位阶段、再调整参数、最后优化构件”的顺序来排查大部分情况下都能解决。另外发布构件这件事环境稳定性比什么都重要。与其在出问题后花大量时间排查不如提前把发布环境固定下来把参数配置好把构件体积控制住。我现在维护的几个项目都用了固定的Docker镜像来发布每次发布前先跑一遍mvn verify确认构件体积正常发布成功率比之前高了很多。如果你也在维护开源项目建议把发布流程写成脚本或者CI任务把关键参数固化下来。这样即使换了机器或者换了维护者发布流程也能保持一致减少因为环境差异导致的问题。