简介:面向Maven初学者与Java构建开发者,这份资料围绕Maven仓库概念、本地JAR包引入及可执行JAR打包三个核心主题,用一个小型Eclipse工程示例说明依赖管理与构建配置要点。压缩包共6个文件,资源类型为zip,包括Eclipse项目配置(project、classpath、prefs)、pom.xml示例以及一个Java主类文件,整体约5KB,配置与源码分离,便于逐项对照。已有2767人学习下载,说明其在日常开发排障中被频繁参考。读者可借此理解本地仓库~/.m2/repository的目录组织,掌握手工放置JAR并执行mvn install引入本地包的流程,同时对比maven-jar-plugin、maven-assembly-plugin、maven-shade-plugin三种打包方式在可执行JAR生成上的差异,快速迁移到实际项目构建中。
1. Maven仓库机制,先搞懂依赖从哪来
Maven 这东西,很多 Java 开发天天在用,但真被问到"你项目里的 jar 包到底从哪来?先找哪个仓库?找不到又会怎样?"不少人会卡壳。我这些年帮团队排查构建问题,发现大半的坑都出在对仓库机制的理解上,所以这篇从仓库说起,再聊本地包引入和可执行 jar 的打包套路,一条线串完。
1.1 仓库分三层,先对号入座
Maven 的仓库体系可以理解成三层结构:本地仓库、远程仓库(私服)、中央仓库。
本地仓库就是你自己机器上的一个目录,默认在用户目录/.m2/repository下。所有从远程下载的依赖都会缓存到这里,项目构建时优先从这里找。你可以把它理解成电脑里的浏览器缓存,第一次访问慢,之后就快多了。
远程仓库,尤其是公司内部的 Nexus 或 Artifactory 这类私服,扮演的是"中转站+保险柜"的角色。开发部门把公共组件传到私服上,整个团队的机器都能拉取,没有必要每个人都去访问外网。外网访问不稳定的时候,私服的价值就特别明显。
中央仓库是 Maven 官方维护的公共仓库,托管了绝大多数开源组件的 jar 包。它位于互联网上,默认配置就能用,但在国内访问速度往往不够理想,所以大家都会配置阿里云等镜像来加速。镜像的本质,就是"换一个距离更近、带宽更大的下载源"。
三层之间是逐级查找的关系:本地仓库 → 私服 → 中央仓库。本地没有就去私服拉,私服没有再往外网拉。拉下来之后会缓存在本地,下次直接用。
1.2 配置文件里的关键节点与镜像加速
Maven 的配置文件是settings.xml,位置有两处:全局配置在 Maven 安装目录下的conf/里,用户配置在~/.m2/目录下。推荐优先修改用户配置,因为它是当前账号私有的,不会影响同一台机器上的其他用户。
一个完整的settings.xml至少需要关注这几个节点:
<settings> <localRepository>D:/maven-repo</localRepository> <mirrors> <mirror> <id>aliyun-public</id> <name>Aliyun Public Mirror</name> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>*</mirrorOf> </mirror> </mirrors> <profiles> <profile> <id>jdk-17</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> </profile> </profiles> </settings>localRepository指定本地仓库路径,默认值在用户目录下,如果你的 C 盘紧张,建议挪到别的盘。mirrorOf的值如果配成*,表示所有远程仓库都走这个镜像;配成central,就只对中央仓库做镜像。多镜像配置时注意,mirrorOf不要互相重叠,否则后面的会被前面覆盖。
JDK 版本属性在打包时常被忽略,但它直接决定了编译产物运行的 Java 版本。如果本机 JDK 是 17,而服务器上是 8,不显式指定编译版本的话,打出来的 class 文件大概率跑不起来。
1.3 依赖搜索顺序与"找不到依赖"的根源
Maven 寻找依赖时遵循一个固定顺序:
- 查本地仓库,存在且版本匹配,直接使用。
- 本地没有,检查是否配置了私服,去私服拉取。
- 私服也没有,走中央仓库(或镜像)。
- 全部找不到,报
Could not resolve dependencies错误。
实际项目里,依赖解析失败的常见原因有这几种:
- 依赖写错了 groupId、artifactId 或 version,坐标对不上。
- 本地仓库里有残留的
.lastUpdated后缀文件,这类文件是下载中断留下的标记,Maven 看到它就不会再尝试重新下载。 - 网络对中央仓库访问不稳定,或者镜像配置错误。
- 依赖传递时某个中间件在私服上没有,导致链条断裂。
排查思路很简单:看到解析失败,先去本地仓库对应路径下看看有没有.lastUpdated文件,有就删掉整个目录再重新构建;没有就去 Maven 中央仓库官网搜一下坐标,核对是否拼写有误。这两步能解决八成的依赖问题。
2. 引入本地Jar包,三种方式对比与实操
项目开发中总会遇到这种情况:有个 jar 是内部工具包,没有传到公司私服,也没法上传到中央仓库,但你的代码就必须依赖它。这时候就需要想办法把它引入到项目里。
2.1 方式一:mvn install-file,把本地包装进本地仓库
这是最推荐的方式,本质是手动将 jar 安装到本地 Maven 仓库,之后在pom.xml里像一个普通依赖一样引用即可。
操作步骤如下:
mvn install:install-file \ -Dfile=./libs/my-custom-tool.jar \ -DgroupId=com.example \ -DartifactId=my-custom-tool \ -Dversion=1.0.0 \ -Dpackaging=jar执行完毕后,你会在本地仓库对应路径下看到com/example/my-custom-tool/1.0.0/目录。然后在项目 pom 里正常声明:
<dependency> <groupId>com.example</groupId> <artifactId>my-custom-tool</artifactId> <version>1.0.0</version> </dependency>这个方法的优势很直接:与普通依赖无差别,不需要改动构建的配置。缺点也同样明显——它只在你本机生效。团队成员如果没执行同样的命令,他们的本地仓库里没有这个依赖,一拉代码就报错。所以这个方式只适用于个人验证或临时使用,团队协作的场景并不合适。
2.2 方式二:system scope的利与弊
在 pom 里直接指定系统路径引用 jar,看起来更省事:
<dependency> <groupId>com.example</groupId> <artifactId>my-custom-tool</artifactId> <version>1.0.0</version> <scope>system</scope> <systemPath>${project.basedir}/libs/my-custom-tool.jar</systemPath> </dependency>这种方式把 jar 存在项目的libs/目录下,随项目一起走,适合团队多人协作的场景。但它有一个埋得很深的问题:systemscope 的依赖在打包时默认不会被包含进最终的产物。如果你用mvn package打出的 jar 扔到服务器上,运行时会报ClassNotFoundException。另外很多云构建平台和容器化构建工具对systemscope 支持不友好,构建容易出幺蛾子。
还有一个隐含问题:system依赖不会参与依赖冲突调节,遇到版本传递问题更难排查。
2.3 方式三:搭建私有仓库(Nexus)统一管理
当团队规模上来了,本地 install 和 system scope 都显得不够正规。标准做法是部署一个 Nexus 私服,把项目依赖统一推上去。
流程大致是:
- 部署 Nexus 服务。
- 在 Nexus 中创建一个 hosted 类型仓库,比如
thirdparty。 - 上传本地 jar 包到该仓库。
- 在
settings.xml或 pom 中配置仓库地址。 - 团队成员正常拉取依赖,无需任何额外操作。
Nexus 的界面操作不算复杂,进入仓库视图后选择 Upload 组件,填写 GAV 坐标并选文件即可。配置完成之后,整个团队解析依赖的行为就统一了。这个方案前期需要一定的部署成本,但对团队长期效率的提升非常明显。
2.4 三种方式的对比与选型建议
这三种方式没有绝对的优劣,更多是适用场景不同。我按实际经验画了一张对比表:
| 方式 | 生效范围 | 打包兼容性 | 团队协作 | 适用场景 |
|---|---|---|---|---|
| mvn install-file | 仅本机 | 好 | 差 | 个人临时验证 |
| system scope | 随项目 | 差 | 一般 | 小项目、快速演示 |
| Nexus 私服 | 全团队 | 好 | 好 | 正常团队项目 |
如果你是一个人做项目,怎么方便怎么来。但到了团队协作阶段,我建议尽早把依赖纳入私服管理,省得哪天新同事拉完代码一脸懵:"这个依赖哪来的?为什么我构建不了?"
3. 多种方式打出可执行Jar包
依赖问题解决之后,就轮到打包了。这里"可执行"的意思是:打出来的 jar 可以java -jar app.jar直接启动,不需要再手动拼 Class-Path。实现方式主要有三种,每种有各自的定位。
3.1 方式一:maven-jar-plugin + 外部依赖目录
Maven 默认打的 jar 里面只有项目自己的 class 和资源文件,不包含第三方依赖。所以即使打包成功,直接java -jar也会报NoClassDefFoundError。
一种做法是用maven-jar-plugin定制 manifest,把 Class-Path 指向外部依赖目录:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>3.4.1</version> <configuration> <archive> <manifest> <mainClass>com.example.MainApplication</mainClass> <addClasspath>true</addClasspath> <classpathPrefix>lib/</classpathPrefix> </manifest> </archive> </configuration> </plugin>然后再用maven-dependency-plugin把项目依赖复制到lib/目录:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin>最后部署的时候把 jar 和lib/目录放在同一个文件夹下,就能正常启动。这种方式的优点是产物比较清爽,依赖单独存放,升级某个依赖时只需要替换 lib 下的 jar。缺点是部署时要多带一个目录,不够"单文件"。
3.2 方式二:maven-shade-plugin,打一个胖jar
如果要追求单个 jar 文件搞定一切,maven-shade-plugin是最常用的方案。它的原理是把所有依赖解压出来,重新打包进同一个 jar 中。
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.5.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <createDependencyReducedPom>false</createDependencyReducedPom> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.MainApplication</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin>这里有几个值得注意的细节。createDependencyReducedPom建议设成false,防止生成的 pom 被替换,导致 IDE 里依赖显示异常。filters里排除签名文件是必要的——很多依赖 jar 包带有 jar 签名,合并时这些签名文件不排除的话,打包过程会直接报SecurityException,或者运行时提示"Invalid signature file digest"。
shade 插件也不是没有问题。当多个依赖包含同名文件(比如某些配置文件、SPI 文件、META-INF/services下的扩展描述)时,后合并进来的会覆盖先合并的,可能导致功能缺失。这种情况需要配合ServicesResourceTransformer这类 transformer 来做合并。如果项目用到了 Spring Boot,官方推荐直接用 Spring Boot 的打包插件,它内置了更完善的资源合并策略,比 shade 更省心。
3.3 方式三:maven-assembly-plugin,生成zip发布包
maven-assembly-plugin的能力不局限于 jar,它可以把项目打成 zip、tar.gz 等格式的发布包,里面可以包含 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.MainApplication</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin>jar-with-dependencies是插件自带的一个预定义描述符,效果与 shade 类似。但它生成的 jar 名字会带-jar-with-dependencies后缀,默认不会替换原 jar。assembly 更适合的场景是配合自定义描述符,把启动脚本、配置文件、静态资源一起打包成发布产物。在 CICD 流程中,这种方式更利于上线部署的标准化。
3.4 manifest主类与Class-Path的底层逻辑
不管用哪种插件,最终都要配置Main-Class和Class-Path。Main-Class告诉 JVM "启动时找哪个类的 main 方法",Class-Path告诉 JVM "运行时去哪找依赖类"。
打开一个可执行 jar 的META-INF/MANIFEST.MF文件,你会看到类似这样的内容:
Manifest-Version: 1.0 Main-Class: com.example.MainApplication Class-Path: lib/dependency-a.jar lib/dependency-b.jarshade 和 assembly 之所以打出的 jar 能独立运行,就是因为它们把依赖真正放进了 jar 里,或者把依赖路径写进了 manifest。而 maven-jar-plugin 的第一种方式,依赖还在外部目录,靠的是 manifest 里的 Class-Path 来定位。理解了这一层,遇到"为什么这个 jar 能跑、那个 jar 跑不了"的问题,你就知道该往哪里排查了。
4. 常见问题排查与避坑实录
4.1 打出的jar运行报"NoClassDefFoundError"
这是最经典的问题。现象是mvn package显示 BUILD SUCCESS,但java -jar一跑就报错。原因基本就是上面说的:打出的 jar 里没有依赖。
处理思路分两步。第一,如果用默认的 spring-boot-maven-plugin,确认是否已经执行了repackage目标。Spring Boot 的打包插件和普通 jar 插件不一样,必须在spring-boot-maven-plugin的 execution 里绑定repackage阶段,而且该插件的mainClass要显式指定,否则在部分场景下会找不到主类,生成一个"不可执行"的原始 jar。第二,如果你用 shade,确认依赖有没有被错误地 exclude 掉,检查dependency-reduced-pom.xml是否杀掉了关键依赖。
4.2 jar包冲突与依赖版本覆盖
Maven 的依赖仲裁策略简单直接:依赖路径最近的优先,深度相同时先声明的优先。这就导致一个常见问题——你的项目里有两个间接依赖分别引入了不同版本的同一个库,实际打包进去的是最先声明的那个,另一个版本的类特征没对齐,运行时就报NoSuchMethodError或者ClassNotFoundException。
排查工具最有效的是mvn dependency:tree,一条命令就能看清完整依赖树。看到版本冲突时,用exclusion排除掉不需要的传递依赖,或者用dependencyManagement统一版本管理。这里一定要记住:dependencyManagement只管版本,不会引入依赖,它只声明版本;真正要引入还是得用dependencies声明依赖本身。
4.3 IDEA与命令行环境不一致
每个项目在 IDEA 里配的 Maven 可能是自己安装的,设置里也可能勾选了"Use Maven wrapper",但你命令行用的那套 Maven 又是另外一份配置,两边 settings.xml、本地仓库路径不同,很容易出现 IDEA 中构建正常、命令行构建失败的情况。
我个人的做法是:在 IDEA 的 Maven 配置页里,强制指定 Maven home path、User settings file 和 Local repository,尽量和命令行保持完全一致。这样无论在 IDE 里还是终端里操作,依赖解析和打包的行为都统一。另外一个容易踩的坑是 IDE 中 Build 按钮和mvn clean package的结果可能不同——如果项目配了 profile,IDE 可能需要手动激活 profile 才会生效,而命令行加参数就能指定。
4.4 Maven构建时的编码与资源过滤问题
中文乱码是个老生常谈但很容易进坑的问题。在 pom 里统一声明编码能避免很多奇怪的构建问题:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties>另外要留意资源过滤的坑。默认情况下 Maven 的 resource 插件不会处理src/main/resources里的特殊符号,但如果你启用了资源过滤(<filtering>true</filtering>),那@xxx@或${xxx}这类占位符会被尝试解析,如果对应的属性不存在,构建就会报错或者替换成空值。所以非必要别开 filtering,开了就确保属性都有定义。
4.5 多模块项目中的打包细节
多模块项目里,父模块的 packaging 通常声明为pom,子模块才会打到 jar。Maven 的 reactor 模式会自动按依赖关系排序模块的构建顺序,子模块依赖了另一个子模块时,它会自动先构建依赖模块。但有一点要注意:子模块之间出现循环依赖时,Maven 会直接构建失败,提示 cycle 错误。这种问题没有技巧,必须调整模块设计。
在多模块打包时,根目录执行mvn clean install和mvn clean package的结果也不同。install会把构建产物安装到本地仓库,方便本地其他项目引用;package只在 target 目录生成产物。如果子模块之间有依赖关系,只执行package可能无法解析到尚未安装的模块,实际使用中如果遇到"找不到父模块"之类的报错,优先考虑在父目录执行install。
写在最后的实操习惯
做 Java 后端这几年,我对 Maven 最大的体会就是:它本质是"约定优于配置"的工具,大部分问题都出在"我猜它应该这样"和"它实际就是这样"的偏差上。遇到构建问题,先看本地仓库,再看依赖树,最后看打包插件配置,基本能解决绝大多数疑难杂症。
最后分享一个我自己一直保留的习惯:新建项目时,在根目录放一个mvnw(Maven Wrapper),它能把 Maven 版本固定下来,团队所有人用同一个版本构建,可以省掉"我这构建没问题啊"之类的经典对话。别看这设置不起眼,版本不一致导致的诡异问题一抓一大把。Maven 不是个多复杂的工具,但把这套流程理清了,构建这块能省下大量无意义的排查时间。
本文还有配套的精品资源,点击获取