简介:面向 Java 开发者的 Maven 实践资源,紧密围绕仓库机制、本地 JAR 引入和可执行 JAR 打包三个核心主题展开。内容先比较本地仓库、远程仓库与中央仓库的定位和协作关系,再说明如何将本地 JAR 按 groupId/artifactId/version 坐标放入本地仓库对应目录,在 pom.xml 中声明依赖后执行 mvn install 完成引用;随后以配置示例分别讲解 maven-jar-plugin、maven-assembly-plugin、maven-shade-plugin 三种打包方式,并对比普通可运行 JAR、依赖集成型 fat JAR 和可重定位资源的 shaded JAR 在类冲突、体积、启动方式上的差异,也顺带说明 manifest 中 Main-Class 的设置与依赖路径约定,方便开发者按交付场景挑选。压缩包共 6 个文件,约 5KB,文件构成以 Maven 配置 XML、Java 示例源码和 Eclipse 工程描述/类路径/偏好设置为主,呈现一个最小化 Maven 工程骨架,导入 IDE 即可对照实践。已有 2767 人学习下载,适合 Maven 初学者和 Java 后端开发者在搭建项目、处理本地二方包或制作可执行交付物时参考,有助于缩短构建配置的试错过程。 如果你去翻各大技术论坛,会发现有关 Maven 的提问长期霸占着两个"经典痛点":一个是"我明明把 jar 包放进项目里了,为什么编译还是找不到类?",另一个是"打包出来的 jar 为什么双击运行不了?"。
这两个问题看起来风马牛不相及,但本质上都指向同一个根源——你并没有真正理解 Maven 的仓库机制和构建生命周期的打包逻辑。很多人从 IDE 里"导入本地 jar"开始学 Maven,结果被 IDE 的友好界面惯坏了,浑然不知底层发生了什么。等到脱离 IDE、用命令行一跑就原形毕露。
这篇文章我打算用实际踩坑的视角,把三件事一次讲透:Maven 仓库到底是怎么运作的、如何正确引入本地包、以及用不同方式打出能直接跑起来的可执行 jar 包。内容偏实操,适合刚把 Maven 跑通、但还没真正理解构建逻辑的同学,也适合被各种打包插件搞晕的老开发。
1. 先弄清楚 Maven 仓库的三层结构:本地仓库、中央仓库、私有源的协作关系
1.1 本地仓库不是你项目的 lib 目录,而是全局依赖缓存
很多新手会混淆"本地仓库"和"项目里的 lib 文件夹",这是第一个认知误区。Maven 的本地仓库默认在用户目录下的.m2/repository里,它本质上是你这台机器上所有 Maven 项目共享的依赖缓存库。也就是说,你 A 项目下载过的依赖,B 项目如果声明了同样坐标,就会直接复用本地仓库里的那份,不会再重复下载。
实际工作中,我是强烈不建议改默认位置的,除非你的 C 盘真的吃紧。真要改,就去改settings.xml里的localRepository标签,比如这样:
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"> <localRepository>D:/maven_repo</localRepository> </settings>但你得想清楚一个后果:IDE 里配置的 Maven 如果指向不同的settings.xml,或者你命令行用的 Maven 和 IDE 内置的 Maven 版本不一致,它们各自维护的本地仓库可能就是两套。很多"换台电脑/换个人跑项目就编译报错"的诡异问题,源头就在这里——本地仓库没有随项目走,它只是缓存。
1.2 中央仓库与镜像/私服的本质关系
中央仓库(Maven Central)是全世界开发者共享的依赖源头,但它部署在海外,国内直连下载速度感人。所以你会看到国内各大云厂商都提供了镜像仓库,比如阿里云仓库地址:
<mirror> <id>aliyunmaven</id> <mirrorOf>central</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>这段配置要放在settings.xml里的<mirrors>节点下。mirrorOf写central表示只对中央仓库生效,如果你配了私服(比如 Nexus、Artifactory),mirrorOf里的值就值得仔细斟酌了:*表示所有仓库请求都走这个镜像,external:*表示只有外部仓库才走镜像、本地私服请求不受影响。如果没有私服,配*问题不大;如果有私服还配*,大概率会出现"私服上独有的包拉不下来"的问题。
这里多说一句关于私服本身的逻辑。私服存在的意义不只是内网加速,更重要的是团队内部发布的二方包(比如公司自研的公共工具库)存放地。配置私服需要在pom.xml里增加<repositories>标签:
<repositories> <repository> <id>nexus</id> <url>http://你的私服地址/repository/maven-public/</url> </repository> </repositories>注意:pom.xml里配置的仓库只对当前项目生效,而settings.xml里的镜像是对全局生效的。两者的优先级关系也常把人绕晕——镜像会拦截仓库请求,而不是替代仓库配置。我见过不少团队把<repository>写到 settings.xml 里,结果项目换到别人机器上就废了,就是没分清这两者的作用域。
1.3 依赖坐标是仓库里的"门牌号"
Maven 仓库里的每个依赖都由groupId、artifactId、version三个坐标唯一定位,缺一个都可能导致拉取失败。这三个坐标的含义很多教程都讲过,但很少有人解释它和文件的映射关系:org.example:demo:1.0.0在仓库里对应的路径是org/example/demo/1.0.0/demo-1.0.0.jar,groupId 里的点号会变成目录层级,这个规律在排查"明明坐标没问题但包找不到"时非常有用。
另外还有两个容易忽略的细节:
- 同一个
groupId:artifactId可以有多个版本,本地仓库里会并存各自版本的目录,不会互相覆盖。 - Maven 解析版本冲突时遵循"最短路径优先"原则,路径相同则先声明者优先。看到这里你应该明白,依赖冲突排查(经常需要敲
mvn dependency:tree命令)和仓库的目录结构强相关。
2. 把本地 jar 包"塞"进项目的几种姿势与坑
2.1 最不推荐的姿势:直接把 jar 扔进项目 lib 目录
先说我见过最多、也最容易翻车的方式:在项目根目录建一个lib文件夹,把本地 jar 放进去,然后在pom.xml里这样写:
<dependency> <groupId>com.local</groupId> <artifactId>some-lib</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${project.basedir}/lib/some-lib.jar</systemPath> </dependency>这个方式当年的确能跑通,因为 IDE 会把这个 jar 加入编译 classpath。但现在 Maven 构建、CI/CD、命令行打包时,scope=system的依赖是默认不会被打进可执行 jar 的,很多"打包成 jar 后提示 ClassNotFoundException"的案例就是这里埋下的雷。它还是个"脏手段",污染了 pom 的可移植性——别人拿到你的项目,如果没有lib目录就废了。
我的建议是:除非你有极其特殊的原因(比如第三方 jar 加密无法入库),否则系统 scope 这条路能不走就不走。
2.2 标准做法:用 mvn install 把本地 jar 安装进本地仓库
正确且通用的做法是,先手动把本地 jar 安装到 Maven 本地仓库,然后再以正常依赖的方式声明。核心命令是:
mvn install:install-file \ -Dfile=/path/to/your/local.jar \ -DgroupId=com.local \ -DartifactId=some-lib \ -Dversion=1.0 \ -Dpackaging=jar执行完之后,你就能在.m2/repository/com/local/some-lib/1.0/目录下看到some-lib-1.0.jar文件。此时在 pom.xml 里正常声明依赖即可,不需要任何 scope 和 systemPath:
<dependency> <groupId>com.local</groupId> <artifactId>some-lib</artifactId> <version>1.0</version> </dependency>这里有一个很实用的小技巧:如果你的本地 jar 还带 sources 或 javadoc,可以把它们一起安装,方便在 IDE 里看源码。执行时追加-Dclassifiers=sources参数,格式为:
mvn install:install-file \ -Dfile=your-sources.jar \ -DgroupId=com.local \ -DartifactId=some-lib \ -Dversion=1.0 \ -Dclassifier=sources \ -Dpackaging=jar2.3 团队协作场景:本地安装不够,要推到私有仓库
自己本机install只是"自嗨",因为其他同事的本地仓库里没有这个包,CI 服务器上也没有。只要你们有 Nexus 或 Artifactory,就应该一步到位,把本地 jar 推送到私服。
可以用mvn deploy:deploy-file命令实现:
mvn deploy:deploy-file \ -Dfile=/path/to/your/local.jar \ -DgroupId=com.local \ -DartifactId=some-lib \ -Dversion=1.0 \ -Dpackaging=jar \ -Durl=http://你的私服地址/repository/maven-releases/ \ -DrepositoryId=nexus注意这里的repositoryId要和settings.xml里配置的<server>节点 id 对应上,否则私服会以匿名身份拒绝上传。很多报 401 权限错误的情况,都是这个 id 没对上。
如果公司没有条件架私有仓库,临时方案可以是把本地 jar 提交到 Git 仓库某个固定目录,再配合一个 Maven 插件(如maven-install-plugin的执行目标)在构建前先执行 install-file。但这是妥协方案,说实话不如直接引入一个轻量级的私有仓库来得省心。
2.4 引入本地包时的典型翻车现场
根据我的实战经验,以下几类报错最典型:
Cannot resolve symbol/程序包xxx不存在:IDE 能看到 jar 但 Maven 看不到,几乎都是因为依赖坐标或者版本和你 install 时不一致。- 打包后运行时
ClassNotFoundException:scope=system的依赖没有被打进最终 jar。如果是打包插件配置了maven-shade-plugin的 artifactSet,还需要额外排除掉那些系统作用域的包。 Duplicate classes冲突:本地包和你引用的中央仓库某个依赖包含了相同的类路径,典型的是老版本日志框架冲突。这种要靠mvn dependency:tree去查重复依赖,再在 pom 里用<exclusions>排除。
3. 可执行 jar 包的本质问题:classpath 与主类入口
3.1 为什么 jar 包"能编译"但"不能运行"
很多人第一次敲java -jar xxx.jar得到"no main manifest attribute"报错时是懵的:明明项目里写了main方法啊。
原因在于:普通 jar 包只是一个 zip 压缩包,它内部有一个META-INF/MANIFEST.MF清单文件,这个文件里如果没有声明Main-Class属性,Java 运行时就不会知道该从哪个类开始执行。你能在 IDE 里右键"运行",那是 IDE 帮你指定了主类,它跟 jar 包的运行机制完全是两码事。
可执行 jar 需要同时满足两个条件:第一,MANIFEST.MF里有Main-Class指向入口类;第二,如果程序依赖了第三方库,还需要在 manifest 里声明Class-Path指向那些依赖,或者更高级的处理方式——把依赖类全部解压合并进一个"胖包"里。
3.2 普通 jar、可执行 jar、胖 jar、瘦 jar 这些概念
先把概念理清楚,后面选插件就不会乱:
- 普通 jar:只包含项目自身的 class 文件和资源文件,不处理依赖。
- 瘦 jar(Thin Jar):自身不包含依赖,靠 manifest 里的
Class-Path指向外部依赖目录来运行。 - 胖 jar(Fat Jar / Uber Jar):把项目自身的类、依赖的第三方类、配置文件全部合并到一个 jar 里,运行时不需要外部依赖也能直接运行。
每种形态都有适用场景。微服务项目的镜像构建一般不喜欢胖 jar,因为镜像分层不方便;但本地工具、演示项目、交给非技术人员运行的小工具,胖 jar 是体验最好的。
3.3 从"当前目录找依赖"到"把依赖装进 jar"的思路演变
以前老式做法是:打完 jar 后,手动在旁边建一个lib目录,把依赖全部复制进去,然后修改 manifest 里的Class-Path指向lib/xxx.jar。这在简单项目里可行,但在依赖树几十上百个 jar 的项目里就是灾难——你根本没法保证Class-Path的顺序和lib目录内容严格一致。
于是更聪明的方案出现了:与其人工维护外部lib目录,不如在打包阶段就把依赖直接塞进 jar 包。这样产物只有一个文件,分发、运行都干净利落。这也是 shade 和 assembly 插件成为主流方案的原因。
4. 三种常用打包插件对比:jar+classpath、shade、assembly
4.1 maven-jar-plugin:最朴素的方式,只打包自己的代码
先看最基本的配置:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.4.1</version> <configuration> <archive> <manifest> <mainClass>com.example.Main</mainClass> </manifest> </archive> </configuration> </plugin>这段配置的效果是:打出来的 jar 里包含项目自己的类和资源,MANIFEST.MF中会多一行Main-Class: com.example.Main。但注意,它不会把任何第三方依赖打进去,运行时如果用到依赖仍然会报ClassNotFoundException。
现实的用法是配合maven-dependency-plugin先把依赖复制到某个目录,比如:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin>这样target/lib下会生成所有依赖 jar,配合 jar 插件在 manifest 里声明Class-Path,最终也是可以运行的。但说实话,我现在基本不这么用了——配置冗长,还得保证lib目录跟着 jar 一起分发,出错的概率不低。
4.2 maven-shade-plugin:目前最主流的"胖 jar"方案
shade 插件我用的最多,它的核心能力是:将项目自身的 class 文件、第三方依赖的 class 文件全部解压后重新打进一个 jar 包,最终产物里不再依赖外部任何 jar。
最小配置如下:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.2</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.Main</mainClass> </transformer> </transformers> </configuration> </execution> </executions> </plugin>注意几个要点:
- 插件默认会把依赖打进同一个 jar,不需要额外配置。
- 如果多个依赖里有同名文件(比如
META-INF/services下的 SPI 配置文件),shade 在合并时可能会互相覆盖,导致服务发现失效。此时需要用到ServicesResourceTransformer进行合并,配置里加一行<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>即可。 - 依赖里如果包含签名文件(
META-INF/*.SF、*.RSA、*.DSA),合并后运行时会报SecurityException: Invalid signature file digest,需要在 filters 中排除:
<filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters>这个坑在引入 spring 相关库时特别常见,报错信息看似吓人,其实是签名文件冲突。
4.3 maven-assembly-plugin:既能打胖包,也能做分发包
assembly 插件的定位比 shade 更"杂",它不但可以把依赖全部打到一个 jar 里,还能生成 zip/tar.gz 格式的分发包,里面包含可执行脚本、配置文件、依赖目录等。如果你需要"一个压缩包 + 启动脚本"这种交付方式,assembly 是首选。
常见的"胖 jar"配置:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.7.1</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>com.example.Main</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>执行mvn clean package后会在target目录下生成xxx-jar-with-dependencies.jar,这就是胖 jar。和 shade 相比,assembly 不太擅长处理META-INF/services文件去重和类冲突重定位,所以如果你的项目用了 SPI、或者有几个依赖里存在同路径类,shade 更加稳妥。
4.4 三者怎么选:一张表说清楚
| 对比维度 | maven-jar-plugin | maven-shade-plugin | maven-assembly-plugin |
|---|---|---|---|
| 是否包含依赖 | 不包含 | 包含(合并类) | 可包含(合并类) |
| 配置复杂度 | 低 | 中 | 中 |
| 依赖冲突处理 | 不处理 | 可排除,可重定位 | 有限处理 |
| SPI 服务合并 | 不支持 | 支持且必须用 | 支持较弱 |
| 额外产物流 | 配合复制依赖可做瘦包 | 单 jar 为主 | 可做 zip/tar.gz 分发包 |
| 典型场景 | 被其他项目依赖的库 | Spring Boot 前的普通服务/工具 | 命令行工具发布包 |
我的个人建议是:如果是纯工具类小项目、要交出去给别人直接java -jar运行,优先shade;如果是需要带启动脚本、配置文件、日志目录一起分发的服务,用assembly的 zip 模式;如果是给别的项目调用 API 的 SDK 类项目,用maven-jar-plugin打普通 jar 就够了,千万别打胖包。
5. Spring Boot 项目打包的特殊逻辑与常见误区
5.1 spring-boot-maven-plugin 的全量重打包机制
Spring 生态里打可执行 jar 时,通常不会去配上面那些插件,而是直接用spring-boot-maven-plugin。它的工作机制容易让人误解:Spring Boot 插件的repackage目标会接管package阶段,但它是"二次加工"——先让 Maven 用默认方式打出原始 jar,然后再把原始 jar 的内容重新整理,生成一个 Spring Boot 特有的"可执行胖 jar"。
我们平时执行mvn clean package看到类似repackage的日志,就是它在工作。最终产物里,项目自身的BOOT-INF/classes、依赖 jar 在BOOT-INF/lib,启动器类在org/springframework/boot/loader,整体目录结构和普通 Java 项目完全不同。这也是为什么你不能用java -jar target/original-xxx.jar去运行(那个只是中间产物)。
标准配置长这样:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <mainClass>com.example.Application</mainClass> </configuration> </plugin>5.2 关于 Spring Boot 打可执行 jar 的三个真相
第一个真相:Spring Boot 生成的可执行 jar 并不是把依赖类解压合并,而是把依赖 jar 原样嵌套在BOOT-INF/lib目录下,由org.springframework.boot.loader.JarLauncher在运行时动态加载。这就意味着,如果你试图用jar tf看到普通 Java 项目那种"所有 class 混在一个文件里"的结构,是看不到的——它本来就是另一种运行模型。
第二个真相:如果你的 Spring Boot 项目里引入了本地 jar(system scope),默认情况下 Spring Boot 的 repackage 也不会把 system scope 依赖包含进BOOT-INF/lib。所以即便 Spring Boot 项目也很容易出现"开发正常、打包后运行缺类"的问题。处理方式还是要先把本地 jar 正常 install 到本地仓库/私服,以 compile scope 引入。
第三个真相:Spring Boot 的Main-Class写在 manifest 里是org.springframework.boot.loader.JarLauncher,你自己的启动类在 manifest 的Start-Class属性里。所以如果你手动去改 manifest,改错成自己的main方法,反而会运行失败。记住这个差异,排查"为什么 java -jar 启动不了"时会很有帮助。
5.3 外部配置文件带出 jar 外的一种实践
可执行 jar 打好之后,很多人会遇到一个问题:配置在application.yml里,而配置是写进BOOT-INF/classes的,每次改配置都要重新打包。实战里更灵活的做法是:让程序优先读取 jar 包外部的config目录或者当前目录下的application.yml。
放到 jar 的同级目录,再在启动脚本里指定:
java -jar app.jar --spring.config.location=config/application.yml如果你希望不写命令行参数、自动检测外部配置文件,可以在 jar 包同级放一个config文件夹,Spring Boot 默认的配置搜索顺序本来就包含./config/。这个策略在交付给运维、免去频繁重打包的开发环境中非常实用。
6. 命令行打包实战:从 clean 到 deploy 的完整操作追踪
6.1 最常用的命令组合及其含义
日常开发和 CI 中最常敲的几条命令我列一下,并说明实际执行了什么:
mvn clean compile清理target目录,然后编译源码到target/classes。这条命令不会打包,也不会跑测试,是快速检查代码能否编译通过的最好方式。
mvn clean package在编译基础上执行测试,并把 jar/war 包生成到target。如果你配置了 shade/assembly/spring-boot 插件,可执行 jar 也是在这个阶段生成的。
mvn clean install在 package 阶段产物基础上,把 jar 安装到本地仓库。这样其他本地项目可以直接以 Maven 依赖方式引用它。如果你改了公共模块,想让依赖方立即生效,就得执行 install。
mvn clean deploy在 install 基础上,将 jar 推送到私服仓库。这里就会用到pom.xml里的<distributionManagement>节点,以及settings.xml里的服务器认证信息。
6.2 跳过测试的正确姿势,而不是"不管测试"
有些人急于打包,直接敲mvn package -DskipTests。这个参数的真实效果是:编译测试代码,但不执行测试。还有一个更彻底的叫-Dmaven.test.skip=true,它连测试代码都不编译。两者区别你要清楚,在 CI 流程里如果测试代码有编译错误但不想跑测试,-DskipTests也会挂掉,因为测试编译是独立于测试执行的。
我个人的习惯是:本地开发打临时包用-Dmaven.test.skip=true追求速度,提交 CI 时强制完整跑测试,绝不跳过。
6.3 命令行里临时指定参数:-D的高阶用法
链式的-D参数是 Maven 使用者最容易忽略的宝藏。比如你想在打包时临时避开某个私服不可用的问题,可以手动把仓库地址变成命令行参数:
mvn clean package -DaltDeploymentRepository=nexus::default::http://私服地址/repository/maven-releases/又比如在构建时动态指定版本号:
mvn clean package -Drevision=2.0.0-SNAPSHOT这要求 pom 里的<version>写成${revision}。这种动态版本号做法在多人分支并行开发时很能减少版本冲突。
7. 实战经验总结:我踩过的坑和给你的建议
7.1 关于本地依赖的三个建议
把 jar 包"放"进项目,说起来只是一句话,但它们之间的差异会直接影响后续构建和分发。我的三个建议是:
- 本地包优先走
mvn install:install-file进本地仓库,别用systemPath,除非这个 jar 只是临时调试用。 - 如果本地包是给多人用的,尽快推送到私服,靠 GitHub 上放 jar 文件、或者靠"大家手动 install"都是隐患极大的方案。
- 把 jar 文件和来源说明放在项目文档里记录一下,甚至可以在 Git 里建一个
third-party-jars/目录存原始 jar,下次有人问"这个 jar 哪来的"不至于查半天。
7.2 关于打包插件的三个建议
- 先看项目类型再选插件。Spring Boot 项目别画蛇添足去配 shade 和 assembly,spring-boot-maven-plugin 自带 repackage 已经足够。普通工具类、无框架项目、需要单个 jar 分发的,shade 优先。
- 用 shade 时,一上来就配好排除签名文件和
ServicesResourceTransformer,不用等到出 bug 再加。这俩是我遇到概率最高的两个问题。 - 任何打包配置比拼的是最终
java -jar能不能跑起来,不要停留在"构建成功"的幻觉里。构建成功只能说明编译和打包没报错,运行时的 classpath、资源文件合并、依赖缺失是另一套维度。
7.3 关于排查思路的三条链路
遇到打包问题,我推荐按照下面的顺序排查,比乱试要快得多:
- 问题表现在"编译失败",先查仓库:敲
mvn dependency:tree -Dverbose,看具体依赖完整路径、版本和来源,确认坐标是否匹配、本地仓库有没有有效缓存文件。很多时候rm -rf ~/.m2/repository/相关目录后重新mvn install反而能解决所谓"缓存损坏"问题。 - 问题表现在"jar 运行找不到主类",先查 manifest:解压看
META-INF/MANIFEST.MF,确认Main-Class写没写对,或者是否被 Spring Boot 的JarLauncher逻辑覆盖了。 - 问题表现在"运行时报 ClassNotFoundException",先看依赖有没有进 jar:用
jar tf your.jar在 jar 里搜索报错的那个类路径,如果不存在就是依赖没打进去;如果存在,多半是重复类或 SPI 冲突。这一步往往能快速定位是 shade 的 artifactSet 配置问题还是 assembly 的 descriptorRef 问题。
打包可执行 jar 这件事,本质上就是在理解 Maven 仓库、生命周期和插件三者之间的关系。仓库决定了依赖从哪来,生命周期决定了构建步骤的顺序,插件决定了最终产物长什么样。把这三条线串起来,遇到任何打包问题你都不会再慌。至少对我来说,当年被scope=system坑过、被 shade 合并签名文件坑过之后,就再也不满足于"能跑就行"了——搞清楚底层逻辑,下次踩坑的概率才会真正降下来。
本文还有配套的精品资源,点击获取