一、Maven 基础
坦率的说我不太喜欢maven 这个工具,觉得它有点过度设计,我也是被潮流挟裹着学习,写这篇文章主要是自己作个总结,顺便也帮助下他人。
Maven 的简介、安装、项目结构、HelloWorld,与 IDE 集成等请参考其他教程,本教程主要讲解 Maven 的基本概念。
Maven 工程与 pom.xml 文件
一个软件项目是一个工程,如果项目很庞大,可以将项目拆分成多个模块,每一个模块又是一个工程,这就是父子工程结构。Maven 工程的描述文件是 pom.xml 文件,pom.xml 文件位于 Maven 工程文件夹根目录下,Maven 工程与 pom.xml 文件是一一对应的。
最简单的 pom.xml
打开任何一个 Maven 工程的 pom.xml,都可以看到有类似
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.7</version>
这样的三行配置,这三行是必需有的,其意义如下:
- groupId: 组织的标识,一般是公司或组织域名
- artifactId:项目的标识,一般是工程名称
- version:项目的版本号
Maven 通过 groupId、artifactId、version 这三个属性就能唯一确定一个 Maven 项目,这三个属性称为 Maven 工程的坐标。常见的开源项目的坐标可以在 mvnrepository 搜索。
接下来会看到
<packaging>jar</packaging>
packaging:指定项目的打包方式常见的有
- jar: 指定工程打包成 jar 文件。
- war: 指定工程打包成 war 文件。
- pom: 指定这是一个 Maven 父工程,可以被子工程继承。
- maven-plugin: 指定工程打包成 Maven 插件。
当省略该行时默认为 jar。
引入依赖
如果你的 Maven 项目需要引用第三方 jar 包,那么需要在 pom.xml 中引入该 jar 包的坐标,引用自定义的 maven 工程也需要在 pom.xml 中引入该工程的坐标, 这称为引入依赖。
例如在<dependencies>
标签下加入如下配置后
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>
即可为你的项目引入 spring-context-4.1.8.RELEASE.jar 依赖。
<dependencies>
标签下可以包含一个或多个 dependency 元素,以声明一个或多个项目依赖。
依赖传递
引入spring-core-4.1.8.RELEASE.jar
的依赖后,Maven 引入的 jar 包不仅仅只有spring-context-4.1.8.RELEASE.jar
,还会有spring-context-4.1.8.RELEASE.jar
依赖的spring-core-4.1.8.RELEASE.jar
,还会有spring-core-4.1.8.RELEASE.jar
依赖的 commons-logging-1.2.jar
,依赖关系不断传递,直至没有依赖为止。
有时我们的项目会出现模块循环依赖的情况 ,于是在构建时 Maven 就会提示失败 。这是因为 Maven 编译项目前要分析依赖关系,确定模块的编译顺序,如果出现循环依赖 Maven 就无法确定哪个模块该先编译了,所以我们需要谨慎设计模块的依赖关系。
依赖排除
依赖传递有时会造成 jar 包冲突,这时我们需要排除掉不需要的 jar 包依赖。
例如排除 commons-logging 的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.8.RELEASE</version>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
属性
pom.xml 中<properties>
标签下可以定义属性,Maven 也有许多内置属性,属性可以在其他地方用${属性名}
进行引用。
属性示例
<properties>
<!-- 定义属性 -->
<springframework.version>4.1.8.RELEASE</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<!-- 引用属性 -->
<version>${springframework.version}</version>
</dependency>
</dependencies>
内置属性例如${basedir}
表示项目根目录,即包含 pom.xml 文件的目录,具体内置属性请参阅其他资料。
二、生命周期、阶段与插件
生命周期
来自软件工程的概念,Maven 对软件构建过程进行抽象。包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和文档生成等几乎所有的构建步骤。Maven 有三个相互独立的生命周期,分别是 clean、default 和 site。
- clean 构建之前进行一些清理工作。
- default 是构建的核心部分,编译,测试,打包,部署等等,
- site 生成项目报告,站点,发布站点,站点的文档。
阶段(phase)
每个生命周期又包含了多个阶段,这些阶段在执行的时候是有固定顺序的,后面的阶段一定要等前面的阶段执行完成后才能被执行。
三种生命周期与阶段的顺序
clean
- pre-clean
- clean 清理项目
- post-clean
default
- validate 验证项目是否正确,以及所有为了完整构建必要的信息是否可用
- generate-sources 生成所有需要包含在编译过程中的源代码
- process-sources 处理源代码,比如过滤一些值
- generate-resources 生成所有需要包含在打包过程中的资源文件
- process-resources 复制并处理资源文件至目标目录,准备打包
- compile 编译项目的源代码
- process-classes 后处理编译生成的文件,例如对 Java 类进行字节码增强(bytecode enhancement)
- generate-test-sources 生成所有包含在测试编译过程中的测试源码
- process-test-sources 处理测试源码,比如过滤一些值
- generate-test-resources 生成测试需要的资源文件
- process-test-resources 复制并处理测试资源文件至测试目标目录
- test-compile 编译测试源码至测试目标目录
- test 使用合适的单元测试框架运行测试。这些测试应该不需要代码被打包或发布
- prepare-package 在真正的打包之前,执行一些准备打包必要的操作。这通常会产生一个包的展开的处理过的版本(将会在 Maven 2.1+中实现)
- package 将编译好的代码打包成可分发的格式,如 JAR,WAR,或者 EAR
- pre-integration-test 执行一些在集成测试运行之前需要的动作。如建立集成测试需要的环境
- integration-test 如果有必要的话,处理包并发布至集成测试可以运行的环境
- post-integration-test 执行一些在集成测试运行之后需要的动作。如清理集成测试环境。
- verify 执行所有检查,验证包是有效的,符合质量规范
- install 安装包至本地仓库,以备本地的其它项目作为依赖使用
- deploy 复制最终的包至远程仓库,共享给其它开发人员和项目(通常和一次正式的发布相关)
site
- pre-site
- site :生成项目的站点文档;
- post-site
- site-deploy :发布生成的站点文档
执行生命周期阶段
maven 命令总会对应于一个特定的阶段,比如运行mvn clean
就是执行 clean 生命周期的 clean 阶段,mvn package
就是执行 defalut 生命周期的 package 阶段。
生命周期与插件
Maven 的生命周期是抽象的,阶段也是抽象的,实际需要插件来执行任务,Maven 本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成。一个插件通常可以完成多个任务,每一个任务就叫做插件的一个目标(goal)。
执行插件目标
- 通过将插件的 goal 绑定到生命周期的具体阶段,再执行 maven 的生命周期阶段,如
mvn package
。 - 直接在命令行以
mvn <插件名称|前缀>:<目标> [-D参数名=参数值 ...]
的形式执行插件目标,例如mvn archetype:generate
就表示调用 maven-archetype-plugin 的 generate 目标,这种方式与生命周期无关。
将插件的目标绑定到生命周期示例
<build>
<plugins>
<plugin>
<!-- 要绑定的插件 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
<executions>
<execution>
<id>attach-source</id>
<!-- 要绑定到的生命周期的阶段 -->
<phase>package</phase>
<goals>
<!-- 要绑定的插件的目标 -->
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Maven 内置插件
Maven 已经默认将很多核心插件绑定到对应的阶段,用户几乎不用配置就能构建 Maven 项目。配置插件
插件的参数在<configuration>
标签下配置。
compiler 插件配置示例:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<!-- 该插件的整体配置,各个目标均使用该配置 -->
<configuration>
<!-- 源代码使用的JDK版本 -->
<source>1.8</source>
<!-- 生成的目标class文件的编译版本 -->
<target>1.8</target>
<!-- 字符集编码 -->
<encoding>UTF-8</encoding>
<!-- 跳过测试 -->
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
常用插件
常用的 maven 插件可以在插件仓库中搜索坐标。
maven-antrun-plugin
- antrun:run 能让用户在 Maven 项目中运行 Ant 任务
maven-compiler-plugin
- compiler:compile 编译位于 src/main/java/目录下的源码
- compiler:testCompile 编译位于 src/test/java/目录下的测试源码
maven-source-plugin
- source:jar 打包源代码
maven-dependency-plugin
- dependency:list 能够列出项目最终解析到的依赖列表
- dependency:tree 能进一步的描绘项目依赖树
- dependency:analyze 可以告诉你项目依赖潜在的问题
- dependency:copy-dependencies 能将项目依赖从本地 Maven 仓库复制到某个特定的文件夹下面
exec-maven-plugin
- exec:exec 运行一个 Maven 外部的程序,该插件还允许你配置相关的程序运行参数。
- exec:java 该目标要求你提供一个 mainClass 参数,然后它能够利用当前项目的依赖作为 classpath,在同一个 JVM 中运行该 mainClass。
自定义插件
- 创建 maven 项目,修改 pom.xml 中的打包方式
<packaging>maven-plugin</packaging>
- 引入 maven-plugin-api
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-plugin-api</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.maven.plugin-tools</groupId>
<artifactId>maven-plugin-annotations</artifactId>
<version>3.5.0</version>
<scope>provided</scope>
</dependency>
- 编写插件目标类 Mojo
/**
* 继承AbstractMojo、实现execute()方法
* 注解设置目标名称,默认阶段。
*/
@Mojo(name = "demoGoal",defaultPhase = LifecyclePhase.PACKAGE)
public class DemoGoalMojo extends AbstractMojo {
// 插件的配置参数。
@Parameter
private String msg;
@Parameter
private List<String> options;
public void execute() throws MojoExecutionException,MojoFailureException{
String info = String.format("外部传递来的参数 msg: [ %s ]", msg);
getLog().info(info);
System.out.println("options:"+ options);
}
}
- 使用
mvn install
命令安装插件 - 在其他项目中使用插件
<build>
<plugins>
<plugin>
<groupId>sample.plugin</groupId>
<artifactId>hello-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
</plugin>
</plugins>
</build>
三、项目继承
一个大型项目一般有多个子项目,为了消除重复或方便统一管理版本号、依赖、属性等,可以把相同的配置抽取出来放入父项目,子项目再继承父项目就可以使用父项目中的配置,形成层次化的项目关系。
zookeeper 父子项目示例
- 父项目,打包类型必须为 POM,不需要
src/main/java
、src/test/java
等目录。
<groupId>org.apache.zookeeper</groupId>
<artifactId>parent</artifactId>
<packaging>pom</packaging>
<version>3.6.0-SNAPSHOT</version>
- 子项目一
<parent>
<!-- parent的坐标 -->
<groupId>org.apache.zookeeper</groupId>
<artifactId>parent</artifactId>
<version>3.6.0-SNAPSHOT</version>
<!-- 指明parent的目录,该配置可以不需要 -->
<relativePath>..</relativePath>
</parent>
<!-- 子项目坐标,可以不声明groupId和version,默认继承parent的 -->
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<packaging>jar</packaging>
- 子项目二
<parent>
<!-- parent的坐标 -->
<groupId>org.apache.zookeeper</groupId>
<artifactId>parent</artifactId>
<version>3.6.0-SNAPSHOT</version>
<!-- 指明parent的目录,该配置可以不需要 -->
<relativePath>..</relativePath>
</parent>
<!-- 子项目坐标,可以不声明groupId和version,默认继承parent的 -->
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper-client</artifactId>
<packaging>pom</packaging>
- 孙项目
<parent>
<!-- parent是子项目二 -->
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper-client</artifactId>
<version>3.6.0-SNAPSHOT</version>
<!-- 指明parent的目录,该配置可以不需要 -->
<relativePath>..</relativePath>
</parent>
<!-- 子项目坐标,版本号继承parent的 -->
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper-client-c</artifactId>
<packaging>jar</packaging>
四、多项目合成
一个大型项目一般有多个子项目,也叫做多模块项目,或者合成项目,合成机制允许我们在父项目中执行生命周期指令后该指令能传递给每一个子项目。合成的目的是为了快速构建项目,继承是为了消除重复或方便统一管理,合成只需在父项目中配置。
zookeeper 项目合成示例:
<groupId>org.apache.zookeeper</groupId>
<artifactId>parent</artifactId>
<packaging>pom</packaging>
<version>3.6.0-SNAPSHOT</version>
<!-- 指定需要合成的子项目 -->
<modules>
<module>zookeeper-docs</module>
<module>zookeeper-jute</module>
<module>zookeeper-server</module>
<module>zookeeper-metrics-providers</module>
<module>zookeeper-client</module>
<module>zookeeper-recipes</module>
<module>zookeeper-assembly</module>
</modules>
当在 parent 项目根目录下执行mvn install
后,命名会传递给所有子项目,相当于在所有子项目下都执行了mvn install
指令。
五、仓库与镜像
本地仓库
是本地计算机上的一个文件夹,本地仓库会缓存远程仓库的构件。windows 默认是 C:\Users\用户名\.m2\repository
目录,可以进入C:\Users\用户名\.m2\settings.xml
文件中修改位置
<localRepository>/path/to/local/repo</localRepository>
远程仓库
顾名思义就是网络上的仓库,网上一些教程把远程仓库分类成什么中央仓库、私服、其它公共仓库的,其实他们本质上一样。Maven 社区提供的远程仓库叫中央仓库,软件开发组织在内部网络搭建的私有 Maven 仓库服务器这叫私服,某些有实力的组织将自己的私有仓库向互联网开放这叫其它公共仓库。
为了加快构件的下载速度或方便管理私有项目,软件开发组织一般都会搭建 Maven 私服,私有项目的构件可以部署到私服上供内部其他项目引用。没有私服的时候也可以使用阿里云仓库 http://maven.aliyun.com/nexus/content/groups/public/
。
仓库检索优先级规则:本地仓库优先级最高,当在本地仓库中找不到构件时 Maven 会去远程仓库中找,远程仓库依据配置文件中的<repository>
标签中的顺序形成一个仓库列表,而中央仓库排在列表的最后面,所以中央仓库的优先级最低。
远程仓库配置示例
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>alibaba</id>
<name>ali Milestones</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
镜像
镜像表示两个远程仓库之间的关系,如果仓库 X 可以提供仓库 Y 存储的内容,那么就可以认为 X 是 Y 的一个镜像。由于地理位置的原因,国内镜像服务器的速度一般比中央仓库快,我们可以通过 settings.xml 配置文件设置镜像关系。镜像相当于一个拦截器,它会拦截去往被镜像的远程服务器的相关请求,把请求重定向到镜像服务器上。由于镜像仓库完全屏蔽了被镜像仓库,当镜像仓库不稳定或者停止服务的时候,Maven 无法访问被镜像仓库,因此无法下载构件。实际工作中,镜像常见的用法是让私服充当其他远程仓库的镜像,这样对远程仓库的访问就导向到了私服上。如果镜像仓库需要登录认证,则需要配置 setting.xml 中的<server>
标签。
配置镜像示例
<!-- 阿里云仓库充当central仓库的镜像 -->
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central/</url>
</mirror>
本教程的目的是快速上手,更详细的配置请参阅其他资料。