
搞Java的人尤其是搞过发布上线、接过存量项目的应该都遇到过这种尴尬场景项目早就打成可执行jar包扔到服务器上跑着了结果运维或者安全团队突然丢过来一个通告“你用的xxx库有严重漏洞必须马上升级”。这个“xxx”还不是你自己代码里的东西而是埋在你那个fat jar里lib目录下的一个第三方jar包。更要命的是原始工程可能早就都不在了git仓库翻了半天没有或者编译环境已经坏了重新构建整个项目成本高得离谱。这时候摆在面前的路只有一条直接对着线上那个jar包动手把里面的第三方依赖换掉。我第一次干这事儿的时候也慌总觉得jar包是个黑盒子怕一改就废。但实际摸过一轮之后发现只要搞懂jar包的内部结构、理清类加载的原理这件事完全可控甚至比重新构建整个工程还省事。这篇文章就把我实践过的几种方案、背后的原理、以及踩过的坑全部整理出来给碰到同样问题的朋友一个能直接照着做的参考。1. 项目内容拆解你到底改的是什么很多人一听“更新jar包内的第三方jar包”第一反应是“这不就是打开jar包把文件拖进去替换吗”。理论上没错但你要真这么干大概率会踩出一堆莫名其妙的坑。原因在于jar包不是普通的文件夹它有自己的内部约定而且不同的打包方式决定了更新方式完全不同。先明确一个核心概念jar包的本质就是一个zip压缩文件内部遵循zip格式规范。也就是说理论上你可以用任何支持zip的工具对jar包进行解压、修改、重新压缩。但jar包又不止是zip它在META-INF/MANIFEST.MF中声明了jar包自身的元数据包括主类、classpath、启动入口等信息。运行时JVM会根据这些信息决定如何加载类。你要更新的“第三方jar包”一般存在于下面两种结构中传统lib结构jar包内有一个lib/目录里面堆着所有第三方依赖MANIFEST.MF里的Class-Path指向每个jar包文件名。很多早期的手工打包项目就是这么干的。Spring Boot fat jar结构依赖在BOOT-INF/lib/下面MANIFEST.MF里声明了Main-Class和Start-Class通过PropertiesLauncher或JarLauncher动态加载内部jar包。这是目前最常见的情况。两种结构对应的更新流程基本一致解压、替换内部jar、重新打包。但细节上差得很多比如Class-Path声明要改或者压缩方式会影响Spring Boot Loader的索引。我后面会逐一说到。还有一点必须提前搞清楚你要更新的第三方jar包是fat jar里的一层而不是fat jar本身。更新的目标应该是lib/或者BOOT-INF/lib/下的那个具体文件比如log4j-core-2.14.1.jar换成log4j-core-2.17.2.jar。如果你把整个外层fat jar当成zip去改但没改对里面的依赖那等于白忙。注意动手之前先确认你要更新的jar包是否真的嵌在目标jar包内部而不是通过外部classpath或者系统变量指定的。用unzip -l或者jar tf看一眼别改了半天发现更新了个寂寞。2. 动手前必看jar包内部到底是啥结构要安全地把内部第三方jar包换掉你至少得知道jar包里有什么、类是怎么被找到的。不然你改得再好JVM启动时找不到类一样废。2.1 MANIFEST.MF 是总指挥任何可执行的jar包META-INF/MANIFEST.MF都是核心。这个文件里记录了Manifest-Version清单版本通常是1.0。Main-Classjar包的启动入口类这个类必须包含main方法。Class-Path依赖的类路径列表传统lib结构下它会显式列出所有内部jar包的文件名。Start-ClassSpring Boot里真正执行业务逻辑的启动类Main-Class固定走向Spring Boot的JarLauncher然后由它去加载Start-Class。如果Class-Path里列了lib/commons-logging.jar而你更新时把这个文件换成了lib/commons-logging-1.3.jar文件名变了但Class-Path没改JVM启动就会报ClassNotFoundException或者NoClassDefFoundError。这就是为什么很多人“替换了文件却没效果”的原因。Spring Boot的结构下不太一样Class-Path通常只指向BOOT-INF/lib/这个目录具体加载哪个jar由JarLauncher在运行时扫描目录实现。所以Spring Boot的fat jar更新依赖只要保证新jar包的文件名和包名结构正确基本不用改MANIFEST。但你如果用的是传统Class-Path模式就必须同步改清单。2.2 lib目录第三方依赖的老巢“jar包的lib中是什么”这个问题其实就是问fat jar里第三方依赖都放哪。答案很统一lib/传统方式或者BOOT-INF/lib/Spring Boot方式。里面每个jar都是独立的第三方库JVM在启动时按需加载。Spring Boot的fat jar结构还有一层特殊性BOOT-INF/classes/是你的项目业务代码编译后的class文件BOOT-INF/lib/是依赖org/springframework/boot/loader/里是Spring Boot自己的加载器类。更新jar包时只能动BOOT-INF/lib/其他目录尽量别碰。外部jar包还有一种情况就是WEB-INF/lib。如果你的应用是个war包部署到Tomcat那第三方依赖在WEB-INF/lib/下更新流程和fat jar几乎一样只是启动机制不同。Tomcat的web应用类加载器会直接扫描这个目录更新后重启即可。2.3 类加载顺序为什么替换了还可能不生效JVM加载类遵循双亲委派模型和classpath中声明的顺序。如果你有多个jar包含相同的类先被加载的会“赢”。在fat jar中旧的jar包如果没被物理删除只是新增了一个新的jar包那JVM可能还是加载旧的实现。所以“更新”必须是替换不只是增加。这个点特别容易忽略有人图省事直接把新版jar塞进去结果运行时行为还是旧的排查半天才发现是旧jar仍然存在。提示如果你同时存在commons-io-2.11.0.jar和commons-io-2.14.0.jarJVM通常按classpath顺序找到哪个用哪个。但jar包内部加载顺序受zip条目顺序影响老版本的zip工具可能把新增条目放在最后导致加载旧版。所以最好的做法是彻底移除旧jar只保留新jar。3. 方案选型三种更新方式各自适合什么场景先给结论没有通吃的方案你的操作系统、工具链、是否保留工程、有没有CI环境都会影响选择。我按优先级拆成三个方案你可以对号入座。3.1 方案一zip命令直接替换最快适合一次性的线上修复这是最原始但也最有效的方式。思路很简单把外层fat jar解压到临时目录替换掉BOOT-INF/lib下的目标jar再重新压缩成jar包。整个过程用命令行就能完成不需要写代码。以Linux或者Git Bash环境为例关键是保持zip目录结构。如果新版jar文件名不同MANIFEST.MF声明了Class-Path的话必须同步修改清单文件。3.2 方案二Java代码实现最适合Windows环境或无zip命令行工具Windows环境下的zip命令版本混乱有些场景下只装了JRE没装JDK连jar命令都没有更别提unzip和zip。但Java代码本身在任何装了Java的机器上都能跑。实现思路是用java.util.zip包读原始jar包里所有条目把不需要替换的条目原样复制到新jar需要替换的条目跳过再把新jar文件写入新jar。这个方案的好处是不依赖任何外部命令。3.3 方案三重新构建工程最正规适合有源码、时间充裕的情况如果源码、构建脚本都还在最干净的方式永远是改pom.xml或build.gradle里的版本号重新打包。这个方法不需要折腾JAR内部结构但要求你具备完整构建环境包括依赖仓库能否访问到新版jar。三种方案我真是都试过。有次线上环境只有JRE没有JDK连jar命令都没有我用方案二写了个小工具把问题解决了另一次完整工程都在直接改Maven依赖版本五分钟重新构建完成。4. 核心细节解析与实操要点重点章节这一节我会给出可复现的具体步骤和参数计算每一步都标注为什么要这么做。4.1 方案一详细步骤Linux/Mac下的zip替换法第一步解压外层jar包。建议在服务器上临时建一个工作目录比如/tmp/jarfix把目标fat jar拷贝进去解压mkdir -p /tmp/jarfix cd /tmp/jarfix cp /opt/app/myapp.jar . unzip -q myapp.jar -d extracted cd extracted这时候你会看到BOOT-INF/lib/Spring Boot或者lib/传统结构目录。看下里面哪个jar需要替换ls BOOT-INF/lib/ | grep log4j第二步备份旧jar拷贝新jar进来然后删除旧jarcd BOOT-INF/lib mv log4j-core-2.14.1.jar log4j-core-2.14.1.jar.bak cp /download/log4j-core-2.17.2.jar . rm log4j-core-2.14.1.jar.bak cd ../..这里有个细节如果新jar的文件名和旧jar不一样你得确认MANIFEST.MF里是否显式引用了旧文件名。尤其传统lib结构。Spring Boot会自动扫描目录文件名不同问题不大但工程里如果配置了Class-Path引用就必须改。改法就是编辑解压出来的META-INF/MANIFEST.MF把lib/log4j-core-2.14.1.jar替换成lib/log4j-core-2.17.2.jar注意别把路径弄丢。第三步重新打包。这一步很多人坑在压缩工具上。jar包虽然不是必须保持特定压缩算法但如果你用zip命令重新压缩整个目录文件顺序、压缩级别可能会变多数情况下没问题。但如果用了jar命令它默认会在压缩时添加META-INF目录你要小心别重复生成MANIFESTcd extracted zip -r ../myapp-new.jar .重新打包出的myapp-new.jar就是更新后的jar包。注意这里的zip -r会从当前目录开始把整个extracted内容打进新jar。如果你在新jar里保留了旧的jar文件运行时可能加载到老的类我建议目录里只保留一个版本。第四步验证unzip -l ../myapp-new.jar | grep log4j-core java -jar ../myapp-new.jar注意很多人会问重新用zip压缩会不会改变jar包的可执行性不会。jar包可执行性由MANIFEST.MF决定跟你用什么zip工具压缩没有关系。你只要保证META-INF/MANIFEST.MF存在且内容完整就行。4.2 方案二详细步骤纯Java更新程序如果你的机器上没有zip命令但装了JDK或JRE可以写一个简短的Java类来完成同样的逻辑。核心思想是读取原始jar包的zip流把除了要替换的文件以外的所有条目写进新jar再把新jar文件作为额外条目写入。import java.io.*; import java.nio.file.*; import java.util.zip.*; public class JarInnerUpdater { private static final String TARGET_JAR path/to/old-lib.jar; private static final String REPLACEMENT_JAR path/to/new-lib.jar; public static void main(String[] args) throws IOException { Path original Paths.get(myapp.jar); Path updated Paths.get(myapp-updated.jar); try (ZipInputStream zis new ZipInputStream( Files.newInputStream(original)); ZipOutputStream zos new ZipOutputStream( Files.newOutputStream(updated))) { ZipEntry entry; while ((entry zis.getNextEntry()) ! null) { if (entry.getName().equals(TARGET_JAR)) { // skip the old inner jar continue; } // copy the entry as-is zos.putNextEntry(new ZipEntry(entry.getName())); byte[] buffer new byte[8192]; int len; while ((len zis.read(buffer)) 0) { zos.write(buffer, 0, len); } zos.closeEntry(); } // add the new inner jar zos.putNextEntry(new ZipEntry(REPLACEMENT_JAR)); Files.copy(Paths.get(REPLACEMENT_JAR), zos); zos.closeEntry(); } System.out.println(Done.); } }这段代码里有一个关键设计ZipInputStream按迭代顺序读取所有zip条目原样复制复制的过程中不但复制了条目内容也重建了zip目录结构。唯一的例外是TARGET_JAR这个条目被跳过然后新jar作为新条目写入。这里的REPLACEMENT_JAR的路径必须和原始jar内的路径保持一致比如原文件是BOOT-INF/lib/log4j-core-2.14.1.jar那么新jar的entry name也应该是这个路径。编译和运行javac JarInnerUpdater.java java JarInnerUpdater程序跑完后会生成myapp-updated.jar。这个方法干净、可移植而且不需要JDK自带的jar命令就能执行。避坑点使用ZipOutputStream写入时默认的压缩级别是DEFLATED与原始jar包可能一致但如果你要对已经压缩过的嵌套jar再压缩会有一定CPU开销但不影响功能。另一个坑是ZipInputStream处理非压缩条目STORED时可能丢失CRC信息如果你发现新jar里某些条目损坏可以在putNextEntry之前显式设置entry.setMethod(ZipEntry.DEFLATED)。4.3 方案三详细步骤Maven/Gradle重新打包前面两个方案都属于“外科手术”但如果你手头有完整工程和构建环境正路是改依赖声明重新打包。Maven场景下修改pom.xml中对应依赖的version字段dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.17.2/version /dependency然后执行mvn clean package生成的fat jar内部BOOT-INF/lib里自然就是新jar。Gradle场景类似修改build.gradle中的依赖版本implementation org.apache.logging.log4j:log4j-core:2.17.2然后执行gradle clean bootJar。如果你的项目用了Spring Boot插件且依赖了外部的第三方jar包比如通过implementation files(lib/xxx.jar)引入的本地jar重新打包后这个jar也会被包含进去。这个方案隐藏的风险是依赖传递。改了一个库的版本可能会拉进来新的传递依赖版本导致其他库冲突。最典型的例子就是你升级Spring Security结果Spring Core也被迫升级然后你的代码里用到的某个类被移除了。所以改完版本后最好跑一遍mvn dependency:tree看依赖树变化。4.4 不同方案的对比与决策我把三个方案放在一张表里方便你自己判断方案适用场景优点缺点风险点zip命令替换Linux服务器、快速热修操作简单无需编程需要unzip/zip命令文件名变更时需改MANIFEST压缩顺序、文件残留导致旧类被加载Java程序替换任何有Java环境的机器跨平台逻辑可控可复现需要写代码编译运行多一步zip条目元数据丢失需手动补偿Maven/Gradle重打包有源码、有构建环境正规不用碰jar内部可能引发依赖传递冲突构建耗时依赖树不一致导致运行时异常我个人的建议是如果是线上紧急修复直接用方案一快如果跨平台需求或有自动化集成需求方案二最适合写进脚本或工具链如果时间允许、项目结构完整方案三最保险。5. 实操过程与核心环节实现这一节我以一次真实的log4j安全漏洞修复为例完整走一遍从备份到验证的全过程把核心环节的操作细节串起来。5.1 场景设定某业务系统以Spring Boot fat jar发布在Linux服务器上jar包路径为/opt/app/payment-service.jar。安全扫描发现BOOT-INF/lib/log4j-core-2.14.1.jar存在CVE漏洞需要升级到2.17.2。原始工程在另一台机器上但构建环境已经无法恢复所以采用在线直接替换的方案。5.2 修复过程记录第一步确认环境which unzip zip java -version unzip -l /opt/app/payment-service.jar | grep log4j看到输出里有BOOT-INF/lib/log4j-core-2.14.1.jar和BOOT-INF/lib/log4j-api-2.14.1.jar确认两个都需要升级。第二步备份原始jar包cp /opt/app/payment-service.jar /opt/backup/payment-service-$(date %Y%m%d).jar备份是必须的万一新jar启动不了你要能在10秒内回滚。旧版jar包的备份是整个文件而不是只备份内部的依赖。第三步创建临时目录并解压mkdir -p /tmp/jarfix cd /tmp/jarfix cp /opt/app/payment-service.jar . unzip -q payment-service.jar -d extracted rm payment-service.jar cd extracted ls BOOT-INF/lib/ | grep log4j第四步替换jar文件cd BOOT-INF/lib cp /download/log4j-api-2.17.2.jar . cp /download/log4j-core-2.17.2.jar . rm log4j-api-2.14.1.jar log4j-core-2.14.1.jar cd ../..这里有个值得注意的技巧如果你下载的新jar文件名跟旧jar完全一致比如都叫log4j-core.jar就不需要改MANIFEST但如果版本号写进了文件名大多数Maven仓库包都这样那Spring Boot的BOOT-INF/lib扫描方式不受影响反而传统Class-Path方式需要同步改清单。Spring Boot的JarLauncher在启动时会扫描BOOT-INF/lib目录下所有jar不依赖文件名只要你删了旧jar、放了新jar加载的就是新jar。第五步重新打包zip -r /tmp/jarfix/payment-service-updated.jar .注意zip -r命令输出中会有很多adding:行还会在结尾提示zip的归档文件生成位置。完成后可以顺手用unzip -l确认unzip -l /tmp/jarfix/payment-service-updated.jar | grep log4j看到输出里只有2.17.2的文件名了说明替换成功。第六步部署验证/opt/jdk-17/bin/java -jar /tmp/jarfix/payment-service-updated.jar --server.port18081先起一个测试端口确认没有启动异常再正式替换线上文件。启动日志里看到业务类初始化成功、数据库连接正常时才算真正完成了更新。我通常还会调用一个业务接口验证关键功能无异常比如发一笔测试交易。这样做的原因在于jar包替换只能保证类存在不能保证新版库的API不兼容你的业务代码。如果有不兼容只启动成功也是不够的。5.3 关键参数与逻辑补充补充几个在上面的步骤中隐含的参数逻辑方便不同场景下你灵活调整压缩级别默认zip压缩级别为6如果你的jar包体积特别大比如100MB以上耗时可能会翻倍但最终运行不受影响。如果实在着急可以用zip -0不压缩方式快速打包但jar会变大。文件权限重新打包的jar包默认权限是当前用户umask值部署到服务器时要确认有执行权限。一般Java应用这么启动只需要读权限但保险起见还是chmod x。JVM版本兼容你替换的新jar如果是用更高版本JDK编译的class文件版本号高而线上JRE版本太低就会报UnsupportedClassVersionError。下载新jar时务必确认class文件版本不高于线上JVM版本比如log4j-core-2.17.2需要JDK8以上。5.4 关于“arm 运行jar包”和“idea引入本地jar包”的延伸热搜词里提到的“arm 运行jar包”很贴合实际。当前不少云服务器是ARM架构比如华为鲲鹏、AWS Graviton。ARM上运行Java应用JVM本身是跨架构的只要你的JDK版本支持ARM比如OpenJDK的aarch64构建jar包替换流程和x86一致。真正要注意的是有些第三方库包含native code比如gdal jar包。这类jar包内部可能封装了.so或.dll你替换时不仅要把jar放进去还必须确保native库版本和架构匹配。比如GDAL 3.10的jar包通常需要配对应的native二进制否则加载时直接报UnsatisfiedLinkError。“idea引入本地jar包”是开发阶段的另一码事。你用IDEA引入本地jar包一般通过File Project Structure Libraries Java添加。打完fat jar之后这个本地jar会被打进BOOT-INF/lib。如果你反悔想升级这个本地jar最省事的做法是在IDEA里删掉旧库、引入新库然后重新打包。如果IDEA环境也没了那就回到前面两种外科手术方案。6. 工具选型与常见问题排查这一节谈谈工具选择以及我在实际操作中踩过的坑、别人问过我的高频问题。6.1 jar命令和zip命令怎么选JDK自带的jar命令本质上也是一个zip工具但它的行为有些特殊。比如用jar uf命令更新jar包内部文件jar uf payment-service.jar -C lib new-version.jar这条命令的含义是从lib目录下把new-version.jar作为新条目添加到payment-service.jar中。听起来很方便但它的坑在于如果旧jar文件名不同jar uf只做添加不做删除旧版本jar仍然留在包里。这就像我在前面说的类加载隐患。所以我一向建议优先用“解压后替换再压缩”的方式而不要依赖jar uf的局部更新。还有一种做法是用zip -d删除旧文件再用zip添加新文件zip -d payment-service.jar BOOT-INF/lib/log4j-core-2.14.1.jar zip payment-service.jar BOOT-INF/lib/log4j-core-2.17.2.jar这种做法的好处是不用解压整个jar操作更快坏处是修改后zip中央目录的条目顺序可能被打乱而且如果jar已经有压缩条目损坏你根本发现不了。我测试过不少场景它对slim jar依赖较少、结构简单的jar效果挺好但对Spring Boot的fat jar兼容性存在风险。最稳的还是全量解压重打。6.2 常见问题排查表问题现象可能原因排查方法启动报ClassNotFoundException内部jar文件名与Class-Path不一致用unzip -p 新jar META-INF/MANIFEST.MF检查Class-Path比对lib目录文件名启动报ZipException: invalid entry CRC重新打包时损坏了条目元数据用zip -r重新压缩整个目录或改用Java程序方案设置DEFLATED启动正常但业务功能异常新版jar不兼容或旧版jar残留搜索lib目录下是否仍有旧文件检查新旧jar的API差异替换后进程还是使用旧文件应用没有完全重启或类被Java缓存加载确保旧进程彻底停止kill -9再启动不要用kill -HUP热加载出现NoSuchMethodError新版jar缺少某方法或签名变化用javap -cp 新jar查看类方法签名对比调用方代码下载的新jar无法引用仓库源配置错误或依赖版本不存在用mvn dependency:get手动拉取确认中央仓库有该版本GDAL等含native库的jar报UnsatisfiedLinkErrorjar里的native二进制与平台不匹配确认下载的是对应ARM/x86和对应操作系统的版本6.3 独家避坑技巧我做了这么多jar包替换总结出三个在实际操作中特别值得注意的细节。第一改完jar包后一定要做“独立环境烟雾测试”。别直接在正在流量生产的机器上替换至少先把新jar拷到一台测试机器或换端口启动一遍。测试内容至少要覆盖启动成功、依赖的数据库/中间件连接正常、调用一个最核心的业务接口。如果还有分布式链路记得确认注册服务时调用的健康检查接口不报错。第二注意jar包压缩后的时间戳和文件顺序会影响增量发布工具。如果你用ansible或者自研的发布系统做文件同步旧jar和新jar的mtime不一致通常能触发上传但如果只是替换了内部文件、外层jar名字没变且mtime没变发布工具可能认为文件没变化导致新包没传上去。解决办法是在替换后显式touch一下新jar文件或者把新jar重命名加上版本号。第三签名jar包是最坑的。有些第三方jar包是经过JAR Signing签名过的比如部分Oracle JDBC驱动。当你解压重打外层fat jar时如果保留了META-INF/*.SF、META-INF/*.RSA等签名文件但内容已经变了JVM在classpath下检测到签名失效会抛SecurityException。处理办法是解压后把META-INF下与旧jar签名相关的.SF、.DSA、.RSA文件删掉然后重新打包。删除后JVM会按未签名jar处理反而能正常启动。前提是你信任这个新替换进去的jar来源。签名校验本身是安全机制但update jar时它是最大的阻碍没有之一。6.4 关于“springboot引入外部jar包”的热搜词延伸很多朋友在“springboot引入外部jar包”的时候如果本地仓库或者中央仓库没有对应依赖会选择把jar手动放到项目根目录下的lib文件夹然后通过Maven配置systemPath引用。这种方式构建出来的fat jar里会有BOOT-INF/lib/xxx.jar逻辑上和Maven仓库依赖没有本质区别。但当你要升级时如果github或者Maven中央仓库已经下线了旧版本就只能先下载新jar替换本地lib目录里的旧文件重新打包。整个过程跟我前面写的方案三完全一致。更有意思的是如果系统通过lib目录方式引入的外部jar原本就不是经过Maven依赖树管理的那你替换后不会引发依赖冲突反而比改pom版本更省事。这也算是一个“引入外部jar包”方式的隐性优点。7. 我的实操总结与个人建议文章写到这儿该说的技术细节基本都覆盖了。最后分享一些我个人的习惯以及长期跟jar包打交道的体会。先说习惯。我每次做jar包替换一定会保留一份原始jar包的副本存放在独立目录文件名里带上日期。更新后不会急着删除临时目录至少保留到新版本在线上稳定运行一周以上再清理。这样如果遇到隐藏的兼容性问题随时能回滚到旧版本而不需要重新从发布服务器拉包。再说几个实际操作层面的建议如果是连锁项目或者微服务系统替换完一个服务的jar包后要关注上下游服务调用是否正常。第三方库升级可能改变序列化行为或HTTP客户端连接池参数而这些通常不会在启动日志里暴露只会在实际流量下暴露。我见过一次升级了JSON库版本后导致某个字段的序列化顺序变化下游服务基于顺序解析直接数据错乱。这类问题属于兼容性测试盲区安全升级前最好对比新旧库的release notes确认行为变化。另一个建议是学会用jdeps或者dependency:analyze做升级影响面分析。尤其当你要跳过多个版本升级时比如2.14跳到2.17甚至2.20中间API的变化可能很大。先用jdeps检查新版jar的依赖项再对照自己代码中引用的类能省不少排查时间。最后关于“更新jar包内的第三方jar包”这件事本身我想纠正一个心态这不是什么高深操作但也不是无脑替换。它本质上是在生产环境上做了一个风险较高的变更所以一定要遵循变更管理的节奏——备份、低危环境验证、灰度上线、监控确认。一步都不能省。我做过很多次这样的升级从最开始手忙脚乱改坏好几个jar到后来形成一套标准流程十分钟完成替换与验证。如果你手头正好遇到类似问题照着这篇文章的步骤走大概率能一次搞定。如果过程中遇到什么特殊情况欢迎在评论区把报错和操作步骤贴出来我看到了会回复。