Maven 入门与私有仓库搭建
📝 笔记摘要:Maven 入门与私有仓库搭建
这篇整理的是一套能跑通的 Maven 笔记,从「Maven 到底解决了什么」讲起,一路到 pom 文件逐个标签的含义、IDEA 里值得装的插件、SSM 项目的搭建顺序,再讲用 Nexus 搭一套自己的私有仓库,把「下载走私服、上传走私服」两条链路都打通。
私服那部分 Windows 和 Docker 两条部署路都写了,仓库类型怎么选、代理仓库填什么远端地址、聚合仓库勾哪几个,都配了截图。末尾整理了几个实战里真踩过的坑。
1. Maven 解决什么问题
没有 Maven 的时候,项目管理基本靠手工jar包。用久了会遇到两类问题:
jar 包版本不统一
A 项目要 Spring 5.2,B 项目要 Spring 5.3,同一个 jar 拷来拷去,迟早冲突
换个机器跑不起来
Windows 上开发好的工程,拷到 Linux 直接报找不到类,因为依赖路径根本不在代码里
Maven 把「这个项目需要什么、从哪来、怎么组装」这三件事写进一个 pom.xml 里,剩下的交给它。
整套机制可以拆成三块作用:
依赖管理
jar 包版本写在 pom 里,不用手动拷,冲突有固定规则可循
跨平台构建
同一份 pom,Windows、Linux 上mvn package出来的结果一致
统一项目结构
所有 Maven 项目长一个样,换项目不用重新找源码在哪
产物也不只是 jar。源码包、帮助文档、war 包、XML 配置,都是插件在生命周期里挂上去的。
2. 生命周期
Maven 的生命周期有三组,互不相干:
clean 生命周期:清理上一轮的产物
pre-clean→clean→post-clean
default 生命周期:编译、测试、打包、发布的主线
validate→compile→test→package→verify→install→deploy
site 生命周期:生成项目站点文档,和打包无关
注意:
clean是独立于其他生命周期的一条线,不属于 default。写mvn clean package实际上是依次调用两条生命周期里的目标,中间以空格分隔,不是从 clean 一路串到 package。
常用阶段各自的产出:
compile编译主代码到target/classes
test运行单元测试
package打 jar(war 项目出 war)
verify校验打包结果,格式校验类插件挂在这
install把产物装进本地仓库
deploy把产物推到远程仓库
项目结构也是 Maven 规定的标准布局,新建项目时目录应该是这样:
3. 安装与配置
去官网下 apache-maven-3.6.3-bin.zip,这是稳定版。解压后的目录长这样:
bin 放可执行命令,boot 是启动器,conf 里的 settings.xml 是全局配置,lib 是依赖库。conf 这个目录后面改镜像、改私服全靠它。
改环境变量之前,先记住一个判断依据——
提示:IDEA 点运行 maven 命令时,实际调的就是
MAVEN_HOME/bin/mvn。所以MAVEN_HOME配错,IDE 里的构建一定跟着错,命令行却可能还正常(因为命令行是用别的mvn)。以 IDEA 的表现来验证最准。
配环境变量的入口在 Windows 的「此电脑 → 属性 → 高级系统设置 → 环境变量」:
配三项:
JAVA_HOME
JDK 的安装根目录,比如C:\Program Files\Java\jdk1.8.0_291
MAVEN_HOME
Maven 解压根目录,比如I:\apache-maven-3.6.3
PATH 追加
%MAVEN_HOME%\bin
%MAVEN_HOME% 这种写法比写死绝对路径好,换机器改一个变量就够了。验证方式是在命令行敲 mvn -v,能打印出 Apache Maven 和 Java 版本就说明两个 HOME 都对上了。
改 conf/settings.xml,两处最常动:
localRepository 本地仓库路径
默认在C:\Users\你的用户名\.m2\repository,换到I:\maven-repo\javaee-repo,C 盘就不会被撑爆
mirrors 镜像地址
国内拉依赖慢,用阿里云加速
<settings>
<localRepository>I:\maven-repo\javaee-repo</localRepository>
<mirrors>
<mirror>
<id>aliyun</id>
<name>aliyun maven</name>
<url>https://maven.aliyun.com/repository/public</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
</settings>
4. 仓库体系
依赖从哪来,取决于它躺在哪一层:
本地仓库 机器上的缓存目录
依赖第一次拉下来就放这儿,之后再构建直接命中本地,不再走网络
远程仓库 公网上的官方仓库
比如 Maven 中央仓库,只有本地没有才会去请求
私服仓库 公司/团队自己搭的
放内部构件,同时代理公网依赖,充当本地和中央之间的缓冲
以 A 项目第一次引入 jedis 为例:
pom 里写了
<dependency>jedis,Maven 先翻本地仓库
有 → 直接用,结束
没有 → 按<mirror>配置去私服找
私服有 → 下载一份到本地仓库,再交给编译器
私服也没有(代理的公网仓库可能有)→ 私服去远程仓库拉,拉完缓存在自己那儿,同时分发一份到你的本地仓库
这就是私服存在的意义:第二个同事构建同一个项目时,直接从他搭的私服拿,不用每个人都去访问中央仓库。
5. Maven 坐标
一个依赖在 Maven 里靠三元组唯一定位,缺一个都定位不到:
groupId 组织名,一般是公司域名反写,如
org.springframework
artifactId 模块名,同一组织内唯一,如spring-core
version 版本号
开发中的版本用 1.0-SNAPSHOT,表示随时会变;正式发布去掉这个后缀,写 1.0。IDEA 里新建模块会让你填这三项:
一个完整的 pom 骨架:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
提示:
<packaging>不写默认是jar,出 web 项目才写war。<properties>里的maven.compiler.source/target不写会跟着当前 JDK 走,团队里 JDK 版本不统一时构建结果就不一致了,写死更稳。
6. IDEA 上值得装的两个插件
Maven Helper,装上以后 pom 文件右键会多一个 Dependency Analyzer:
面板会分三栏列出:被使用的依赖、声明但未使用的依赖(灰色,可以删)、被其他依赖间接引入的依赖。排查 NoSuchMethodError 这类冲突时,先来这里看同一个 jar 是不是进了两个版本。
maven-search,用来替代浏览器搜 mvnrepository:
搜到之后右侧直接生成可复制的 maven 和 gradle 依赖片段,省得手打坐标。快捷键 Ctrl + Shift + M,菜单入口在 Tools → Maven Search。
7. pom 文件详解
出处:本节标签说明与示例取自 Introduction to the POM(Apache Maven 官方文档,Apache License 2.0)
7.1 根元素
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
...
</project>
<modelVersion>固定写4.0.0,这是 POM 本身的 schema 版本,和项目版本无关
7.2 parent 继承
<parent>
<groupId>org.example</groupId>
<artifactId>parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
relativePath 决定去哪找父 pom
默认../pom.xml,即当前目录的上一级
写<relativePath/>(空元素)表示本地找不到,直接去仓库按坐标找
父 pom 在更深的位置就写相对路径,如../../parent/pom.xml
能继承到什么
dependencies、dependencyManagement、build 里的配置、properties
不能继承什么
artifactId、name、properties 里项目自己定义的属性会覆盖父的
7.3 build 路径管理
不写 <build> 时,Maven 用一套约定路径。写在 <build> 里可以改掉:
<build>
<sourceDirectory>src/main/java</sourceDirectory>
<scriptSourceDirectory>src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>src/test/java</testSourceDirectory>
<outputDirectory>target/classes</outputDirectory>
<testOutputDirectory>target/test-classes</testOutputDirectory>
</build>
sourceDirectory主源码目录,默认src/main/java
testSourceDirectory测试源码目录,默认src/test/java
outputDirectory主代码编译输出,默认target/classes
testOutputDirectory测试代码编译输出,默认target/test-classes
scriptSourceDirectory脚本源目录
7.4 build 资源管理
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<targetPath>targetPath</targetPath>
<filtering>false</filtering>
<includes>
<include>**/*.properties</include>
</includes>
<excludes>
<exclude>**/*.java</exclude>
</excludes>
</resource>
</resources>
<testResources>
<testResource>
<directory>src/test/resources</directory>
</testResource>
</testResources>
</build>
directory资源所在目录
targetPath拷贝到outputDirectory下的哪个子路径
filtering是否启用变量替换,详见下方说明
includes只打包匹配的,支持**通配
excludes排除匹配的
filtering 是什么:开启后,资源文件里写的
${project.version}会在打包时被替换成 pom 里的真实值。所以生产环境的数据库地址可以抽成env.properties,只在一份 pom 里维护。
7.5 pluginManagement 与 plugins
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
</plugin>
</plugins>
</build>
pluginManagement 只声明不启用
作用是统一锁版本,子模块引用这个插件时可以省略 version
plugins 真正执行构建
这条才让插件挂到生命周期上
提示:为什么每个项目都要显式声明
maven-compiler-plugin之类的版本?因为 Maven 的内置插件版本跟着 Maven 版本走,团队里 Maven 版本不同时,构建出来的产物行为会不一致。在pluginManagement里统一锁死版本是最省事的做法。
7.6 build 其他元素
<build>
<extensions>
<extension>
<groupId>com.example</groupId>
<artifactId>some-extension</artifactId>
<version>1.0</version>
</extension>
</extensions>
<defaultGoal>clean</defaultGoal>
<directory>${project.basedir}/target</directory>
<finalName>${project.artifactId}-${project.version}</finalName>
<filters>
<filter>src/main/resources/filters.properties</filter>
</filters>
</build>
extensions 打包阶段的扩展,用得少,做特殊打包格式时才碰
defaultGoal 敲mvn不带参数时执行的阶段
directory 构建输出根目录,默认target
finalName 产物文件名,不写默认是artifactId-version
filters filtering 时用到的属性文件,一个 filter 可以供多处资源引用
7.7 dependencies 依赖声明
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.8</version>
<type>jar</type>
<scope>compile</scope>
<optional>false</optional>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
type 依赖类型,默认 jar。取 war、ejb、test-jar 等
classifier 同一 groupId + artifactId 下的附加物,如 sources、javadoc
systemPath 引用本地绝对路径的 jar,Maven 3 起不推荐,会告警
optional true 表示这个依赖不会传递给依赖本项目的下游
exclusions 排除掉某个传递依赖,用来解冲突
scope 决定依赖在哪些阶段可见,一共五个取值:
compile 编译、测试、运行全都要,不写默认就是它
provided 编译和测试需要,运行期由容器提供,不打进包。servlet-api 属于这类
runtime 运行期需要但编译期用不到,比如 JDBC 驱动
test 只在测试阶段有效,不会传递
system 直接指向本地文件,已废弃,别用
提示:打包出的 war 里没有
provided依赖,这是设计如此,不是漏配。Tomcat 容器自己会提供 servlet-api,重复打进 war 反而会冲突。
7.8 dependencyManagement 版本集中管理
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-framework-bom</artifactId>
<version>5.3.8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
只声明版本,不实际引入依赖
子模块写<dependency>时可以省掉 version,自动继承这里的版本
import 是特殊 scope,只能用在 pom 类型上
作用是导入一份 BOM,把里面所有管理的版本一次性引进来
父子项目里,子模块的 <dependencies> 真正加依赖,父模块的 <dependencyManagement> 锁版本,这个组合是多模块项目最常用的写法。
8. Maven 搭建 SSM 项目
在 IDEA 里用 Maven 搭一个 Spring + SpringMVC + MyBatis 的 web 项目,顺序是固定的:
IDEA 新建 webapp 项目,Project SDK 选好,Artifact 选
xxx:war exploded
导入 Maven 依赖,确认 pom 已生成
配置springmvc.xml,扫描 controller 包
配置web.xml,前端控制器指向 DispatcherServlet
配置applicationContext.xml,扫 service 和 dao 层
写jdbc.properties,数据源参数
分层建包:domain、service、impl、controller、dao、mapper
写 Mapper 接口和对应的Mapper.xml
配置 Tomcat 发布,访问首页
提示:第 2 步和第 9 步最容易漏。IDEA 新建 webapp 项目时勾上
Use Maven for dependencies才会生成 pom;不发布的话依赖注入全是空指针,报错位置还离真正原因很远。
9. Nexus 私有仓库搭建
Nexus 是最常搭的 Maven 私服,一个进程同时干三件事:代理公网仓库、存放团队内部构件、给前端/npm 等其他生态提供仓库。
9.1 Windows 部署
下载 nexus-3.42.0-01-win64.zip 解压后,先配环境变量,方式和 Maven 一样:
PATH 里追加 bin 目录:
Nexus 要占用的端口写在配置文件里,不改的话默认是 8081,和别的东西容易撞。路径是 sonatype-work/nexus3/etc/nexus-default.properties:
application-port=8088
application-host=0.0.0.0
然后启动,Windows 版进 bin 目录执行:
nexus.exe /run
首次启动会跑一会儿内嵌的 Elasticsearch,日志刷完之前页面访问不了。起来之后打开 http://localhost:8088,用 admin 登录。Nexus 3.17 以后初始密码是随机生成的,在数据目录里:
登录进去第一件事是去 Administration → Security → Anonymous Access 把匿名访问打开,否则拉依赖会被 401 拦住。开放之前先想清楚:开放之后这个 Nexus 就变成团队内部谁都能拉包的状态了。
提示:默认只监听本机(
127.0.0.1)。要给别人访问,把application-host改成0.0.0.0,同时确认防火墙放行了端口,并给 admin 换个密码。内网私服不要直接暴露到公网。
9.2 Docker 部署
已经有 Docker 环境的话直接上镜像,比 Windows 版省事:
docker pull sonatype/nexus3
mkdir /data/nexus-data
chown -R 200 /data/nexus-data
docker run -d -p 8081:8081 \
--name nexus \
--restart always \
-v /data/nexus-data:/nexus-data \
sonatype/nexus3
chown -R 200 这一步不能省:容器里的 Nexus 以 UID 200 运行,宿主机目录属主不对的话容器写不进去,日志里只会报权限错误,很难一眼看出原因
端口映射:容器内固定 8081,要改对外端口只改冒号左边
-v 挂载数据目录:不挂载的话容器一删,所有仓库配置和构件全丢
9.3 建仓库
登录后进 Administration → Repositories → Create repository,能看到当前所有仓库:
创建时会先让选 recipe,Maven 依赖选 maven2 (proxy):
三种 recipe 的分工:
proxy 代理远端仓库,本地没有就转发过去拉,拉完缓存在 Nexus 里
hosted 自己往里放东西,团队mvn deploy的目标
group 聚合,把几个仓库挂在一个 URL 下,对外只暴露一个地址
建 proxy,远端地址填阿里云,国内拉取比中央仓库快很多:
建 hosted,官方仓库不会帮你建 releases 和 snapshots,得自己建两个,版本策略选 Release 或 Snapshot:
建 group,把 proxy 和两个 hosted 都加进 members,配好之后对外就一个 URL:
建完之后仓库列表就齐了:
maven-central 是自带的代理仓库,maven-public 是自己建的聚合仓库,另外两个是自己建的私有仓库。
9.4 下载走私服
改 settings.xml,加一个 mirror 把所有请求都转给 Nexus:
<mirrors>
<mirror>
<id>nexus</id>
<mirrorOf>*</mirrorOf>
<url>http://localhost:8088/repository/maven-public/</url>
</mirror>
</mirrors>
<mirrorOf>*</mirrorOf> 所有仓库都走这个镜像
<mirrorOf>central</mirrorOf> 只镜像中央仓库
<mirrorOf>external:*</mirrorOf> 镜像所有远程仓库,本地仓库不受影响
改完清一次本地缓存重新拉,Nexus 的 Browse 里就能看到缓存下来的构件。
提示:
mirrorOf写*时,Nexus 一挂掉所有依赖都拉不动,整个团队的构建会一起停摆。写external:*至少本地仓库不受影响。
注意:老配置里常见的
http://jcenter.bintray.com/已经停止服务了,2021 年底就下线,现在配着只会拉不到包。遇到依赖从 jcenter 解析的报错,把那个 mirror 换成https://maven.aliyun.com/repository/public或直接指向 Maven 中央仓库。
9.5 上传走私服
上传需要认证,settings.xml 里配 server,id 要和 pom 里 distributionManagement 的 id 对上:
<servers>
<server>
<id>nexus-releases</id>
<username>admin</username>
<password>你的私服密码</password>
</server>
<server>
<id>nexus-snapshots</id>
<username>admin</username>
<password>你的私服密码</password>
</server>
</servers>
注意:密码写在
settings.xml里是明文。这份文件不进 git 仓库,也别拷到别人机器上。Nexus 支持settings-security.xml做加密,也可以给每个人建独立的 Nexus 账号,各自配各自的 server。
pom 里声明发到哪里:
<distributionManagement>
<repository>
<id>nexus-releases</id>
<name>Nexus Releases</name>
<url>http://localhost:8088/repository/maven-releases/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<name>Nexus Snapshots</name>
<url>http://localhost:8088/repository/maven-snapshots/</url>
</snapshotRepository>
</distributionManagement>
然后执行:
mvn deploy
Maven 会按版本号自动分流:带
-SNAPSHOT的发到snapshotRepository,正式版本发到repository。所以两个私有仓库都要建,缺一个 deploy 就会报错。
10. 实战问题
10.1 让 B 项目引用 A 项目
A 项目在本地,B 项目要用它,走正常依赖引用是找不到的。把 A 装进本地仓库,B 里再按坐标引:
<dependency>
<groupId>com.example</groupId>
<artifactId>A</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
A 如果是 Spring Boot 项目,默认打出来的是 Spring Boot 专用的 jar,包一层 BOOT-INF 目录,普通 Maven 项目当依赖引入时读不到里面的 class。要先把它变成普通 jar,在 A 的 pom 里加:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>2.4.3</version>
<configuration>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
</resource>
</resources>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>
spring-boot-maven-plugin 的 skip 关掉重打包,不再生成带 BOOT-INF 的可执行 jar
maven-resources-plugin 指定从 src/main/resources 打包,绕开 Boot 的资源目录
maven-surefire-plugin 跳过测试阶段,避免测试依赖拖垮 B 项目
提示:这套配置会让 A 失去 Spring Boot 的启动器,本地开发 A 自己时要用
spring-boot:run,不能java -jar。另外务必确认 A 和 B 用的是同一个settings.xml和同一个本地仓库——本地仓库路径不一致,B 就会去私服找一个根本没 deploy 过的版本,然后报找不到构件。
10.2 package、install、deploy 的区别
package 编译、测试、打包,产物在
target下,出了这个目录就没了
install package 之后,把产物装进本地仓库,其他本地项目能引用
deploy install 之后,再把产物推到远程私服,团队其他人能拉
执行顺序是逐级包含的:install 会自动先跑 package,deploy 会自动先跑 install。
10.3 pom 打包和 jar 打包的区别
pom 类型 只写依赖配置和构建配置,不含源码,一般给多模块项目的父工程用
jar 类型 默认类型,包含编译后的 class 和资源,可以被引用也可以执行
父工程打包成 pom,子模块写 <parent> 指过来,版本、插件、依赖管理全部由父工程统一下发。多模块项目的根目录那个项目,<packaging> 就是 pom。
💡 补充几个原文没写的:
跳过测试别去改 pom
临时不想跑测试,加命令行参数mvn package -DskipTests,比在 pom 里锁死 surefire 2.5 版本干净。改 pom 那个写法是为了一次配长期生效,代价是整个团队都跳测试
查依赖冲突用命令行
mvn dependency:tree -Dverbose,被冲突掉的依赖会标omitted for conflict,比在 IDEA 里一层层翻快
快照不更新
改了别人的 SNAPSHOT 依赖但拉的还是旧的,mvn clean install -U,-U强制检查远程快照
私服挂了不至于全线停摆
<mirrorOf>用external:*而不是*,本地已缓存的依赖不依赖网络
Maven 和 JDK 版本要一起锁
内置插件版本跟着 Maven 版本走,团队里 Maven 版本不统一时,在pluginManagement里锁死关键插件版本是最直接的解法
🔥 一句话总结:Maven 把依赖和构建都收进一个 pom 撑着,本地仓库当缓存、settings.xml 管镜像和私服,私服跑起来之后下载改
<mirror>、上传改<distributionManagement>加mvn deploy,剩下的坑基本都出在版本和路径没统一上。
