序言
不知道你是否意识到,构建(build)是每一位程序员每天都在做的工作。
除了编写源代码外,我们每天有相当一部分的时间花在了编译、运行单元测试、生成文档、打包和部署等繁琐且不起眼的工作上,这就是构建。
构建必不可少,那么能否减少构建时间呢?
为了将我们从这个沼泽中解脱出来,使得软件的构建可以像全自动流水线一样,只需要一条简单的命令,让所有繁琐的步骤都能够自动完成,快速得到结果,便有人发明了 Maven。
什么是 Maven?
Maven 翻译为“专家”,“内行”,它是 Apache 下的一个纯 Java 开发的开源项目,是一个项目管理工具。
Maven 主要服务于基于 Java 平台的项目构建、依赖管理和项目信息管理。
Maven 作为一个异常强大的构建工具,能够帮助我们自动化构建过程,从清理、编译、测试到生成报告,再到打包和部署。
当然,Maven 的优点不仅仅只有这个,它还能帮助我们标准化构建过程。
比如说,在 Maven 之前,10 个项目可能有 10 种构建方式。但在有了 Maven 后,所有项目的构建命令都是简单一致的,这极大地避免了不必要的学习成本,有利于促进项目团队的协作开发,并且,它还跨平台哦!
嘻嘻,Maven 的优点可不仅仅于此!
那么,下面来了解一下吧!
基本概念
Maven 坐标
什么是 Maven 坐标?
关于坐标,大家最熟悉的定义应该来自于平面几何,平面中任何一个坐标都能够唯一标识该平面的一点。
在实际生活中,我们也可以将地址看成是一种坐标。省、市、区、街道等一系列信息可以唯一标识城市中的任一地址,快递公司正是基于这样一种坐标进行日常工作的。
对应于平面中的点和城市中的地址,Maven 的世界中拥有数量非常巨大的构件,也就是平时用的一些jar、war
等文件。
在 Maven 为这些构件引入坐标概念之前,我们无法使用任何一种方式来唯一标识这些所有构件。
因此,当需要用到 Spring 依赖,Mybatis 依赖的时候,需要去各自网站下载。
但是,由于不同网站的风格迥异,开发人员大量的时间用在了搜索、浏览网页等工作方面,这是十分浪费时间的行为。
对人而言,时间就是金钱啊!
为此,Maven 定义了这样一组规则:世界上任何一个构件都可以使用 Maven 坐标来进行唯一标识,Maven 坐标的元素主要包括groupId、artifactId、version
。
现在,只要我们提供正确的坐标元素,Maven 就能找到对应的构件。
依赖元素详解
Maven 坐标为各种构件引入了秩序,任何一个构件都必须明确定义自己的坐标,而一组 Maven 坐标是通过一些元素定义的,比如常见的groupId
、artifactId
、version
。
示例坐标如下:
1 | <groupId>org.springframework</groupId> |
元素说明如下:
groupId
:定义当前 Maven 项目隶属的实际项目。其表示方式与 Java 包名类似,通常与域名反向一一对应,如org.apache.struts
代表apache
公司的struts
项目artifactId
:定义实际项目中的一个 Maven 项目(模块)version
:定义 Maven 项目当前所处的版本(创建时默认为1.0 SNAPSHOT
)scope
:定义依赖的范围exclusions
:定义用来排除传递性依赖
scope——依赖作用范围
虽然 Maven 引入坐标管理了 Java 项目的依赖,但由于项目中的代码存在编译阶段、测试阶段、运行阶段,不同阶段会使用不同位置的文件(即类文件*.class
存放的路径),引入的依赖并不一定需要全部作用每个阶段,所以依赖需要选择性在某些阶段生效,要达到此效果就需要指定依赖的生效范围了。
举个例子:1
2
3
4
5
6
7
8
9
10
11
12<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.5.RELEASE</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
在引入上面的依赖项目中,编译项目主代码的时候需要用到 spring-core,该文件以依赖的方式被引人到 classpath 中。
其次,Maven 在编译和执行测试的时候会使用另外一套 classpath。上面的 junit 就是一个很好的例子,该文件也以依赖的方式引人到测试使用的 classpath 中,不同的是这里的依赖范围是 test。
最后,实际运行 Maven 项目的时候,又会使用一套 classpath,上面的 spring-core 需要在该 classpath中,而 junit 则不需要。
依赖范围就是用来控制依赖与这三种 classpath (编译 classpath、测试 classpath、运行 classpath)的关系。
在 Maven 中,存在以下几种依赖范围:
compile
: 编译依赖范围(默认依赖范围),对于编译、测试、运行三种 classpath 均有效test
:测试依赖范围,只对于测试 classpath 有效,在编译主代码或者运行项目的使用时将无法使用此类依赖。比如 junit,它只有在编译测试代码及运行测试的时候才需要provided
:已提供依赖范围,对于编译和测试 classpath 有效,但在运行时无效。比如servlet-api
,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供(Tomcat 内置servlet-api
),就不需要 Maven 重复地引入一遍runtime
:运行时依赖范围,对于测试和运行 classpath 有效,但在编译主代码时无效。比如 JDBC 驱动实现,项目主代码的编译只需要 JDK 提供的 JDBC 接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体 JDBC 驱动system
:系统依赖范围。该依赖与三种 classpath 的关系,和provided
依赖范围完全一致。但是,使用system
范围的依赖时必须通过systemPath
元素显式地指定依赖文件的路径。由于此类依赖不是通过 Maven 仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。systemPath元素可以引用环境变量,一般在使用第三方依赖包的时候使用该依赖;1
2
3
4
5
6
7<dependency>
<groupId>cn.lovike</groupId>
<artifactId>third-sdk</artifactId>
<scope>system</scope>
<version>1.0</version>
<systemPath>${project.basedir}/src/main/resources/lib/third-sdk-1.0.0.jar</systemPath>
</dependency>import
:导人依赖范围。该依赖范围不会对三种 classpath 产生实际的影响。该范围的依赖只在dependencyManagement
元素下才有效果,使用该范围的依赖通常指向一个 POM,作用是将目标 POM 中的dependencyManagement
配置导人并合并到当前 POM 的dependencyManagement
元素中。例如想要在 B 模块中使用与 A 模块完全一样的dependencyManagement
配置,除了复制配置或者继承这两种方式之外,还可以使用import
范围依赖将这一配置导入
exclusions——排除依赖
传递性依赖会给项目隐式地引人很多依赖,这极大地简化了项目依赖的管理,但是有些时候这种特性也会带来问题。
例如,当前项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的 SNAPSHOT 版本,那么这个 SNAPSHOT 就会成为当前项目的传递性依赖,而 SNAPSHOT 的不稳定性会直接影响到当前的项目。这时就需要排除掉该SNAPSHOT,并且在当前项目中声明该类库的某个正式发布的版本。
还有一些情况,你可能也想要替换某个传递性依赖,比如 Sun JTA API,Hibernate依赖于这个 JAR,但是由于版权的因素,该类库不在中央仓库中,而 Apache Geronimo 项目有一个对应的实现。这时你就可以排除 Sun JAT API,再声明 Geronimo 的 JTA API 实现。
为了解决这些问题,我们可以使用exclusions
来排除相应的依赖:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
<exclusions>
<exclusion>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka_2.11</artifactId>
</exclusion>
</exclusions>
</dependency>
比如上面就排除了spring-boot
自带的Kafka
依赖。
代码中使用exclusions
元素声明排除依赖,该元素可以包含一个或者多个exclusion
子元素,因此可以排除一个或者多个传递性依赖。
需要注意的是,声明exclusion
的时候只需要groupld
和artifactld
,而不需要version
元素,这是因为只需要groupId
和artifactld
就能唯一定位依赖图中的某个依赖。换句话说,Maven 解析后的依赖中,不可能出现groupId
和artifactld
相同,但是version
不同的两个依赖。
Maven 仓库
既然 Maven 为构件引入了坐标,那么,Maven 又是从哪里下载构件的呢?
答案很简单,Maven 内置了一个中央仓库的地址,该仓库包含了世界上大部分流行的开源项目构件,Maven 会在需要的时候去那里下载。
在我们开发自己项目的时候,也需要为其定义适当的坐标,这是 Maven 强制要求的。
在这个基础上,其他 Maven 项目才能引用该项目生成的构件。
什么是 Maven 仓库?
前面提到过,构件从 Maven 仓库下载,那么 Maven 仓库是什么呢?
在一台工作站上,可能会有几十个 Maven 项目,所有项目都使用maven-complier-pluin
,这些项目中的大部分都用到了log4j
,有一小部分用到了Spring Framework
,还有一小部分用到了Mybatis
。
在每个有需要的项目中都放置了一份重复的log4j
或者Mybatis
还是Spring Framework
的jar包,显然不是最好的解决方案,不仅造成了磁盘空间的浪费,而且也难于统一管理。
得益于坐标机制,任何 Maven 项目使用任何一个构件的方式都是完全相同的。
在此基础上,Maven 可以在某个位置统一存储所有 Maven 共享的构件,这个统一的位置就是仓库。
实际的 Maven 项目将不再各自存储其依赖文件,它们只需要声明这些依赖的坐标,在需要的时候,Maven 会自动根据坐标找到仓库中的构件,并使用它们。
为了实现重用,项目构建完毕后生成的构件也可以安装或者部署到仓库中,供其他项目使用。
Maven 仓库的种类
对于 Maven 来说,仓库只分为两类:
- 本地仓库
- 远程仓库
当 Maven 根据坐标寻找构件的时候,它首先会查看本地仓库,若本地仓库存在此构件,则直接使用;若本地仓库不存在此构件,或者需要查看更新的构件版本,Maven 将去远程仓库查找,发现需要的构件之后,下载到本地仓库再使用。
若本地仓库和远程仓库都没有需要的构件,Maven 就会报错。
在这个最基本的分类上,还有一些特殊的远程仓库,比如
- 中央仓库
- 私服
- 其他远程仓库
中央仓库是 Maven 核心自带的远程仓库,它包含了绝大部分开源的构件。
在默认配置下,当本地仓库没有 Maven 需要的构件的时候,它就会尝试从中央仓库下载。
私服是另一种特殊的远程仓库,为了节省带宽和时间,可以在局域网内架设一个私有的仓库服务器,用其代理所有外部的远程仓库,内部的项目还能部署到私服上供其他项目使用。
本地仓库
一般来说,在 Maven 项目目录下,是没有诸如lib/
这样用来存放依赖文件的目录。当 Maven 执行编译或测试时,若需要使用依赖文件,它总是基于坐标使用本地仓库的依赖文件。
默认情况下,不管是在 Windows 还是 Linux上,每个用户在自己的家目录下都有一个路径名为.m2/repository/
的仓库目录。
有时候,因为某些原因(例如 C 盘空间不够),用户会想要自定义本地仓库的目录地址,这时,可以在相关编辑文件更改它(在后文环境配置中我们将会更改它)。
一个构件只有在本地仓库中之后,才能由其他 Maven 项目使用,那么构件如何进入到本地仓库呢?
最常见的是依赖 Maven 从远程仓库下载到本地仓库中。
当然,另一种常见的情况是:将本地项目的构件安装到 Maven 仓库中。
例如,本地有两个项目 A 和 B,两者都无法从远程仓库获得,而同时 A 又依赖于 B,为了能构件 A,B 就必须首先得以构建并安装到本地仓库中。
在某个项目中执行mvn clean install
命令,就能看到如下输出:
Install 插件的install
目标将项目的构件输出文件安装到本地仓库。
远程仓库
安装好 Maven 之后,若不执行任何 Maven 命令,本地仓库目录是不存在的。
当用户输入第一条 Maven 命令之后,Maven 才会创建本地仓库,然后根据配置和需要,从远程仓库下载构件至本地仓库。
这好比藏书。
例如,我想要读《挪威的森林》,会检查自己的书房是否已经收藏了这本书,若没有发现这本书,于是就跑去书店买一本回来,放在书房里。
可能我有一天又想读一本英文版的《百年孤独》,而书房里只有中文版,于是又去书店找,可发现书店也没有,好在还有网上书店,于是从 TB 买了一本,几天后我收到了这本书,又放到了自己的书房。
本地仓库就好比书房,我需要读书的时候先从书房找,相应的,Maven 需要构件的时候先从本地仓库找。
远程仓库就好比书店(包括实体书店、网上书店等),当我无法从自己的书房找到需要的书的时候,就会从书店购买后放到书房里。
当 Maven 无法从本地仓库找到需要的构件的时候,就会从远程仓库下载构件至本地仓库。
一般地,对于每个人来说,书房只有一个,但外面的书店很多,网上书店也很多。类似地,对于 Maven 而言,每个用户只有一个本地仓库,但可以配置访问很多远程仓库。
中央仓库
由于最原始的本地仓库是空的,Maven 必须知道至少一个可用的远程仓库,才能在执行 Maven 命令的时候下载到需要的构件。
中央仓库就是这样一个默认的远程仓库,Maven 的安装文件自带了中央仓库的配置。
由于中央仓库设在国外,因此下载构件的速度很慢,所以在环境配置中我们将会更改它。
私服
私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的 Maven 用户使用。
当 Maven 需要下载构件的时候,它从私服请求,若私服上不存在该构件,则从外部的远程仓库下载,缓存在私服上之后,再为 Maven 的下载请求提供服务。
此外,一些无法从外部仓库下载到的构件也能从本地上传到私服上供大家使用,如下图:
私服可以帮助你:
- ① 节省自己的外网带宽;
- ② 加速 Maven 构建;
- ③ 部署第三方构件;
- ④ 提高稳定性,增强控制;
- ⑤ 降低中央仓库的负荷。
一般,公司内部都会部署一个 Maven 私服来放置自身研发的项目构件。
Maven 的依赖管理
依赖:一个 Java 项目可能要使用一些第三方的 jar 包才可以运行,那么我们说该 Java 项目依赖了这些第三方的 jar 包。
在传统项目的依赖管理中,缺点很明显,其所依赖的 jar 包完全靠人进行,需要从不同网站下载 jar 包添加到项目中,有些还找不到,费事费力,而且没有对 jar 包版本的统一管理,容易导致版本冲突。
Maven 项目管理所依赖的 jar 包不需要手动地向项目中添加 jar 包,而只需在pom.xml
文件(Maven工程的配置文件)添加 jar 包的坐标,就会自动从 Maven 仓库下载 jar 包,并自动导入项目中。
比如说,我们在需要 Spring MVC 框架的 jar 包的时候,可以在pom.xml
文件加入以下配置:1
2
3
4
5
6
7<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.2.6.RELEASE</version>
</dependency>
</dependencies>
优点
- 通过
pom.xml
文件对jar
的版本可以进行统一管理,可以避免版本冲突 - 可以十分方便的从 Maven 仓库中下载 jar 包,不用再一个一个网站找了
Maven 生命周期及插件
在 Maven 出现之前,项目构建的生命周期就已经存在,软件开发人员每天都在对项目进行清理、编译、测试及部署。可以想象的是,虽然各种手工方式十分类似,但不可能完全一样;同样地,对于自动化脚本,大家也是各写各的,能满足自身需求即可,换个项目就的重头再来。
Maven 的生命周期就是为了对所有的构建过程进行抽象和统一。
Maven 从大量项目和构建工具中学习和反思,然后总结了一套高度完善的、易扩展的生命周期。
这个生命周期包括了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤,我们可以通过mvn
命令完成这些步骤。
换而言之,几乎所有项目的构建,都能映射到这样一个生命周期上。
Maven 的生命周期是抽象的,这意味着生命周期本身不做任何实际的工作,在 Maven 的设计中,实际的任务(如编译源代码)都交由插件来完成。
由于 Maven 的核心仅仅定义了抽象的生命周期,而把具体任务交给插件(插件以独立的构建形式存在)完成。因此,Maven 核心的分发包只有不到 3 MB 的大小,Maven 会在需要的时候下载并使用插件。
对于插件本身,为了能够复用代码,它往往能够完成多个任务。
例如maven-dependency-plugin
,它能够基于项目以来做很多事情,比如:
- 分析项目依赖,帮助找出潜在的无用依赖
- 列出项目的依赖树,帮助分析依赖来源
- 列出项目所有已解析的依赖
- 等等
为这样的功能编写一个独立的插件显然是不可取的,因为这些任务背后有很多可以复用的代码,因此,这些功能聚集在一个插件里,每个功能就是一个插件。
Maven 的生命周期与插件相互绑定,用以完成实际的构建任务。
具体而言,是生命周期的阶段与插件的目标相互绑定,以完成某个具体的构建任务。
例如项目编译这一任务,它对应了default
生命周期的compile
这一阶段,而maven-complier-plugin
这一插件的compile
目标能够完成该任务。
因此,将它们绑定,就能实现项目编译的目的。
简单来讲,不同的 Maven 命令会让不同的 Maven 插件去执行相应的操作以完成相应的功能。
Maven 项目工程约定目录
前面提到过 Maven 项目遵循一定的规范,因此所有的 Maven 项目都应统一目录结构,具体如下:
Maven 命令
Maven 将项目构建的过程进行标准化,每个阶段使用一个命令完成,下面为常见的几个 Maven 命令:
mvn clean
:清理命令,执行该命令会删除target
目录的内容。mvn compile
:编译命令,作用是将src/main/java
下的文件编译为class
文件输出到target
目录下。mvn test
:测试命令,会执行src/main/java
的单元测试类。mvn package
:打包命令,对 Java 工程打包为jar
包,对 Web 工程则打包为war
包。mvn install
:安装命令,将项目打包成jar
包或war
包发布到本地仓库。
注意哦
:compile-->test-->package-->install
命令生命周期按顺序执行,即test
命令执行时先执行compile
命令再执行test
,package
命令执行时compile-->test-->package
,部分很少使用的命令省略,请自行了解。
当然,Maven 的命令不仅仅于此,这些只是常用的。知道就可以了,比如 IDEA 默认支持 Maven 插件的图形化,可以直接通过图形点击运行即可,就不用再输命令了。
Maven 安装与配置
Mac 下的安装
mac 使用Homebrew
包管理工具安装即可,没有该工具需要先使用如下命令安装:1
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
之后使用Homebrew
命令直接安装 Maven 即可(不指定版本则默认获取最新版),环境变量将自动配置:
1 | brew install maven |
Windows 下安装
从官网下载:Maven官网
下载来解压开来:
解压后里面的目录:
环境变量的配置
新建系统变量:
之后去 Path 添加:
终端出现以下信息则配置成功:
Maven 配置文件的修改
指定本地仓库位置
将本地仓库(localRepository)位置指定(虽然不指定也行,但默认配置会占用 C 盘空间,mac 系统可以不这么做,还是看你心情),找到安装的 maven 的conf
文件夹中的setting.xml
文件(mac 的位置略有不同),然后打开编辑:
这里我们将仓库位置设在了 D 盘,你可以根据个人情况自行设置,更改完成后我们就可以将 C 盘的.m2
文件删除以节省点硬盘空间。
修改中央仓库下载地址
Maven 安装完成后都需要修改 maven 中央仓库的镜像,不然下载速度会很慢的,因为默认的 Maven 中央仓库设在国外。
因此,一般将其它修改为阿里云的仓库,加入下面代码:1
2
3
4
5
6<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</mirror>
加入位置的图示:
查找 jar 包的 Maven 坐标网站
查找 jar 包坐标网址有很多,这里列举几个常用的:
- mvnrepository.com(基本用这个)
- search.maven.org(界面简约)
- maven.aliyun.com(阿里云镜像,访问速度快)
其他
有时候 Jar 包可能从中央仓库找不到(如 Oracle 7 的包),这时候就需要手动安装了:1
mvn install:install-file -Dmaven.repo.local=/Users/lovike/repository -DgroupId=com.oracle -DartifactId=ojdbc7 -Dversion=12.1.0.2 -Dpackaging=jar -Dfile=ojdbc7-12.1.0.2.jar
其中,不同参数分别代表:
-Dmaven.repo.local
:代表安装的本地仓库路径-DgroupId
:项目所属组-DartifactId
:本项目具体模块-Dversion
:版本-Dpackaging
:代表打包方式-Dfile
:代表需要安装的包
参考
- 许晓斌. Maven 实战 [M]. 机械工业出版社,2010