共计 77712 个字符,预计需要花费 195 分钟才能阅读完成。
Maven
一.Maven 简介
1.1 何为 maven
Maven 可翻译为 ” 知识的积累 ” or” 专家 ”, 是一款成功的开源跨平台的项目管理工具, 无论小型的开源类库项目, 还是大型的企业级应用; 无论传统的瀑布式开发, 还是流行的敏捷模式,Maven 都能大显身手.
1.1.1 构建工具
我们一直在不停的寻找避免重复的方法, 设计的重复, 编码的重复, 文档的重复, 当然还有构建的重复.Maven 最大化的消除了构建的重复, 抽象了构建生命周期, 并且为绝大部分的构建任务提供了已实现的插件, 我们不需要再定义过程, 甚至不需要去实现这些过程中的一些任务, 只需要遵循 Maven 中的约定.
同时 Maven 帮助我们标准化构建过程, 以前十个项目可能有十种构建方式, 有了 Maven 后, 所有项目的构建命令都是一直且简单的. 因此 Maven 作为一个构建工具:
- 可以帮我们自动化构建,
- 可以帮我们抽象构建过程
- 提供构建任务是实现
- 跨平台
- 对外提供一直的操作接口
1.1.2 不仅仅是构建工具
Maven 不仅是构建工具, 还是一个依赖管理工具和项目信息管理工具, 提供中央仓库来帮忙我们自动下载构建, 通过引入一套经纬机制来系统准确的定位每一个构建(artifact).
1.1.3 Maven
在 Maven 之前, 有过程式的 Make 和 Ant, 开发者需要显示的指定每一个目标, 以及完成该目标所需要执行的任务. 针对每一个项目, 开发者都需要重新编写这一过程, 而其中就隐含着大量重复.
而 Maven 是声明式的, 项目构建过程和过程各个阶段所需的工作都由插件实现, 并且大部分插件都是现成的, 开发者只需要声明项目的基本元素,Maven 就执行内置的, 完整的构建过程.
二.Maven 的使用
2.1 pom 文件
Maven 项目的核心是 pom.xml,POM(Project Object Model, 项目对象模型)定义了项目的基本信息, 用于描述项目如何构建, 声明项目依赖等.
<modelVersion>4.0.0</modelVersion>:modelVersion 指定了当前 Pom 模型的版本, 固定为 4.0.0
<groupId>com.lsy</groupId>:groupId 定义了项目属于哪个组, 这个组往往和项目所在的组织和公司相关
<artifactId>hello-world</artifactId>:artifactId 定义了当前 Maven 项目在组中唯一的 ID
<version>1.0-SNAPSHOT</version>:version 指定了 Hello World 项目当前的版本, 其中 SNAPSHOT 意为快照, 说明该项目还处于开发中
<name>Maven Hello World Project</name>:name 元素声明了一个对于用户更为友好的项目名称. 非必须
当运行 mvn clean compile 命令:clean 告诉 Maven 清理输出目录 target/,compile 告诉 Maven 编译项目主代码, 从输出中看到 Maven 首先执行 clean:clean 任务, 删除 target/ 目录(默认情况下,Maven 构建的所有输出都在 target 目录中, 接着执行 resources:resources 任务(未定义项目资源), 最后执行 compiler:compile 任务, 将项目主代码编译至 target/classes 目录
其中 clean:clean,resources:resources 和 compiler:compile 对应了 Maven 插件及插件目标, 比如 clean:clean 是 clean 插件的 clean 目标,compiler:compile 是 compiler 插件的 compile 目标
2.2 测试
首先添加 Junit 依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
<dependencies>
其中 scope 元素为依赖范围, 若依赖范围为 test 则表示该依赖只对测试有效, 如果不声明依赖范围, 则默认是 compile, 表示该依赖对主代码和测试代码均有效
当运行 mvn clean test 命令,Maven 实际执行的不止 test 任务, 而是 clean:clean,resources:resources,compiler:compile,resources:testResources 以及 compiler:testCompile, 即在执行测试之前, 会先自动执行项目资源处理, 主代码编译, 测试资源处理, 测试代码编译等工作, 这是 Maven 生命周期的一个特性.
由于 Maven 核心插件之一 compiler 插件默认支持 Java1.3, 因此需要配置插件支持 Java1.8
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8<source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
2.3 打包和运行
将项目进行编译, 测试之后, 下一个重要的步骤就是打包(package), 当没有指定打包类型时, 默认打包类型是 jar, 当执行 mvn clean package 命令,Maven 会在打包之前进行编译, 测试等操作, 之后通过 jar:jar 任务负责打包, 实际上就是 jar 插件的 jar 目标将项目主代码打包成一个 hello-world-1.0-SNAPSHOT.jar 的文件
当其他项目需要直接引用这个 jar 时, 接下来需要一个安装的步骤, 执行 mvn clean install, 此命令执行了安装任务 install:install, 将项目输出的 jar 安装到了 Maven 本地仓库中, 而构件只有被下载到本地仓库后, 才能由 Maven 项目使用.
2.4 使用 Archetype 生成项目骨架
mvn archetype:generate, 当运行插件时, 格式为:groupId:artifactId:version:goal, 默认使用最新的稳定版
三. 坐标和依赖
3.1 坐标
- Maven 的一大功能还是管理项目依赖, 为了能自动化解析任何一个 Java 构件,Maven 就必须将它们唯一标识, 这就依赖管理的底层基础 — 坐标
- 在实际生活中, 我们可以将地址看作是一种坐标, 省, 市, 区, 街道等一系列信息可以唯一标识城市中的任意居住地址, 对应在 Maven 的世界中拥有非常巨大的构件, 也就是平常使用的一些 jar,war 等文件
Maven 定义了这样一规则: 世界上任意一个构建都可以使用 Maven 坐标唯一标识,Maven 坐标的元素包括
- groupId: 定义当前 Maven 项目隶属公司的实际项目
- artifactId: 该元素定义实际项目中的一个 Maven 模块, 推荐做法使用实际项目名称为 artifact 的前缀, 便于寻找实际构建
- version: 该元素定义 Maven 项目当前所处的版本
- package: 该元素定义 Maven 项目的打包方式, 默认为 jar 包
- classifier: 该元素用来帮助定义构建输出一些附属构建, 附属构件与主构件对应
<groupId\>org.sonatype.nexus</groupId\> <artifactId\>nexus-indexer</artifactId\> <version\>2.0.0</version\> <packaging\>jar</packaging\> <!--> 1\. 以上 5 个元素,groupId,artifactId,version 是必须定义的,packaging 是可选的, 而 classifier 是不能直接定义的 2\. 项目构件的文件名格式:artifactId-version\[-classifier\].packaging
3.2 依赖
3.2.1 依赖的配置
项目要引用 Maven 中的构建, 就需要在 pom 文件中, 通过坐标来使用依赖, 在根元素 project 下的 dependencies 可以包含一个或多个 dependency 元素, 用以声明一个或多个项目依赖
- groupId,artifactId 和 version: 依赖的基本坐标, 对于任何一个依赖来说, 基本坐标是最重要的
- type: 依赖的类型, 对应项目坐标定义的 packaging, 一般不必声明, 默认为 jar
- scope: 依赖的范围
- optional: 标记依赖是否可选
- exclusions: 用来排除传递性依赖
3.2.2 依赖的范围
- Maven 项目在编译项目主代码时需要使用一套 classpath, 如编译项目主代码时需要使用 spring-core, 该文件以依赖的方式被引入到 classpath 中. 其次,Maven 在编译和执行测试的时候会使用另外一套 classpath, 依赖同样会引入到相应的 classpath 中, 最后在运行 Maven 项目时, 又会使用一套 classpath
依赖范围就是用来控制依赖与三种 classpath(编译 classpath, 测试 classpath, 运行 classpath)的关系
- compile: 编译依赖范围, 没有指定时, 为默认依赖范围. 使用此依赖范围的 Maven 依赖, 对于编译, 测试运行三种 classpath 都有效, 典型的例子为:spring-core, 在编译, 测试, 运行阶段都需要使用该依赖
- test: 测试依赖范围, 使用此依赖范围的 Maven 依赖, 只对于测试的 classpath 有效, 在编译主代码或者运行项目时将无法使用此类依赖, 典型的例子就是 JUnit, 它只有在编译器测试代码及运行测试的时候才需要
- provided: 已提供依赖范围, 使用此依赖范围的 Maven 依赖, 对于编译和测试 classpath 有效, 但在运行时无效, 典型的例子就是 servlet-api, 编译和测试项目的时候需要该依赖, 但在运行项目的时候, 由于容器已经提供, 就不需要 Maven 重复引入了
- runtime: 运行时依赖范围, 使用此依赖范围的 Maven 依赖, 对于测试和运行 classpath 有效, 但在编译主代码时无效, 典型的例子是 JDBC 驱动实现, 项目主代码的编译只需要 JDK 提供的 JDBC 接口, 只有在执行测试或运行项目的时候才需要实现上述接口的具体 JDBC 驱动
- system: 系统依赖范围, 该依赖与三种 classpath 的关系, 和 provided 依赖范围完全一致, 但是, 使用 system 范围的依赖时必须通过 systemPath 元素显示地指定依赖文件的路径, 由于此类依赖不是通过 Maven 仓库解析的, 而且往往与本机系统绑定, 可能造成构建的不可移植性
<dependency>
<groupId>javax.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>- import: 导入依赖范围, 该依赖范围不会对三种 classpath 产生实际的影响, 主要用于导入其他 pom 文件中的 dependencyManagement 元素对于依赖版本约束的内容
3.2.3 传递性依赖
在使用 Maven 依赖时, 如 Spring Framework, 此依赖又会依赖其他的开源库, 因此实际中往往会下载一个很大的如 spring-framework-2.5.6-with-dependencies.zip 包, 这样往往就引入了很多不必要的依赖, 而 Maven 的传递性依赖机制就可以很好的解决这一问题
- 当 A 项目引入一个 compile 范围的 B 依赖, 而 B 依赖中有一个 compile 范围的 C 依赖, 那么 C 依赖同样会成为 A 的 compile 范围依赖
传递性依赖和依赖范围
- 假设 A 依赖于 B,B 依赖于 C, 那么 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖
- 当第二直接依赖是 compile 的时候, 传递性依赖与第一直接依赖范围一致
- 当第二直接依赖是 test 的时候, 依赖不会得以传递
- 当第二直接依赖是 provided 的时候, 只传递第一直接依赖范围为 provided 的依赖, 且传递性依赖范围为 provided
- 当第二直接依赖是 runtime 的时候, 传递性地依赖的范围与第一直接依赖的范围一致, 但 compile 例外, 此时传递性依赖的范围为 runtime
3.2.4 可选依赖
假如有这样一个依赖关系,A 依赖于 B,B 依赖于 X 和 Y,B 对于 X 和 Y 的依赖都是可选依赖, 根据传递性依赖的定义, 如果这三个依赖的范围都是 compile, 那么 X,Y 就是 A 的 compile 范围传递性依赖, 但是由于 X,Y 都是可选依赖, 所以依赖不会得以传递, 因此 X,Y 不会对 A 有任何影响
为什么会有可选依赖这一特性呢?
- 当 B 实现了两个特性, 特性一依赖于 X, 特性二依赖于 Y, 并且这两个特性是互斥的, 用户不可能同时使用两个特性, 比如 B 是一个持久层隔离工具包, 支持多种数据库, 在使用这个工具包的时候, 只会依赖一种数据库
<dependencies\> <dependency\> <groupId\>mysql</groupId\> <artifact\>mysql-connector-java</artifact\> <version\>5.6.0</version\> <optional\>true</optional\> </dependency\> <dependency\> <groupId\>postgresql</groupId\> <artifactpostgresql</artifact> <version\>8.4-701.jdbc3</version\> <optional\>true</optional\> </dependency\> </dependencies\> * 以上使用 <optional\> 元素表示两个依赖为可选依赖, 他们只会对当前项目 B 产生影响, 当其他项目依赖于 B 时, 这两个依赖不会被传递, 所以当 A 依赖于 B 项目时, 如果要使用 mysql 数据库, 那么需要显式的声明 mysql-connector-java 这一依赖 * 关于可选依赖, 在理想的情况下, 是不应该使用可选依赖的, 使用可选依赖的原因是某一个项目中实现了多个特性, 而根据单一职责原则, 应该针对不同的特性分别创建一个 Maven 项目, 用户根据需要选择使用其中某一个依赖
3.2.5 排除依赖
传递性依赖会给项目隐式的引入很多依赖, 这极大的简化了项目依赖的管理, 但是有时候这种特性䧥带来问题
- 例如, 当前项目有一个第三方依赖, 而这个依赖由于某些原因依赖了另一个类库的 SNAPSHOT, 那么整个 SNAPSHOT 就会成为当前项目的传递性依赖, 而 SNAPSHOT 的不稳定性会直接影响到当前的项目, 这时候就需要排除掉该 SNAPSHOT, 并且在当前项目中声明该类库的某个正式发布版
<dependencies>
<dependency>
<groupId>com.lsy.myproject</groupId>
<artifactId>myproject-a</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.lsy.myproject</groupId>
<artifactId>myproject-b</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.lsy.myproject</groupId>
<artifactId>myproject-b</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
3.2.6 归类依赖
- 当一些依赖来自同一个项目的不同模块, 这些依赖的版本都应该是相同的, 将来升级也是一起升级, 如 Spring Framework, 这时可以使用 properties 元素定义 Maven 属性
<properties>
<springframework.version>4.2.1</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<verison>${springframework.version}</verison>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<verison>${springframework.version}</verison>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<verison>${springframework.version}</verison>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<verison>${springframework.version}</verison>
</dependency>
</dependencies>
3.2.7 优化依赖
在软件开发过程中, 通常会通过重构等方式不断优化自己代码, 同样, 对于 Maven 项目的依赖也需要对其进行优化
- 去除多余的依赖
- 显示声明某些必要的依赖
Maven 会自动解析所有项目的直接依赖和间接依赖, 并且根据规则判断每个依赖范围, 对于一些依赖冲突, 也能进行调整, 以确保任何一个构建只有唯一的版本在依赖中存在, 此称为解析依赖
- mvn dependency:list 查看当前项目的已解析依赖
- mvn dependency:tree 以树结构查看已解析依赖
mvn dependency:analyze 解析依赖
- Used undeclared dependencies: 指项目中使用到的, 但是没有显示声明的依赖, 这种依赖意味着潜在的风险, 当前项目直接在使用它们, 所以需要显示声明任何项目中直接用到的依赖
- Unused declared dependencies: 指项目中未使使用的. 但显示声明的依赖
四. 仓库
- 之前已经介绍了 Maven 的坐标和依赖, 坐标和依赖是任何一个构件在 Maven 世界中的逻辑表示方式, 而构建的物理表示方式是文件,Maven 通过仓库来同一管理这些文件
-
在 Maven 世界中, 任何一个以依赖, 插件或项目构建的输出, 都可以成为构件
- 例如:log4j-1.2.15.jar,maven-compiler-plugin-2.0.2.jar 或项目打包后的 myproject-1.0.0-SNAPSHOT.jar
- 在一台工作站上, 可能会有几十个项目, 所有项目在 /lib 目录下都会有自己所需的依赖包, 而这些依赖中都有大量的重复, 每个项目各自存储自己所需的依赖包不仅造成磁盘空间的浪费, 而且也难于同一管理, 文件的复制等操作
- 得益于坐标机制, 任何 Maven 项目使用任何一个构建的方式都是相同的, 因此,Maven 可以在某个位置统一存储所有 Maven 项目共享的构件, 这个统一的位置就是仓库, 为了实现重用, 项目构建完毕后生成的构建也可以安装或部署到仓库中
4.1 仓库的布局
任何一个构建都有其唯一的坐标, 根据这个坐标可以定义其在仓库中的唯一存储路径, 这就是 Maven 的仓库布局方式, 如 log4j:log4j:1.2.15 这一依赖, 其对应的仓库路径为 log4j/log4j/1.2.15/log4j-1.2.15.jar
- 路径与坐标的关系为:groupId/artifactId/version/artifactId-verision.packaging
4.2 仓库的分类
对于 Maven 来说, 仓库分为两类
- 本地仓库
远程仓库
- 中央仓库:Maven 核心自带的仓库服务器, 它包含了绝大部分开源的构件
- 私服: 另一种特殊的远程仓库, 为了节省宽带和时间, 应该在局域网内部构建一个私有仓库服务器, 用其代理所有外部的远程仓库, 内部项目部署到私服上供其它项目使用
- 其他公共库
- 当 Maven 根据坐标寻找构件时, 它首先会查看本地仓库, 如果本地仓库存在此构件, 则直接使用, 如果本地仓库不存在此构件, 或者需要查看是否有更新的构件版本,Maven 就会去远程仓库查找, 发现需要的构件后下载到本地仓库再使用, 若本地和远程都没有, 则报错
4.3 本地仓库
一般来说, 在 Maven 项目目录下, 没有注入 lib/ 这样用来存放依赖文件的目录, 当 Maven 执行编译或测试时, 如果需要使用依赖文件, 它总是基于坐标使用本地仓库的依赖文件
- 默认情况, 在用户目录下路径名为.m2/repository/ 的仓库目录, 当想自定义本地仓库目录地址时, 可以编辑~/.m2/setting.xml, 设置 localRepository 元素来指定仓库地址
<settings>
<localRepository>D:/java/repository/</localRepository>
</settings>- 默认情况下,~/.m2/settings.xml 文件是不存在的, 用户需要从 Maven 安装目录复制 $M2_HOME/conf/settings.xml 文件再编辑, 建议不要直接修改全局目录的 settings.xml, 而是在用户目录下进行修改
一个构建只有在本地仓库中, 才能由其它 Maven 项目使用
- 依赖 Maven 从远程仓库下载到本地仓库中
- 将本地项目的构件安装到 Maven 本地仓库中 mvn clean install
4.4 远程仓库
安装好 Maven 后, 如果不执行任何 Maven 命令, 本地仓库目录是不存在的, 只有输入第一条 Maven 命令后,Maven 才会创建本地仓库, 并根据配置和需要, 从远程仓库下载至本地仓库
- 这就好比藏书, 本地仓库好比书房, 远程仓库好比书店, 我需要读书时先去书房找, 当书房没有时, 就去书店买回放到书房, 并且一般对每个人来说, 书房只有一个, 而外面的书店可以有多个
4.4.1 中央仓库
最原始的本地仓库是空的, 所以 Maven 必须知道至少一个可用的远程仓库, 才能在执行 Maven 命令时下载到需要的构建, 中央仓库就是这样一个默认的远程仓库, 可以通过解压工具打开 $M2_HOME/lib/maven-model-builder-3.0.jar 中的 org/apache/maven/model/pom-4.0.0.xml 文件
<repositories>
<repository>
<id>central</id>
<name>Maven Repository Switchboard</name>
<url>http://repo1.maven/org/maven2</url>
<layout>default</layout>
<snapshots>
<enable>false</enable>
</snapshots>
</repository>
</repositories>
- 这段配置是所有 Maven 项目都会继承的超级 Pom 文件, 这段配置使用 id central 对中央仓库进行唯一标识, 其名称为 Maven Repository Switchboard, 它使用 default 仓库布局, 也就是在之前介绍的仓库布局, 最后 snapshots 元素表示不从该仓库下载快照版
4.4.2 私服
私服是一种特殊的远程仓库, 它是架设在局域网内的仓库服务, 私服代理广域网上的远程仓库, 供局域网内的 Maven 用户使用, 当 Maven 需要下载构建的时候, 它从私服请求, 如果私服上不存在该构件, 则从外部的远程仓库下载, 缓存在私服上之后, 再为 Maven 的下载提供服务
- 此外, 一些无法从外部下载的构件也可以从本地上传到私服上供大家使用
私服的好处
- 节省外网带宽: 建立私服同样可以减少组织自己的开支, 大量的对于外部仓库的重复请求会消耗很大的带宽, 利用私服代理外部仓库之后, 对外的重复构件下载便可以消除, 即降低外网带宽的压力
- 加速 Maven 构件: 不停的连接请求外部仓库是十分耗时的, 但 Maven 的一些内部机制 (如快照更新检查井) 要求 Maven 在执行构建时不停的检查远程仓库数据, 因此, 当项目配置很多外部仓库时, 构建速度就会降低
- 部署第三方构件: 当某个构件无法从任何一个外部远程仓库获取, 建立私服之后, 便可以将这些构件部署到这个内部的仓库中, 供内部的 Maven 项目使用
- 提高稳定性, 增强控制:Maven 构建高度依赖远程仓库, 因此, 大哥 Internet 不稳定的时候,Maven 构建也会变得不稳定, 甚至无法构建, 使用私服后, 即是短暂时没有 Internet 连接, 由于私服中有大量缓存,Maven 依然可以正常运行, 并且私服中有很多额外的权限功能控制
- 降低中央仓库的负荷: 每天中央仓库都需要面对大量的下载请求, 使用私库可以降低对于中央仓库的负荷
远程仓库的配置
很多情况下, 默认的中央仓库无法满足项目的需求, 可能项目需要的构件存在于另一个远程仓库中, 可以通过 Pom/settings 文件中来配置该仓库
<!–> POM 文件 </!–>
<project>
…..
<repositories>
<repository>
<id>jboss</id>
<name>JBoss Repository</name>
<url>http://repository.jboss.com/m…;/url>
<release>
<enabled>true</enabled>
</release>
<snapshots>
<enabled>false</enabled>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
</project>
<!–> settings 文件 </!–>
<profiles>
<profile>
<id>dev</id>
<activation>
<activatedByDefault>true</activatedByDefault>
</activation>
<repositories>
<repository>
<id>jboss</id>
<name>JBoss Repository</name>
<url>http://repository.jboss.com/m…;/url>
<release>
<enabled>true</enabled>
</release>
<snapshots>
<enabled>false</enabled>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
</profile>
</profiles>
- 在 repositories 元素下, 可以使用 repository 子元素声明一个或多个远程仓库, 并且声明一个 id, 任何一个仓库声明的 id 必须是唯一 id
- Maven 自带的中央仓库使用的 id 为 central, 如果其他仓库使用该 id, 则会覆盖中央仓库的配置, 该配置中的 url 指向了仓库的地址
该配置中的 release 和 snapshots 元素用来控制 Maven 对于发布版构件和快照版构件的下载, 对于 release 和 snapshots 元素来说除了 enabled 子元素, 还有 updatePolicy 和 checksumPolicy 元素
updatePolicy: 用来配置 Maven 从远程仓库检查更新的频率, 默认为 daily
- daily: 每天
- never: 从不
- always: 每次
- interval :X : 每隔 X 分钟一次
checksumPolicy: 配置 Maven 检查文件失败时的策略, 默认为 warn
- fail:Maven 遇到验证和错误就让构建失败
- warn:Maven 遇到验证和错误就发出警告信息
- ignore:Maven 遇到验证和错误时完全忽略
<snapshots\> <enabled\>true</enabled\> <updatePolicy\>daily</updatePolicy\> <checksumPolicy\>ignore</checksumPolicy\> </snapshots\> * 远程仓库的验证 * 大部分远程仓库无需认证就可以访问, 但出于安全考虑, 我们需要提供一些认证信息才能访问一些远程仓库 * 配置认证信息和配置仓库信息不同, 仓库信息可以直接配置在 POM 文件中, 但是认证信息必须配置在 settings.xml 文件中 * 其中 server 元素的 id 必须与 POM 文件中需要认证的 repository 元素的 id 完全一致 <settings\> ...... <servers\> <server\> <id\>my-project</id\> <username\>user</username\> <password\>password</password\> </server\> </servers\> ....... </settings\> * 部署至远程仓库 * 私服一大作用就是部署第三方构件, 包括组织内部生成的构件以及无法从外部仓库直接获取的构件, 无论是日常开发中生成的构件, 还是正式版本发布的构件, 都需要部署到仓库中 * distributionManagement 包含 repository 和 snapshotRepository 子元素, 前者表示发布版构件的仓库, 后者表示快照版的仓库 * 往远程仓库部署构件时, 往往需要认证, 认证配置需在 settings.xml 中创建一个 server 元素, 其 id 与仓库的 id 匹配, 无论从远程仓库下载构件, 还是部署构件至远程仓库, 当需要认证时, 配置方式都是一样的 <!--> 在 POM 文件中配置 distributionManagement </!--> <project\> ..... <distributionManagement\> <repository\> <id\>jboss-release</id\> <name\>JBoss Release Repository</name\> <url\>http://192.168.1.99/content/repository/jboss-release</url\> </repository\> <snapshotRepository\> <id\>jboss-snapshots</id\> <name\>JBoss Snapshots Repository</name\> <url\>http://192.168.1.99/content/repository/jboss-snapshots</url\> </snapshotRepository\> </distributionManagement\> ..... </project\>
4.5 快照
- 在 Maven 的世界中, 任何一个项目或者构件都必须有自己的版本, 版本的值可能是 1.0.0,1.3-alpha-4,2.0,2.1-SNAPSHOT 或者 2.1-20191214.221414-13, 其中 1.0.0,1.3-alpha- 4 和 2.0 是稳定的发布版本, 而 2.1-SNAPSHOT 和 2.1-20091214.221414-13 是不稳定的快照版本
- 对于一个稳定的版本, 如果仓库中已经包含, 那么 Maven 就不会再去对照远程仓库进行更新, 除非每次执行 Maven 命令前, 清除本地仓库中等待稳定版本, 而对于一个正在迭代的项目, 如果要实时更新版本的内容就需要频繁的修改新的版本名称, 这样是对版本号的滥用
- 针对这种情况, 使用快照版时,Maven 会自动为构件打上时间戳, 因此,Maven 就能随时找到仓库中该构建最新版本的文件, 一旦有新的更新, 就会去同步到本地仓库. 当项目经过完善的测试后需要发布的时候, 再将快照版本更改为发布版本
- 快照版本只应该在组织内部的项目或模块之间依赖使用, 因为这时, 组织对这些快照版本的依赖具有完全的理解和控制权, 项目不应该依赖任何组织外部的快照版本依赖, 由于快照版本的不稳定性, 随时可能发生变化, 这样的依赖会有潜在的危险
4.6 从仓库解析依赖的机制
当本地仓库没有依赖构件的时候,Maven 会自动从远程仓库下载, 当依赖版本为快照版本时,Maven 会自动找到最新的快照, 这背后的依赖机制可以概括如下
- 当依赖的范围时 system 的时候,Maven 直接从本地文件系统解析构件
- 根据依赖坐标计算仓库路径后, 尝试从本地仓库寻找构件, 如果发现相应的构件, 则解析成功
- 在本地仓库不存在相应构件的情况下, 如果依赖的版本时显式的发布版本构件, 如 1.2,2.1-beta- 1 等, 则遍历所有的远程仓库, 发现后, 下载并解析使用
- 如果依赖的版本时 RELEASE 或 LATEST, 则基于更新策略读取所有远程仓库的元数据 groupId/artifact/maven-metadata.xml, 将其与本地仓库的对应元数据合并后, 计算出 RELEASE 或 LATEST 真实的值, 然后基于这个真实的值检查本地仓库和远程仓库
- 如果依赖的版本是 SNAPSHOT, 则基于更新策略读取所有远程仓库的元数据 groupId/artifactId/maven-metadata.xml, 将其与本地仓库的对应数据合并后, 得到最新的快照版本的值, 然后基于该值检查本地仓库, 或者从远程仓库下载
- 如果最后解析得到的构件版本是时间戳格式, 如 1.4.1-20191104.121455-8, 则复制其时间戳格式的文件至非时间戳格式, 如 SNAPSHOT, 并使用该非时间戳格式的构件
<!–> 基于 groupId 和 artifactId 的 maven-metadata.xml </!–>
<?xml version=”1.0″ encoding=”UTF-8″?>
<metadata>
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus</artifactId>
<versioning>
<latest>1.4.2-SNAPSHOT</latest>
<release>1.3.7</release>
<versions>
<version>1.3.5</version>
<version>1.3.6</version>
<version>1.3.7</version>
<version>1.4.0-SNAPSHOT</version>
<version>1.4.1-SNAPSHOT</version>
<version>1.4.2-SNAPSHOT</version>
</versions>
<lastUpdated>20191214221133</lastUpdated>
</versioning>
</metadata>该 XML 文件列出了仓库中存在的构件所有可用的版本, 同时 latest 元素指向了这些版本中最新的那个版本 1.4.2-SNAPSHOT, 而 release 元素指向了这些版本中最新的发布版本 1.3.7,Maven 通过合并多个远程仓库及本地仓库的元数据, 就能计算出基于所有仓库的 latest 和 release
- 需要注意的是, 在依赖声明使用 LATEST 和 RELEASE 是不推荐的做法, 因为 Maven 随时可能解析到不同的构件, 且 Maven 不会明确告诉用户这样的变化
4.7 镜像
如果仓库 X 可以提供仓库 Y 存储的所有内容, 那么就可以认为 X 是 Y 的一个镜像, 由于地理位置的因素, 中央仓库的下载速度会比较慢, 这时我们可以配置 Maven 使用镜像来代替中央仓库, 编辑 settings.xml
<settings>
……
<mirrors>
<mirror>
<id>maven.net.cn</id>
<name>one of the central mirror in china</name>
<url>http://maven.net.cn/content/g…;/url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
……
</settings>
<mirrorOf> 的值为 central, 表示该配置为中央仓库的镜像, 任何对于中央仓库的请求都会转至该镜像, 用户也可以使用同样的方法配置其他仓库的镜像, 另外三个元素 id,name,url 与一般仓库配置无异, 表示该镜像仓库的唯一标识, 名称以及地址
- 若该镜像要进行验证, 即基于该 id 配置仓库认证
<settings\> ..... <mirrors\> <mirror\> <id\>internal-repository</id\> <name\>Internal Repository Mananger</name\> <url\>http://192.168.1.100/maven2/</url\> <mirrorOf\>\*</mirrorOf\> </mirror\> </mirrors\> ..... </settings\>
以上 mirrorOf 元素的值为 *, 表示该配置是所有 Maven 仓库的镜像, 对于任何远程仓库的请求都会被转至该指定的仓库, 如果镜像仓库需要验证, 则配置一个 id 为 internal-repository 的 <server> 即可
- <mirrorOf>*<mirrorOf>: 匹配所有远程仓库
- <mirrorOf>external:*<mirrorOf>: 匹配所有远程仓库, 使用 localhost 的除外, 使用 file:// 协议的除外, 也就是说, 匹配所有不在本机上的远程仓库
- <mirrorOf>repo1,repo2<mirrorOf>: 匹配仓库 repo1 和 repo2, 用逗号分隔多个仓库
- <mirrorOf>*,! repo1<mirrorOf>: 匹配所有的远程仓库, 除 repo1 外, 使用感叹号将仓库从匹配中排除
- 需要注意的是, 由于镜像仓库完全屏蔽了被镜像仓库, 当镜像仓库不稳定或停止服务时,Maven 仍将无法访问被镜像仓库, 因此无法下载构件
五. 生命周期和插件
5.1 Maven 的生命周期
在 Maven 出现之前, 项目构建的生命周期就已经存在, 但是不同的公司和开发人员虽然同样在做构件工作, 其对于不同的项目却不能够重用, 只能重新定制开发, 而 Maven 的生命周期就是为了对所有的构件过程进行抽象和统一
- 这个生命周期包含了项目的清理, 初始化, 编译, 测试, 打包, 集成测试, 验证, 部署和站点生成等几乎所有的构件步骤, 也就是说几乎所有项目的构件, 都能映射到这样一个生命周期上
Maven 的生命周期是抽象的, 这意味着生命周期本身不做任何实际的工作, 在 Maven 的设计中, 实际的任务 (如编译主代码) 都交由插件完成, 这种思想和设计模式的模方法类似, 在父类中定义算法的整体结构, 子类可以通过实现或重写父类的方法来控制实际的行为
public abstract class Template{
public void build(){
initialize();
compile();
test();
packagee();
integrationTest();
deploy();
}protect abstract void initialize();
protect abstract void compile();
protect abstract void test();
protect abstract void packagee();
protect abstract void integrationTest();
protect abstract void deploy();
}
- 在 Maven 的生命周期中抽象了各个步骤, 定义了他们的次序, 但是没有提供具体的实现, 而通过插件机制为每个构件步骤绑定一个或多个插件行为, 而且 Maven 为大多数构件步骤都绑定了默认的插件, 例如, 针对编译的 maven-compiler-plguin, 针对测试的 maven-surefire-plugin 等
- Maven 定义的生命周期和插件机制一方面保证了所有 Maven 项目有一致的构件标准, 另一方面又通过默认的插件简化和稳定实际项目的构件, 此外, 该机制还提供了足够的扩展, 用户可以通过配置现有的插件或自定义插件来自定义构件行为
三套声明周期
Maven 拥有三套相互独立的生命周期, 他们分别为 clean,default 和 site, 每个生命周期包含一些阶段, 这些阶段是有序的, 并且后面的阶段依赖于前面的阶段, 但是三套声明周期本身是互相独立的
clean 生命周期: 清理项目
- pre-clean: 执行一些清理前需要完成的工作
- clean: 清理上次构件生成的文件
- post-clean: 执行一些清理后需要完成的工作
default 声明周期: 定义了真正的构件所需执行的所有步骤
- validate
- initialize
- generate-sources
- process-sources: 处理项目主资源文件, 一般来说针对 /src/main/resources 目录的内容进行变量替换等工作后, 复制到项目输出的主 classpath 目录中
- compile: 编译项目的主源码, 一般来说针对 /src/main/java 目录下的 Java 文件至目录输出的主 classpath 目录中
- process-classes
- generate-test-sources
- process-test-sources: 处理项目测试资源文件, 一般来说针对 /src/test/resources 目录的内容进行变量替换工作后, 复制到项目输出的测试 classpath 目录
- test-compile: 编译项目的测试代码, 一般来说针对 /src/test/java 目录下的 java 文件至输出的测试 classpath 目录中
- test: 使用单元测试框架运行测试, 测试代码不会被打包或部署
- prepare-package
- package: 接收编译好的代码, 打包成可发布的格式, 如 jar
- pre-integration-test
- integration-test
- post-integration-test
- verify
- install: 将包安装到 Maven 本地仓库, 供本地其他 Maven 项目使用
- deploy: 将最终的包复制到远程仓库, 供其他开发人员和 Maven 项目使用
site 声明周期: 建立和发布项目站点,Maven 能够基于 POM 所包含的信息, 自动生成一个友好的站点供交流和发布项目信息
- pre-site: 执行一些在生成项目站点之前需要完成的工作
- site: 生成项目站点文档
- post-site: 执行一些在生成项目站点后需要完成的工作
- site-deploy: 将生成的项目站点发布到服务器上
命令行与生命周期
从命令行执行 Maven 任务最主要方式就是调用 Maven 的生命周期, 各个生命周期是相互独立的, 而生命周期的阶段是有前后依赖关系的
- mvn clean: 该命令调用 clean 生命周期的 clean 阶段, 实际执行阶段为 pre-clean 和 clean
- mvn test: 该命令调用 default 生命周期的 test 阶段, 实际执行的阶段为 default 生命周期的 validate,initialize 直到 test 的所有阶段, 这也解释为什么在执行测试的时候, 项目代码能够自动编译
- mvn clean install: 该命令调用 clean 生命周期的 clean 阶段和 default 生命周期的 install 阶段
- mvn clean deploy site-deploy: 该命令调用 clean 生命周期的 clean 阶段,default 生命周期的 deploy 阶段和 site 生命周期的 site-deploy 阶段
5.2 插件
5.2.1 插件目标和绑定
Maven 的核心仅仅定义了抽象的生命周期, 具体的任务是交由插件完成的, 插件以独立的构件形式存在
- 对于插件本身而言, 为了能够复用代码, 它往往能够完成多个任务, 为每个功能编写一个插件显然不合理, 因为这些任务背后有大量可复用的代码, 因此, 这些功能聚集到一个插件里面, 每个功能就是一个插件目标
Maven 的生命周期和插件相互绑定, 用以完成实际的构件任务, 具体而言, 是生命周期的阶段与插件的目标相互绑定, 以完成某个具体的构件任务
- 例如: 编译这一任务对应了 default 生命周期的 compile 这一阶段, 而 maven-compiler-plugin 这一插件的 compile 目标能完成此任务
自定义绑定
除了内置绑定以外, 用户还能够自己选择将某个插件目标绑定到生命周期的某个阶段上, 这种自定义绑定方式能让 Maven 项目在构件过程中执行更多更丰富的任务
- 当需要创建项目的源码 jar 包,maven-source-plugin 可以帮我们完成任务, 但是内置的插件绑定关系中并没有涉及这一任务, 因此需要自行配置, 它的 jar-no-fork 目标能将项目主代码打包成 jar 文件, 可以将其绑定到 default 生命周期的 verify 阶段, 在执行完集成测试和安装构件之前创建源码 jar 包
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>2.1.1</version>
<executions>
<execution>
<id>attach-sources</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>在 POM 的 build 元素下的 plugins 子元素中声明插件的使用, 上例用到了是 maven-source-plugin, 其 groupId 为 org.apache.maven.plugins, 用户总是应该声明一个非快照版本, 这样可以避免由于插件版本变化造成的构件不稳定性
- 除了基本的插件坐标声明外, 还有插件执行配置,executions 下每个 execution 子元素可以用来配置执行一个任务, 该例配置了一个 id 为 attach-sources 的任务, 通过 phase 配置将其绑定到 verify 生命周期阶段上, 再通过 goals 配置指定要执行的插件目标, 在运行 mvn verify 时就会执行该任务
有时候. 即是不通过 phase 元素配置生命周期阶段, 插件目标也能绑定到生命周期中去, 原因是: 有很多插件的目标在编写时就已经定义了默认绑定阶段, 可以使用 maven-help-plugin 查看详细信息
mvn help:describe -Dplugin=org.apache.maven.plugins:maven-source-plugin:2.1 -Ddetail
- Bound to phase : package 默认绑定到 package 生命周期
- 当插件目标被绑定到不同生命周期阶段的时候, 其执行顺序会由生命周期阶段的先后顺序决定, 如果多个目标被绑定到同一个阶段, 他们的顺序就由插件声明的先后顺序决定
5.2.2 插件配置
用户可以通过配置插件目标的参数, 进一步调整插件目标所执行的任务, 几乎所有 Maven 插件的目标都可以配置参数, 用户可以通过命令行和 POM 配置等方式来配置这些参数
命令行插件配置
在日常的 Maven 使用中, 我们通常从命令行输入并执行 Maven 命令, 很多插件目标的参数都支持从命令行配置, 用户可以在 Maven 命令中使用 - D 参数, 并伴随一个参数键 = 参数值的形式, 来配置插件目标的参数
mvn install -Dmaven.test.skip = ture: 给 maven.surefire-plugin 提供一个 maventest.skip 参数, 当参数为 true 时, 就会跳过执行测试
- 参数 - D 是 Java 自带的, 其功能是通过命令行设置一个 Java 系统属性,Maven 简单地重用了该参数, 在准备插件的时候检查系统属性来实现插件参数的配置
POM 中插件全局配置
并不是所有的插件参数都适合用命令行配置, 有些参数的值从项目创建到项目发布都不会改变, 或者说很少改变, 这种情况下在 POM 文件中一次性配置比重复在命令行输入更合理
- 用户可以在声明插件的时候, 对插件进行一个全局的配置, 所有基于该插件的目标任务, 都会使用这些配置, 如:maven-compiler-plugin 来编译 1.8 版本的源文件, 生成与 JVM1.8 兼容的字节码文件
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifact>maven-compiler-plugin</artifact>
<version>2.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
5.2.3 获取插件信息
当遇到一个构建任务时, 不仅需要知道去哪里找到合适的插件, 还需要详细的了解插件的配置点, 由于 Maven 的插件非常多, 其中大部分没有完善的文档, 因此, 通过帮助命令来了解插件显得非常重要
使用 maven-help-plugin 描述插件
除了访问在线的插件文档外, 可以借助 maven-help-plugin 来获取插件的详细信息
mvn help:decribe -Dplugin = org.apache.maven.plugins:maven-compiler-plugin:2.1
- 这里执行的是 maven-help-plugin 的 decribe 目标, 在参数 plugin 中输入需要描述插件的 groupId,artifactId 和 version,Maven 在命令行的输出 maven-compiler-plugin 的简要信息, 包括插件的坐标, 目标前缀和目标等信息
在描述插件时, 可以省去版本信息,Maven 会自动获取最新版本来进行表述, 并可以用目标前缀来替换坐标
- mvn help:describe -Dplugin=compiler
如果仅仅描述某个插件目标的信息, 则加上 goal 参数, 更详细的信息, 则加上 detail 参数
- mvn help:describe -Dplugin=compiler -Dgoal=compile -Ddetail
5.2.4 插件解析机制
为了方便用户使用和配置插件,Maven 不需要用户提供完整的插件坐标信息, 就可以解析得到正确的插件
插件仓库
- 与依赖构件一样, 插件构件同样基于坐标存储在 Maven 仓库中, 在需要的时候 Maven 先从本地仓库寻找, 如果不存在, 则从远程仓库查找, 找到插件之后, 再下载到本地仓库使用
- 不同于 repositories 以及 repository 子元素, 插件的远程仓库使用 pluginRepositories 和 pluginRepository 配置
<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Maven Plugin Resository</name>
<url>http://repo1.maven.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<release>
<enabled>true</enabled>
<updatePolicy>never</updatePolicy>
</release>
</pluginRepository>
</pluginRepositories>插件默认的 groupId
- 在 POM 中配置插件时, 如果该插件时 Maven 的官方插件(即 groupId 为 org.apache.maven.plugins), 就可以省略 groupId,Maven 在解析该插件的时候, 会自动使用默认 groupId 不起
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>解析插件版本
同样为了简化插件的配置和使用, 在用户没有提供插件版本的情况下,Maven 会自动解析插件版本
- 首先 Maven 在超级 POM 中为所有核心插件设定了版本, 超级 POM 是所有 Maven 项目的父 POM, 所有项目都继承这个超级 POM 配置, 因此即使用户不加任何配置,Maven 在使用核心插件时, 它的版本就已经确定了
- 如果用户使用的某个插件没有设定版本, 并且这个插件也不属于核心插件,Maven 机会去检查所有仓库中可用的版本, 通过仓库元数据 groupId/artifactId/maven-metadata.xml 文件, 如 maven-compiler-plugin 插件为例, 它在 org/apache/maven/plugins/maven-compiler-plugin/maven-metadata.xml
<?xml version=”1.0″ encoding=”UTF-8″?>
<metadata>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<versioning>
<latest>2.1</latest>
<release>2.1</release>
<versions>
<version>2.0-beta-1</version>
<version>2.1</version>
<version>2.2</version>
<version>2.3</version>
</versions>
<lastUpdated>20190102092331</lastUpdated>
</versioning>
</metadata>- Maven 遍历本地仓库和所有远程仓库, 将仓库元数据合并后, 就能计算出 latest 和 release 的值,maven3 将版本解析到最新的非快照版, 如 2.1
5.2.5 解析插件前缀
- Maven 命令支持使用插件前缀来简化插件的使用, 插件前缀和 groupId:artifact 是一一对应的, 这种匹配关系存储在仓库元数据中, 与之前提到的 groupId/artifactId/maven-metadata.xml 不同, 这里仓库元数据为 groupId/maven-metadata.xml, 一般插件都位于 /org/apache/maven/plugins/ 和 org/code-haus/mojo/, 可以通过 settings.xml 配置让 Maven 检查其他 groupId 上的插件仓库元数据
<settings>
<pluginGroups>
<pluginGroup>com.my.plugins</pluginGroup>
</pluginGroups>
</settings>- 在仓库的元数据文件中可以看到插件的前缀定义
<metadata>
<plugins>
<plugin>
<name>Maven Clean Plugin</name>
<prefix>clean</prefix>
<artifact>maven-clean-plugin</artifact>
</plugin>
<plugin>
<name>Maven Compiler Plugin</name>
<prefix>compile</prefix>
<artifact>maven-compile-plugin</artifact>
</plugin>
<plugin>
<name>Maven Dependency Plugin</name>
<prefix>dependency</prefix>
<artifact>maven-dependency-plugin</artifact>
</plugin>
</plugins>
</metadata>
六. 聚合与继承
- Maven 的集合特性能够把项目各个模块聚合在一起构建, 而 Maven 的继承特性则能帮助抽泣各模块相同的依赖和插件等配置, 在简化 POM 的同时, 还能促进各个模块配置的一致性
6.1 集合
aggregator
<modelVersion>4.0.0</modelVersion>
<groupId>com.lsy.project</groupId>
<artifact>project-aggregator</artifact>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Aggregator</name>
<modules>
<module>project-email</module>
<module>project-register</module>
</modules>
- 对于聚合模块, 其打包方式 packaging 的值必须为 pom
通过 modules 元素来实现聚合, 用户可以通过在一个打包方式为 pom 的 Maven 项目中声明任意数量的 module 元素来实现模块的聚合, 这里每个 module 的值都是一个当前 POM 的相对目录
- 如 aggregator 的 POM 路径为 …/project-aggregator/pom.xml, 那么 project-email 对应 的目录为 …/project-aggregator/project-email/, 并且目录中包含了 pom.xml,src/main/java,src/test/java 等内容, 离开了 project-aggregator 也能独立创建 =
- 聚合模块和其他模块的目录结构并非一定要父子关系, 也可以是平行关系
<modules>
<module>../prject-email</module>
<module>../project-register</module>
</modules>- 当在聚合模块中执行 mvn clean install 时,Maven 首先解析聚合模块的 POM, 分析要构件的模块, 并计算出一个反应堆构件顺序(Reactor Build Order), 然后根据这个顺序构件各个模块
聚合模块的构件顺序
- Maven 按序读取 POM 文件, 如果 POM 没有依赖模块, 那么就构件模块, 否则就先构件其依赖模块
6.2 继承
从以上的聚合模块和其他模块中可以看到, 多个被管理模块的 POM 文件中会有大量重复的相同配置, 他们有相同的 groupId 和 version, 相同的依赖, 相同的插件配置, 而通过 POM 文案的继承可以消除重复
project-parent 父模块
<modelVersion>4.0.0</modelVersion>
<groupId>com.lsy.project</groupId>
<artifactId>project-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Parent</name>
- 父模块的 POM 文件使用与其他模块一直的 groupId 和 version, 它的 packaging 方式为 pom, 与聚合模块一致
- 父模块主要是为了消除配置的重复, 因此它本身不包含除 POM 文件之外的项目文件, 也就不需要 src/main/java 之类的文件夹了
project-email 子模块
<parent>
<groupId>com.lsy.project</groupId>
<artifactId>project-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
<relativePath>../project-parent/pom.xml</relativePath>
</parent>
<artifactId>project-email</artifactId>
<name>Email</name>
<dependencies>
…..
</dependencies>
使用 parent 元素声明父模块,parent 下子元素 groupId,artifactId,version 指定父模块的坐标, 元素 relativePath 表示父模块 POM 的相对路径, 表示 Email 模块和其父模块是在平行的目录下
- 当构件项目时,Maven 会首先根据 relativePath 检查父 POM 文件, 如果找不到再从本地仓库找,relativePath 的默认值为../pom.xml, 也就是说,Maven 默认父 POM 在上一层目录
- 子模块没有 groupId 和 version, 是因为子模块隐式的从父模块继承了这两个元素, 从而消除了不必要的配置
可继承的 POM 元素
- groupId: 项目组 ID, 项目坐标的核心元素
- version: 项目版本, 项目坐标的核心元素
- description: 项目的描述信息
- organization: 项目的组织信息
- inceptionYear: 项目的创始年份
- url: 项目的 URL 地址
- develops: 项目的开发者信息
- contributors: 项目贡献者信息
- distributionManagement: 项目的部署配置
- issueManagement: 项目的缺陷跟踪系统信息
- ciManagement: 项目的持续集成系统信息
- scm: 项目的版本控制系统信息
- mailingList: 项目的邮件列表信息
- properties: 自定义属性
- dependencies: 项目的依赖配置
- dependencyManagement: 项目的依赖管理配置
- repositories: 项目的仓库配置
- pluginRepositories: 项目的插件仓库配置
- build: 包括项目的源码目录配置, 输出目录配置, 插件配置, 插件管理配置等
- reporting: 项目的输出目录配置, 报告插件配置等
依赖管理
- dependencies 元素是可以被继承的, 说明依赖是会被继承的, 所以我们可以将子模块共有的依赖配置到父模块中, 子模块就可以移除这些依赖, 简化配置
上述方法是可行的, 但是可能将来会有新的模块并不需要父模块中的一些依赖, 这就会产生不合理的现象, 从而 Maven 提供了 dependencyManagement 元素, 既能让子模块继承到父模块的依赖配置, 又能保证子模块依赖使用的灵活性
- 在 dependencyManagement 元素下的依赖声明不会引入实际的依赖, 不过它能够约束 dependencies 下的依赖使用
<modelVersion\>4.0.0</modelVersion\> <groupId\>com.lsy.project</groupId\> <artifactId\>project-parent</artifactId\> <version\>1.0.0-SNAPSHOT</version\> <packaging\>pom</packaging\> <name\>Parent</name\> <properties\> <springframework.version\>4.3.1</springframework.version\> </properties\> <dependencyManagement\> <dependencies\> <dependency\> <groupId\>org.springframework</groupId\> <artifactId\>spring-core</artifactId\> <version\>${springframwork.version}</version\> </dependency\> <dependency\> <groupId\>org.springframework</groupId\> <artifactId\>spring-beans</artifactId\> <version\>${springframwork.version}</version\> </dependency\> <dependency\> <groupId\>org.springframework</groupId\> <artifactId\>spring-context</artifactId\> <version\>${springframwork.version}</version\> </dependency\> </dependencies\> </dependencyManagement\> * 这里使用 dependencyManagement 声明的依赖既不会给 parent 模块引入依赖, 也不会给子模块引入依赖, 不过这段配置是会被继承的 * 子模块 POM <parent\> <groupId\>com.lsy.project</groupId\> <artifactId\>project-parent</artifactId\> <version\>1.0.0-SNAPSHOT</version\> <relativePath\>../project-parent/pom.xml</relativePath\> </parent\> <artifactId\>project-email</artifactId\> <name\>Email</name\> <dependencies\> <dependency\> <groupId\>org.springframework</groupId\> <artifactId\>spring-core</artifactId\> </dependency\> <dependency\> <groupId\>org.springframework</groupId\> <artifactId\>spring-beans</artifactId\> </dependency\> <dependency\> <groupId\>org.springframework</groupId\> <artifactId\>spring-context</artifactId\> </dependency\> </dependencies\> * 使用这种依赖管理机制, 可以在父 POM 中使用 dependencyManagement 声明依赖能够统一项目规范中的依赖版本, 当版本在父 POM 中声明之后, 子模块使用依赖时就不需要声明了. 也不会发生多个子模块使用依赖版本不一致的情况 * scoper 元素的 import 依赖范围只在 dependencyManagement 元素下才有效果, 使用该范围的依赖通常指向一个 POM 文件, 作用是将 POM 中的 dependencyManagement 配置导入并合并到当前 POM 的 dependencyManagement 元素中 * 所以除了复制配置和继承父模块这两种方式外, 还可以通过 import 范围依赖导入这一配置 <dependencyManagement\> <dependencies\> <dependency\>com.lsy.project</dependency\> <artifactId\>project-parent</artifactId\> <version\>1.0-SNAPSHOT</version\> <type\>pom</type\> <scope\>import</scope\> </dependencies\> </dependencyManagement\>
插件管理
- Maven 提供了 dependencyManagement 元素帮助管理依赖, 类似的,Maven 也提供了 pluginManagement 元素管理插件, 该元素中配置的依赖同样不会造成实际的插件调用行为, 而 POM 文件中配置了真正的 plugin 元素, 并且 groupId 和 artifact 一致时, 才会产生实际的插件行为
- 父模块
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.1.1</version>
<executions>
<execution>
<id>attach-source</id>
<phase>verify</phase>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
</build>子模块
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifact>maven-source-plugin</artifact>
</plugin>
</plugins>
</build>
- 子模块使用了 maven-source-plugin 插件, 同时又继承了父模块的 pluginManagement 配置
集合与继承的关系
多模块的聚合与继承其实是两个概念, 其目的是完全不同的, 前者为了方便快速的构件项目, 后者主要为了消除重复配置
- 对于聚合模块来说: 它知道有哪些模块被聚合, 但是那些被聚合的模块并不知道这个聚合模块的存在
- 对于继承关系的父 POM 来说: 它不知道有哪些子模块继承于它, 但是那些子模块都必须知道自己的父模块是什么
- 在实际项目中, 一个 POM 往往即是聚合模块, 又是父模块
约定优于配置
Maven 提倡 ” 约定优于配置 ”,Maven 只需要一个简单的 POM 文件, 就可以完成清除, 构件等任务
- 源码目录:src/main/java/
- 编译输出目录为:target/classes/
- 打包方式为:jar
- 包输出目录为:target/
- Maven 此机制的来源就是超级 POM 文件, 此文件在 $MAVEN_HOME/lib/maven-model-builder-x.x.x.jar 中的 org/apache/maven/model/pom-4.0.0.xml 路径下
<repositories>
<repository>
<id>central</id>
<name>Maven Repository Swithboard</name>
<url>http://repo1.maven.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<name>Maven Plugin Repository</name>
<url>http://repo1.maven.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<updatePolicy>never</updatePolicy>
</releases>
</pluginRepository>
</pluginRepositories>
<build>
<!–> 定义了项目的主输出目录 </!–>
<directory>${project.basedir}/target</directory>
<!–> 主代码输出目录 </!–>
<outputDirectory>${project.build.directory}/classes</outputDirectory>
<!–> 最终构件的名称格式 </!–>
<finalName>${project.artifactId}-${project.version}</finalName>
<!–> 测试代码输出目录 </!–>
<testOutputDirectory>${project.build.directory}/test-
classes</testOutputDirectory>
<!–> 主源码目录 </!–>
<sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
<!–> 脚本源码目录 </!–>
<scriptSourceDirectory>src/main/scripts</scriptSourceDirectory>
<!–> 测试源码目录 </!–>
<testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
<!–> 主资源目录 </!–>
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
</resource>
</resources>
<!– > 测试资源目录 </!–>
<testResources>
<testResource>
<directory>${project.basedir}/src/test/resources</directory>
</testResource>
</testResources><!–> 核心插件版本 </!–>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.3</version>
</plugin>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.3</version>
</plugin>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.3</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.0.3</version>
</plugin>
……
</plugins>
</pluginManagement>
</build>
七. Nexus 私服
7.1 Nexus 内置的仓库
Nexus 仓库有四种类型
- group: 仓库组
- hosted: 宿主仓库
- proxy: 代理仓库
- virtual: 虚拟仓库
* Maven 可以从宿主仓库下载构件,Maven 也可以从代理仓库下载构件, 而代理仓库会简介的从远程仓库下载并缓存构件 * 而一般为了方便, 我们会从仓库组下载构件, 而仓库组没有实际的内容, 它只是管理一组实际的仓库, 当它接收到请求时, 它会转向其包含的宿主仓库或代理仓库获得实际构件的内容
仓库的 Policy 属性
- Release: 发布版
- Snapshot: 快照版
7.2 配置 Maven 从 Nexus 下载构件
- 在 POM 文件中配置仓库和插件仓库 —— 只对当前项目有效
<project>
…….
<repositories>
<repository>
<id>nexus</id>
<name>Nexus</name>
<url>http://localhost:8081/nexus/content/groupId/public/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>nexus</id>
<Name>Nexus</Name>
<url>http://localhost:8081/nexus/content/groupId/public/</url>
<release>
<enabled>true</enabled>
</release>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
…….
</project>在 settings.xml 文件中配置 — 全局有效
- settings.xml 文件并不支持直接配置 repositories 和 pluginRepositories, 但是可以通过 Maven 提供的 Profile 机制, 让用户将仓库配置到 settings.xml 中的 Profile 中
<settings\> .... <profiles\> <profile\> <id\>nexus</id\> <repositories\> <repository\> <id\>nexus</id\> <name\>Nexus</name\> <url\>http://localhost:8081/nexus/content/groupId/public/</url\> <release\> <enabled\>true</enabled\> </release\> <snapshots\> <enabled\>ture</enabled\> </snapshots\> </repository\> </repositories\> <pluginRepositories\> <pluginRepository\> <id\>nexus</id\> <Name\>Nexus</Name\> <url\>http://localhost:8081/nexus/content/groupId/public/</url\> <release\> <enabled\>true</enabled\> </release\> <snapshots\> <enabled\>true</enabled\> </snapshots\> </pluginRepository\> </pluginRepositories\> </profile\> </profiles\> <!--> 激活指定 id 的 profile </!--> <activeProfiles\> <activeProfile\>nexus</activeProfile\> </activeProfiles\> </settings\>
以上配置已经能让 Maven 项目从 Nexus 私服下载构件了, 但是 Maven 仍然会去访问中央仓库, 现在我们想要将所有请求都仅仅通过私服, 这就需要借助于 Maven 镜像了
<settings>
…..
<mirrors>
<mirror>
<id>nexus</id>
<mirrorOf>*</mirrorOf>
<url>http://localhost:8081/nexus/content/groupId/public/</url>
</mirror>
</mirrors><profiles>
<profile>
<id>nexus</id>
<repositories>
<repository>
<id>central</id>
<name>central</name>
<url>http://central</url>
<release>
<enabled>true</enabled>
</release>
<snapshots>
<enabled>ture</enabled>
</snapshots>
</repository>
</repositories><pluginRepositories>
<pluginRepository>
<id>central</id>
<Name>Nexus</Name>
<url>http://central</url>
<release>
<enabled>true</enabled>
</release>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles><!–> 激活指定 id 的 profile </!–>
<activeProfiles>
<activeProfile>nexus</activeProfile>
</activeProfiles>
</settings>
- 这里仓库和插件仓库配置的 id 都为 central, 它们将会覆盖 POM 中央仓库的配置, 并且这里的 url 已经无关紧要, 因为所有的请求都会通过镜像访问私服地址
7.3 部署构件至 Nexus
如果只是为了代理外部公共仓库, 那么 Nexus 的代理仓库就已经完全足够了, 对于另一类 Nexus 仓库 — 宿主仓库来说, 他们主要作用是存储组织内部的, 或一些无法从公共仓库获得的第三方构件, 用户可以通过配置 Maven 自动部署构件至 Nexus 的宿主仓库
使用 Maven 部署构件至 Nexus
- 日常开发生成的快照版本构件可以直接部署到 Nexus 中策略为 Snapshot 的宿主仓库中, 项目正式发布的构件则应该部署到 Nexus 中策略为 Release 的宿主仓库中
- POM 文件配置
<project>
…..
<distributeManagement>
<repository>
<id>nexus-releases</id>
<name>Nexus Release Repository</name>
<url>http://localhost:8081/nexus/content/repositories/
release/</url>
</repository>
<snapshotRepository>
<id>nexus-snapshots</id>
<name>Nexus Snapshots Repository</name>
<url>http://localhost:8081/nexus/content/repositories/
snapshots</url>
</snapshotRepository>
</distributeManagement>
…..
</project>- Nexus 的仓库对于匿名用户只是可读的, 为了能够部署构件, 还需要在 settings.xml 中配置认证信息
<settings>
….
<servers>
<server>
<id>nexus-releases</id>
<username>admin</username>
<password>admin123</password>
</server>
<server>
<id>nexus-snapshots</id>
<username>admin</username>
<password>admin123</password>
</server>
</servers>
….
</settings>
八. Profile
- 一个优秀的构件系统必须足够灵活, 它应该能够让项目在不同的环境下都能够成功的构件, 例如: 开发环境, 测试环境和产品环境, 这些环境的数据库配置不尽相同, 那么项目构建时就需要能够识别其所在的环境并正确使用配置
8.1 属性
<properties>
<springframework.version>4.3.1</springframework.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframwork.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframwork.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
- 这是最常见的 Maven 属性使用的方式, 通过 properties 元素使得用户自定义一个或多个 Maven 属性, 然后在 POM 的其他地方使用 ${属性名称}的方式来引用该属性, 这种做法的最大意义是消除重复
Maven 的属性有六类
内置属性
- ${basedir}: 表示项目根目录, 即包含 pom.xml 文件的目录
- ${version}: 表示项目的版本
POM 属性: 用户可以使用该类属性引用 POM 文件中对应元素的值
${project.artifactId}: 对应 <project><artifactId> 元素的值
- ${project.build.sourceDirectory}: 项目的主源码目录. 默认为 /src/main/java/
- ${project.build.testSourceDirectory}: 项目的测试代码源码目录. 默认为 /src/test/java/
- ${project.build.directory}: 项目构件输出目录. 默认为 /target
- ${project.build.outputDirectory}: 项目主代码编译输出目录. 默认为 /target/classes
- ${project.build.testOutputDirectory}: 项目测试代码编译输出目录. 默认为 /target/test-classes
- ${project.build.groupId}: 项目的 groupId
- ${project.build.artifactId}: 项目的 artifactId
- ${project.build.build.finalName}: 项目打包输出文件的名称, 默认为 ${project.artifactId}
-${project.version}
- 自定义属性:<properties> 元素下自定义的 Maven 属性
- settings 属性: 与 POM 属性同理, 用户使用 settings. 来引用, 如:${settings.localRepository}指向用户本地仓库的地址
Java 系统属性: 所有 Java 系统属性都可以使用 Maven 属性引用, 如 ${user.home}指向用户目录
- 可以通过 mvn help:system 查看所有的 java 系统属性
环境变量属性: 所有环境变量都可以用 env. 来引用, 例如:${env.JAVA_HOME}
- 可以通过 mvn help:system 查看所有的环境变量
8.2 资源过滤
- 在不同的开发环境中, 项目的源码应该使用不同的方式进行构件, 最常见的就是数据库的配置了, 在开发中, 有些项目会在 src/main/resources/ 目录下放置不同环境下的数据库配置
database.jdbc.driverClass = com.mysql.jdbc.Driver
database.jdbc.connectionURL = jdbc:mysql://localhost:3306/dev
database.jdbc.username = dev
database.jdbc.password = dev-passwd
database.jdbc.driverClass = com.mysql.jdbc.Driver
database.jdbc.connectionURL = jdbc:mysql://localhost:3306/test
database.jdbc.username = test
database.jdbc.password = test-passwd- 为了应对环境的变化, 我们可以使用 Maven 属性将这些会发生变化的部分提取出来
database.jdbc.driverClass = ${db.driver}
database.jdbc.connectionURL = ${db.url}
database.jdbc.username = ${db.username}
database.jdbc.password = ${db.password}在 settings.xml 中通过 profile 定义不同环境下的配置数据
<profiles>
<profile>
<id>dev</id>
<properties>
<db.driver>com.mysql.jdbc.Driver</db.driver>
<db.url>jdbc:mysql://localhost:3306/dev</db.url>
<db.username>dev</db.username>
<db.password>dev-passwd</db.password>
</properties>
</profile><profile>
<id>test</id>
<properties>
<db.driver>com.mysql.jdbc.Driver</db.driver>
<db.url>jdbc:mysql://localhost:3306/test</db.url>
<db.username>test</db.username>
<db.password>test-passwd</db.password>
</properties>
</profile>
</profiles>
需要注意的是:
- Maven 属性默认只会在 POM 文件中才会被解析, 也就是说 ${db.username}放到 POM 文件中会变成 dev 或 test, 但是在 src/main/resources/ 目录下仍然还是 ${db.username}, 因此, 需要让 Maven 解析资源文件中的 Maven 属性
- 资源文件处理的是 maven-reosurces-plugin 做事, 它默认的行为只是将项目主资源文件复制到主代码编译输出目录
开启资源过滤
- Maven 默认的主资源目录和测试目录的定义是在超级 POM 文件中
<resources\> <resource\> <directory\>${project.basedir}/src/main/resources</directory\> <filtering\>true</filtering\> </resource\> </resources\> <testResources\> <testResource\> <directory\>${project.basedir}/src/test/resources</directory\> <filtering\>true</filtering\> </testResource\> </testResources\>
- 通过 mvn clean install -Pdev : 通过 mvn 的 - P 参数表示在命令行激活一个 profile=dev 的 profile
8.3 Profile
激活 Pofile
命令行激活
用户可以使用 mvn 命令行参数 -P 加上 profile 的 id 来激活 profile, 多个 id 之间逗号分隔
- mvn clean install -Pdev,-Ptest
settings 文件显示激活
- 如果用户希望某个 profile 默认一直处于激活状态, 就可以配置 settings.xml 文件的 activeProfile 元素, 表示其配置的 profile 对所有项目都处于激活状态
<settings>
…..
<activeProfiles>
<activeProfile>dev</activeProfile>
</activeProfiles>
…..
</settings>系统属性激活
- 用户可以配置当前某系统属性存在时, 自动激活 profile
<profiles>
<profile>
<activation>
<property>
<name>test</name>
</property>
</activation>
…..
</profile>
</profiles>- 用户可以配置当前某系统属性存在且等于 a 时, 自动激活 profile
<profiles>
<profile>
<activation>
<property>
<name>test</name>
<value>a</value>
</property>
</activation>
…..
</profile>
</profiles>用户可以在命令行声明系统属性
- mvn clean install -Dtest=x
操作系统环境变量激活
- Profile 还可以根据操作系统环境激活
<profiles>
<profile>
<activation>
<os>
<name>Windows10</name>
</os>
</activation>
…..
</profile>
</profiles>文件存在与否激活
- Maven 可以根据项目中某个文件是否存在来决定是否激活 profile
<profiles>
<profile>
<activation>
<file>
<missing>a.properties</missing>
<exists>b.properties</exists>
</file>
</activation>
…..
</profile>
</profiles>默认激活
- 用户可以在定义 profile 的时候指定其默认激活
<profiles>
<profile>
<id>dev</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
…..
</profile>
</profiles>以上激活优先级从上之下依次减小
- 可以通过 mvn help:active-profiles 来查看当前激活的 profile
- 可以通过 mvn help:all-profiles 来查看所有的 profile
profile 的种类
根据具体的需要, 可以在一下位置声明 profile
Maven
一.Maven 简介
1.1 何为 maven
Maven 可翻译为 ” 知识的积累 ” or” 专家 ”, 是一款成功的开源跨平台的项目管理工具, 无论小型的开源类库项目, 还是大型的企业级应用; 无论传统的瀑布式开发, 还是流行的敏捷模式,Maven 都能大显身手.
1.1.1 构建工具
我们一直在不停的寻找避免重复的方法, 设计的重复, 编码的重复, 文档的重复, 当然还有构建的重复.Maven 最大化的消除了构建的重复, 抽象了构建生命周期, 并且为绝大部分的构建任务提供了已实现的插件, 我们不需要再定义过程, 甚至不需要去实现这些过程中的一些任务, 只需要遵循 Maven 中的约定.
同时 Maven 帮助我们标准化构建过程, 以前十个项目可能有十种构建方式, 有了 Maven 后, 所有项目的构建命令都是一直且简单的. 因此 Maven 作为一个构建工具:
- 可以帮我们自动化构建,
- 可以帮我们抽象构建过程
- 提供构建任务是实现
- 跨平台
- 对外提供一直的操作接口
1.1.2 不仅仅是构建工具
Maven 不仅是构建工具, 还是一个依赖管理工具和项目信息管理工具, 提供中央仓库来帮忙我们自动下载构建, 通过引入一套经纬机制来系统准确的定位每一个构建(artifact).
1.1.3 Maven
在 Maven 之前, 有过程式的 Make 和 Ant, 开发者需要显示的指定每一个目标, 以及完成该目标所需要执行的任务. 针对每一个项目, 开发者都需要重新编写这一过程, 而其中就隐含着大量重复.
而 Maven 是声明式的, 项目构建过程和过程各个阶段所需的工作都由插件实现, 并且大部分插件都是现成的, 开发者只需要声明项目的基本元素,Maven 就执行内置的, 完整的构建过程.
二.Maven 的使用
2.1 pom 文件
Maven 项目的核心是 pom.xml,POM(Project Object Model, 项目对象模型)定义了项目的基本信息, 用于描述项目如何构建, 声明项目依赖等.
<modelVersion>4.0.0</modelVersion>:modelVersion 指定了当前 Pom 模型的版本, 固定为 4.0.0
<groupId>com.lsy</groupId>:groupId 定义了项目属于哪个组, 这个组往往和项目所在的组织和公司相关
<artifactId>hello-world</artifactId>:artifactId 定义了当前 Maven 项目在组中唯一的 ID
<version>1.0-SNAPSHOT</version>:version 指定了 Hello World 项目当前的版本, 其中 SNAPSHOT 意为快照, 说明该项目还处于开发中
<name>Maven Hello World Project</name>:name 元素声明了一个对于用户更为友好的项目名称. 非必须
当运行 mvn clean compile 命令:clean 告诉 Maven 清理输出目录 target/,compile 告诉 Maven 编译项目主代码, 从输出中看到 Maven 首先执行 clean:clean 任务, 删除 target/ 目录(默认情况下,Maven 构建的所有输出都在 target 目录中, 接着执行 resources:resources 任务(未定义项目资源), 最后执行 compiler:compile 任务, 将项目主代码编译至 target/classes 目录
其中 clean:clean,resources:resources 和 compiler:compile 对应了 Maven 插件及插件目标, 比如 clean:clean 是 clean 插件的 clean 目标,compiler:compile 是 compiler 插件的 compile 目标
2.2 测试
首先添加 Junit 依赖
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
<dependencies>
其中 scope 元素为依赖范围, 若依赖范围为 test 则表示该依赖只对测试有效, 如果不声明依赖范围, 则默认是 compile, 表示该依赖对主代码和测试代码均有效
当运行 mvn clean test 命令,Maven 实际执行的不止 test 任务, 而是 clean:clean,resources:resources,compiler:compile,resources:testResources 以及 compiler:testCompile, 即在执行测试之前, 会先自动执行项目资源处理, 主代码编译, 测试资源处理, 测试代码编译等工作, 这是 Maven 生命周期的一个特性.
由于 Maven 核心插件之一 compiler 插件默认支持 Java1.3, 因此需要配置插件支持 Java1.8
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8<source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
2.3 打包和运行
将项目进行编译, 测试之后, 下一个重要的步骤就是打包(package), 当没有指定打包类型时, 默认打包类型是 jar, 当执行 mvn clean package 命令,Maven 会在打包之前进行编译, 测试等操作, 之后通过 jar:jar 任务负责打包, 实际上就是 jar 插件的 jar 目标将项目主代码打包成一个 hello-world-1.0-SNAPSHOT.jar 的文件
当其他项目需要直接引用这个 jar 时, 接下来需要一个安装的步骤, 执行 mvn clean install, 此命令执行了安装任务 install:install, 将项目输出的 jar 安装到了 Maven 本地仓库中, 而构件只有被下载到本地仓库后, 才能由 Maven 项目使用.
2.4 使用 Archetype 生成项目骨架
mvn archetype:generate, 当运行插件时, 格式为:groupId:artifactId:version:goal, 默认使用最新的稳定版
三. 坐标和依赖
3.1 坐标
- Maven 的一大功能还是管理项目依赖, 为了能自动化解析任何一个 Java 构件,Maven 就必须将它们唯一标识, 这就依赖管理的底层基础 — 坐标
- 在实际生活中, 我们可以将地址看作是一种坐标, 省, 市, 区, 街道等一系列信息可以唯一标识城市中的任意居住地址, 对应在 Maven 的世界中拥有非常巨大的构件, 也就是平常使用的一些 jar,war 等文件
Maven 定义了这样一规则: 世界上任意一个构建都可以使用 Maven 坐标唯一标识,Maven 坐标的元素包括
- groupId: 定义当前 Maven 项目隶属公司的实际项目
- artifactId: 该元素定义实际项目中的一个 Maven 模块, 推荐做法使用实际项目名称为 artifact 的前缀, 便于寻找实际构建
- version: 该元素定义 Maven 项目当前所处的版本
- package: 该元素定义 Maven 项目的打包方式, 默认为 jar 包
- classifier: 该元素用来帮助定义构建输出一些附属构建, 附属构件与主构件对应
<groupId>org.sonatype.nexus</groupId> <artifactId>nexus-indexer</artifactId> <version>2.0.0</version> <packaging>jar</packaging> <!--> 1. 以上 5 个元素,groupId,artifactId,version 是必须定义的,packaging 是可选的, 而 classifier 是不能直接定义的 2. 项目构件的文件名格式:artifactId-version[-classifier].packaging
3.2 依赖
3.2.1 依赖的配置
项目要引用 Maven 中的构建, 就需要在 pom 文件中, 通过坐标来使用依赖, 在根元素 project 下的 dependencies 可以包含一个或多个 dependency 元素, 用以声明一个或多个项目依赖
- groupId,artifactId 和 version: 依赖的基本坐标, 对于任何一个依赖来说, 基本坐标是最重要的
- type: 依赖的类型, 对应项目坐标定义的 packaging, 一般不必声明, 默认为 jar
- scope: 依赖的范围
- optional: 标记依赖是否可选
- exclusions: 用来排除传递性依赖
3.2.2 依赖的范围
- Maven 项目在编译项目主代码时需要使用一套 classpath, 如编译项目主代码时需要使用 spring-core, 该文件以依赖的方式被引入到 classpath 中. 其次,Maven 在编译和执行测试的时候会使用另外一套 classpath, 依赖同样会引入到相应的 classpath 中, 最后在运行 Maven 项目时, 又会使用一套 classpath
依赖范围就是用来控制依赖与三种 classpath(编译 classpath, 测试 classpath, 运行 classpath)的关系
- compile: 编译依赖范围, 没有指定时, 为默认依赖范围. 使用此依赖范围的 Maven 依赖, 对于编译, 测试运行三种 classpath 都有效, 典型的例子为:spring-core, 在编译, 测试, 运行阶段都需要使用该依赖
- test: 测试依赖范围, 使用此依赖范围的 Maven 依赖, 只对于测试的 classpath 有效, 在编译主代码或者运行项目时将无法使用此类依赖, 典型的例子就是 JUnit, 它只有在编译器测试代码及运行测试的时候才需要
- provided: 已提供依赖范围, 使用此依赖范围的 Maven 依赖, 对于编译和测试 classpath 有效, 但在运行时无效, 典型的例子就是 servlet-api, 编译和测试项目的时候需要该依赖, 但在运行项目的时候, 由于容器已经提供, 就不需要 Maven 重复引入了
- runtime: 运行时依赖范围, 使用此依赖范围的 Maven 依赖, 对于测试和运行 classpath 有效, 但在编译主代码时无效, 典型的例子是 JDBC 驱动实现, 项目主代码的编译只需要 JDK 提供的 JDBC 接口, 只有在执行测试或运行项目的时候才需要实现上述接口的具体 JDBC 驱动
system: 系统依赖范围, 该依赖与三种 classpath 的关系, 和 provided 依赖范围完全一致, 但是, 使用 system 范围的依赖时必须通过 systemPath 元素显示地指定依赖文件的路径, 由于此类依赖不是通过 Maven 仓库解析的, 而且往往与本机系统绑定, 可能造成构建的不可移植性
<dependency> <groupId>javax.sql</groupId> <artifactId>jdbc-stdext</artifactId> <version>2.0</version> <scope>system</scope> <systemPath>${java.home}/lib/rt.jar</systemPath> </dependency>
- import: 导入依赖范围, 该依赖范围不会对三种 classpath 产生实际的影响, 主要用于导入其他 pom 文件中的 dependencyManagement 元素对于依赖版本约束的内容
3.2.3 传递性依赖
在使用 Maven 依赖时, 如 Spring Framework, 此依赖又会依赖其他的开源库, 因此实际中往往会下载一个很大的如 spring-framework-2.5.6-with-dependencies.zip 包, 这样往往就引入了很多不必要的依赖, 而 Maven 的传递性依赖机制就可以很好的解决这一问题
- 当 A 项目引入一个 compile 范围的 B 依赖, 而 B 依赖中有一个 compile 范围的 C 依赖, 那么 C 依赖同样会成为 A 的 compile 范围依赖
传递性依赖和依赖范围
- 假设 A 依赖于 B,B 依赖于 C, 那么 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖
- 当第二直接依赖是 compile 的时候, 传递性依赖与第一直接依赖范围一致
- 当第二直接依赖是 test 的时候, 依赖不会得以传递
- 当第二直接依赖是 provided 的时候, 只传递第一直接依赖范围为 provided 的依赖, 且传递性依赖范围为 provided
- 当第二直接依赖是 runtime 的时候, 传递性地依赖的范围与第一直接依赖的范围一致, 但 compile 例外, 此时传递性依赖的范围为 runtime
3.2.4 可选依赖
假如有这样一个依赖关系,A 依赖于 B,B 依赖于 X 和 Y,B 对于 X 和 Y 的依赖都是可选依赖, 根据传递性依赖的定义, 如果这三个依赖的范围都是 compile, 那么 X,Y 就是 A 的 compile 范围传递性依赖, 但是由于 X,Y 都是可选依赖, 所以依赖不会得以传递, 因此 X,Y 不会对 A 有任何影响
为什么会有可选依赖这一特性呢?
- 当 B 实现了两个特性, 特性一依赖于 X, 特性二依赖于 Y, 并且这两个特性是互斥的, 用户不可能同时使用两个特性, 比如 B 是一个持久层隔离工具包, 支持多种数据库, 在使用这个工具包的时候, 只会依赖一种数据库
<dependencies> <dependency> <groupId>mysql</groupId> <artifact>mysql-connector-java</artifact> <version>5.6.0</version> <optional>true</optional> </dependency> <dependency> <groupId>postgresql</groupId> <artifactpostgresql</artifact> <version>8.4-701.jdbc3</version> <optional>true</optional> </dependency> </dependencies>
- 以上使用 <optional> 元素表示两个依赖为可选依赖, 他们只会对当前项目 B 产生影响, 当其他项目依赖于 B 时, 这两个依赖不会被传递, 所以当 A 依赖于 B 项目时, 如果要使用 mysql 数据库, 那么需要显式的声明 mysql-connector-java 这一依赖
- 关于可选依赖, 在理想的情况下, 是不应该使用可选依赖的, 使用可选依赖的原因是某一个项目中实现了多个特性, 而根据单一职责原则, 应该针对不同的特性分别创建一个 Maven 项目, 用户根据需要选择使用其中某一个依赖
3.2.5 排除依赖
传递性依赖会给项目隐式的引入很多依赖, 这极大的简化了项目依赖的管理, 但是有时候这种特性䧥带来问题
例如, 当前项目有一个第三方依赖, 而这个依赖由于某些原因依赖了另一个类库的 SNAPSHOT, 那么整个 SNAPSHOT 就会成为当前项目的传递性依赖, 而 SNAPSHOT 的不稳定性会直接影响到当前的项目, 这时候就需要排除掉该 SNAPSHOT, 并且在当前项目中声明该类库的某个正式发布版
<dependencies> <dependency> <groupId>com.lsy.myproject</groupId> <artifactId>myproject-a</artifactId> <version>1.0.0</version> <exclusions> <exclusion> <groupId>com.lsy.myproject</groupId> <artifactId>myproject-b</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.lsy.myproject</groupId> <artifactId>myproject-b</artifactId> <version>1.1.0</version> </dependency> </dependencies>
3.2.6 归类依赖
当一些依赖来自同一个项目的不同模块, 这些依赖的版本都应该是相同的, 将来升级也是一起升级, 如 Spring Framework, 这时可以使用 properties 元素定义 Maven 属性
<properties> <springframework.version>4.2.1</springframework.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <verison>${springframework.version}</verison> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <verison>${springframework.version}</verison> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <verison>${springframework.version}</verison> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <verison>${springframework.version}</verison> </dependency> </dependencies>
3.2.7 优化依赖
在软件开发过程中, 通常会通过重构等方式不断优化自己代码, 同样, 对于 Maven 项目的依赖也需要对其进行优化
- 去除多余的依赖
- 显示声明某些必要的依赖
Maven 会自动解析所有项目的直接依赖和间接依赖, 并且根据规则判断每个依赖范围, 对于一些依赖冲突, 也能进行调整, 以确保任何一个构建只有唯一的版本在依赖中存在, 此称为解析依赖
- mvn dependency:list 查看当前项目的已解析依赖
- mvn dependency:tree 以树结构查看已解析依赖
mvn dependency:analyze 解析依赖
- Used undeclared dependencies: 指项目中使用到的, 但是没有显示声明的依赖, 这种依赖意味着潜在的风险, 当前项目直接在使用它们, 所以需要显示声明任何项目中直接用到的依赖
- Unused declared dependencies: 指项目中未使使用的. 但显示声明的依赖
四. 仓库
- 之前已经介绍了 Maven 的坐标和依赖, 坐标和依赖是任何一个构件在 Maven 世界中的逻辑表示方式, 而构建的物理表示方式是文件,Maven 通过仓库来同一管理这些文件
-
在 Maven 世界中, 任何一个以依赖, 插件或项目构建的输出, 都可以成为构件
- 例如:log4j-1.2.15.jar,maven-compiler-plugin-2.0.2.jar 或项目打包后的 myproject-1.0.0-SNAPSHOT.jar
- 在一台工作站上, 可能会有几十个项目, 所有项目在 /lib 目录下都会有自己所需的依赖包, 而这些依赖中都有大量的重复, 每个项目各自存储自己所需的依赖包不仅造成磁盘空间的浪费, 而且也难于同一管理, 文件的复制等操作
- 得益于坐标机制, 任何 Maven 项目使用任何一个构建的方式都是相同的, 因此,Maven 可以在某个位置统一存储所有 Maven 项目共享的构件, 这个统一的位置就是仓库, 为了实现重用, 项目构建完毕后生成的构建也可以安装或部署到仓库中
4.1 仓库的布局
任何一个构建都有其唯一的坐标, 根据这个坐标可以定义其在仓库中的唯一存储路径, 这就是 Maven 的仓库布局方式, 如 log4j:log4j:1.2.15 这一依赖, 其对应的仓库路径为 log4j/log4j/1.2.15/log4j-1.2.15.jar
- 路径与坐标的关系为:groupId/artifactId/version/artifactId-verision.packaging
4.2 仓库的分类
对于 Maven 来说, 仓库分为两类
- 本地仓库
远程仓库
- 中央仓库:Maven 核心自带的仓库服务器, 它包含了绝大部分开源的构件
- 私服: 另一种特殊的远程仓库, 为了节省宽带和时间, 应该在局域网内部构建一个私有仓库服务器, 用其代理所有外部的远程仓库, 内部项目部署到私服上供其它项目使用
- 其他公共库
- 当 Maven 根据坐标寻找构件时, 它首先会查看本地仓库, 如果本地仓库存在此构件, 则直接使用, 如果本地仓库不存在此构件, 或者需要查看是否有更新的构件版本,Maven 就会去远程仓库查找, 发现需要的构件后下载到本地仓库再使用, 若本地和远程都没有, 则报错
4.3 本地仓库
一般来说, 在 Maven 项目目录下, 没有注入 lib/ 这样用来存放依赖文件的目录, 当 Maven 执行编译或测试时, 如果需要使用依赖文件, 它总是基于坐标使用本地仓库的依赖文件
默认情况, 在用户目录下路径名为.m2/repository/ 的仓库目录, 当想自定义本地仓库目录地址时, 可以编辑~/.m2/setting.xml, 设置 localRepository 元素来指定仓库地址
<settings> <localRepository>D:/java/repository/</localRepository> </settings>
- 默认情况下,~/.m2/settings.xml 文件是不存在的, 用户需要从 Maven 安装目录复制 $M2_HOME/conf/settings.xml 文件再编辑, 建议不要直接修改全局目录的 settings.xml, 而是在用户目录下进行修改
一个构建只有在本地仓库中, 才能由其它 Maven 项目使用
- 依赖 Maven 从远程仓库下载到本地仓库中
- 将本地项目的构件安装到 Maven 本地仓库中 mvn clean install
4.4 远程仓库
安装好 Maven 后, 如果不执行任何 Maven 命令, 本地仓库目录是不存在的, 只有输入第一条 Maven 命令后,Maven 才会创建本地仓库, 并根据配置和需要, 从远程仓库下载至本地仓库
- 这就好比藏书, 本地仓库好比书房, 远程仓库好比书店, 我需要读书时先去书房找, 当书房没有时, 就去书店买回放到书房, 并且一般对每个人来说, 书房只有一个, 而外面的书店可以有多个
4.4.1 中央仓库
最原始的本地仓库是空的, 所以 Maven 必须知道至少一个可用的远程仓库, 才能在执行 Maven 命令时下载到需要的构建, 中央仓库就是这样一个默认的远程仓库, 可以通过解压工具打开 $M2_HOME/lib/maven-model-builder-3.0.jar 中的 org/apache/maven/model/pom-4.0.0.xml 文件
<repositories> <repository> <id>central</id> <name>Maven Repository Switchboard</name> <url>http://repo1.maven/org/maven2</url> <layout>default</layout> <snapshots> <enable>false</enable> </snapshots> </repository> </repositories>
- 这段配置是所有 Maven 项目都会继承的超级 Pom 文件, 这段配置使用 id central 对中央仓库进行唯一标识, 其名称为 Maven Repository Switchboard, 它使用 default 仓库布局, 也就是在之前介绍的仓库布局, 最后 snapshots 元素表示不从该仓库下载快照版
4.4.2 私服
私服是一种特殊的远程仓库, 它是架设在局域网内的仓库服务, 私服代理广域网上的远程仓库, 供局域网内的 Maven 用户使用, 当 Maven 需要下载构建的时候, 它从私服请求, 如果私服上不存在该构件, 则从外部的远程仓库下载, 缓存在私服上之后, 再为 Maven 的下载提供服务
- 此外, 一些无法从外部下载的构件也可以从本地上传到私服上供大家使用
私服的好处
- 节省外网带宽: 建立私服同样可以减少组织自己的开支, 大量的对于外部仓库的重复请求会消耗很大的带宽, 利用私服代理外部仓库之后, 对外的重复构件下载便可以消除, 即降低外网带宽的压力
- 加速 Maven 构件: 不停的连接请求外部仓库是十分耗时的, 但 Maven 的一些内部机制 (如快照更新检查井) 要求 Maven 在执行构建时不停的检查远程仓库数据, 因此, 当项目配置很多外部仓库时, 构建速度就会降低
- 部署第三方构件: 当某个构件无法从任何一个外部远程仓库获取, 建立私服之后, 便可以将这些构件部署到这个内部的仓库中, 供内部的 Maven 项目使用
- 提高稳定性, 增强控制:Maven 构建高度依赖远程仓库, 因此, 大哥 Internet 不稳定的时候,Maven 构建也会变得不稳定, 甚至无法构建, 使用私服后, 即是短暂时没有 Internet 连接, 由于私服中有大量缓存,Maven 依然可以正常运行, 并且私服中有很多额外的权限功能控制
- 降低中央仓库的负荷: 每天中央仓库都需要面对大量的下载请求, 使用私库可以降低对于中央仓库的负荷
远程仓库的配置
很多情况下, 默认的中央仓库无法满足项目的需求, 可能项目需要的构件存在于另一个远程仓库中, 可以通过 Pom/settings 文件中来配置该仓库
<!--> POM 文件 </!--> <project> ..... <repositories> <repository> <id>jboss</id> <name>JBoss Repository</name> <url>http://repository.jboss.com/maven2/</url> <release> <enabled>true</enabled> </release> <snapshots> <enabled>false</enabled> </snapshots> <layout>default</layout> </repository> </repositories> </project> <!--> settings 文件 </!--> <profiles> <profile> <id>dev</id> <activation> <activatedByDefault>true</activatedByDefault> </activation> <repositories> <repository> <id>jboss</id> <name>JBoss Repository</name> <url>http://repository.jboss.com/maven2/</url> <release> <enabled>true</enabled> </release> <snapshots> <enabled>false</enabled> </snapshots> <layout>default</layout> </repository> </repositories> </profile> </profiles>
- 在 repositories 元素下, 可以使用 repository 子元素声明一个或多个远程仓库, 并且声明一个 id, 任何一个仓库声明的 id 必须是唯一 id
- Maven 自带的中央仓库使用的 id 为 central, 如果其他仓库使用该 id, 则会覆盖中央仓库的配置, 该配置中的 url 指向了仓库的地址
该配置中的 release 和 snapshots 元素用来控制 Maven 对于发布版构件和快照版构件的下载, 对于 release 和 snapshots 元素来说除了 enabled 子元素, 还有 updatePolicy 和 checksumPolicy 元素
updatePolicy: 用来配置 Maven 从远程仓库检查更新的频率, 默认为 daily
- daily: 每天
- never: 从不
- always: 每次
- interval :X : 每隔 X 分钟一次
checksumPolicy: 配置 Maven 检查文件失败时的策略, 默认为 warn
- fail:Maven 遇到验证和错误就让构建失败
- warn:Maven 遇到验证和错误就发出警告信息
- ignore:Maven 遇到验证和错误时完全忽略
<snapshots> <enabled>true</enabled> <updatePolicy>daily</updatePolicy> <checksumPolicy>ignore</checksumPolicy> </snapshots>
远程仓库的验证
- 大部分远程仓库无需认证就可以访问, 但出于安全考虑, 我们需要提供一些认证信息才能访问一些远程仓库
- 配置认证信息和配置仓库信息不同, 仓库信息可以直接配置在 POM 文件中, 但是认证信息必须配置在 settings.xml 文件中
其中 server 元素的 id 必须与 POM 文件中需要认证的 repository 元素的 id 完全一致
<settings> ...... <servers> <server> <id>my-project</id> <username>user</username> <password>password</password> </server> </servers> ....... </settings>
部署至远程仓库
私服一大作用就是部署第三方构件, 包括组织内部生成的构件以及无法从外部仓库直接获取的构件, 无论是日常开发中生成的构件, 还是正式版本发布的构件, 都需要部署到仓库中
- distributionManagement 包含 repository 和 snapshotRepository 子元素, 前者表示发布版构件的仓库, 后者表示快照版的仓库
往远程仓库部署构件时, 往往需要认证, 认证配置需在 settings.xml 中创建一个 server 元素, 其 id 与仓库的 id 匹配, 无论从远程仓库下载构件, 还是部署构件至远程仓库, 当需要认证时, 配置方式都是一样的
<!--> 在 POM 文件中配置 distributionManagement </!--> <project> ..... <distributionManagement> <repository> <id>jboss-release</id> <name>JBoss Release Repository</name> <url>http://192.168.1.99/content/repository/jboss-release</url> </repository> <snapshotRepository> <id>jboss-snapshots</id> <name>JBoss Snapshots Repository</name> <url>http://192.168.1.99/content/repository/jboss-snapshots</url> </snapshotRepository> </distributionManagement> ..... </project>
4.5 快照
- 在 Maven 的世界中, 任何一个项目或者构件都必须有自己的版本, 版本的值可能是 1.0.0,1.3-alpha-4,2.0,2.1-SNAPSHOT 或者 2.1-20191214.221414-13, 其中 1.0.0,1.3-alpha- 4 和 2.0 是稳定的发布版本, 而 2.1-SNAPSHOT 和 2.1-20091214.221414-13 是不稳定的快照版本
- 对于一个稳定的版本, 如果仓库中已经包含, 那么 Maven 就不会再去对照远程仓库进行更新, 除非每次执行 Maven 命令前, 清除本地仓库中等待稳定版本, 而对于一个正在迭代的项目, 如果要实时更新版本的内容就需要频繁的修改新的版本名称, 这样是对版本号的滥用
- 针对这种情况, 使用快照版时,Maven 会自动为构件打上时间戳, 因此,Maven 就能随时找到仓库中该构建最新版本的文件, 一旦有新的更新, 就会去同步到本地仓库. 当项目经过完善的测试后需要发布的时候, 再将快照版本更改为发布版本
- 快照版本只应该在组织内部的项目或模块之间依赖使用, 因为这时, 组织对这些快照版本的依赖具有完全的理解和控制权, 项目不应该依赖任何组织外部的快照版本依赖, 由于快照版本的不稳定性, 随时可能发生变化, 这样的依赖会有潜在的危险
4.6 从仓库解析依赖的机制
当本地仓库没有依赖构件的时候,Maven 会自动从远程仓库下载, 当依赖版本为快照版本时,Maven 会自动找到最新的快照, 这背后的依赖机制可以概括如下
- 当依赖的范围时 system 的时候,Maven 直接从本地文件系统解析构件
- 根据依赖坐标计算仓库路径后, 尝试从本地仓库寻找构件, 如果发现相应的构件, 则解析成功
- 在本地仓库不存在相应构件的情况下, 如果依赖的版本时显式的发布版本构件, 如 1.2,2.1-beta- 1 等, 则遍历所有的远程仓库, 发现后, 下载并解析使用
- 如果依赖的版本时 RELEASE 或 LATEST, 则基于更新策略读取所有远程仓库的元数据 groupId/artifact/maven-metadata.xml, 将其与本地仓库的对应元数据合并后, 计算出 RELEASE 或 LATEST 真实的值, 然后基于这个真实的值检查本地仓库和远程仓库
- 如果依赖的版本是 SNAPSHOT, 则基于更新策略读取所有远程仓库的元数据 groupId/artifactId/maven-metadata.xml, 将其与本地仓库的对应数据合并后, 得到最新的快照版本的值, 然后基于该值检查本地仓库, 或者从远程仓库下载
如果最后解析得到的构件版本是时间戳格式, 如 1.4.1-20191104.121455-8, 则复制其时间戳格式的文件至非时间戳格式, 如 SNAPSHOT, 并使用该非时间戳格式的构件
<!--> 基于 groupId 和 artifactId 的 maven-metadata.xml </!--> <?xml version="1.0" encoding="UTF-8"?> <metadata> <groupId>org.sonatype.nexus</groupId> <artifactId>nexus</artifactId> <versioning> <latest>1.4.2-SNAPSHOT</latest> <release>1.3.7</release> <versions> <version>1.3.5</version> <version>1.3.6</version> <version>1.3.7</version> <version>1.4.0-SNAPSHOT</version> <version>1.4.1-SNAPSHOT</version> <version>1.4.2-SNAPSHOT</version> </versions> <lastUpdated>20191214221133</lastUpdated> </versioning> </metadata>
该 XML 文件列出了仓库中存在的构件所有可用的版本, 同时 latest 元素指向了这些版本中最新的那个版本 1.4.2-SNAPSHOT, 而 release 元素指向了这些版本中最新的发布版本 1.3.7,Maven 通过合并多个远程仓库及本地仓库的元数据, 就能计算出基于所有仓库的 latest 和 release
- 需要注意的是, 在依赖声明使用 LATEST 和 RELEASE 是不推荐的做法, 因为 Maven 随时可能解析到不同的构件, 且 Maven 不会明确告诉用户这样的变化
4.7 镜像
如果仓库 X 可以提供仓库 Y 存储的所有内容, 那么就可以认为 X 是 Y 的一个镜像, 由于地理位置的因素, 中央仓库的下载速度会比较慢, 这时我们可以配置 Maven 使用镜像来代替中央仓库, 编辑 settings.xml
<settings> ...... <mirrors> <mirror> <id>maven.net.cn</id> <name>one of the central mirror in china</name> <url>http://maven.net.cn/content/groups/public/</url> <mirrorOf>central</mirrorOf> </mirror> </mirrors> ...... </settings>
<mirrorOf> 的值为 central, 表示该配置为中央仓库的镜像, 任何对于中央仓库的请求都会转至该镜像, 用户也可以使用同样的方法配置其他仓库的镜像, 另外三个元素 id,name,url 与一般仓库配置无异, 表示该镜像仓库的唯一标识, 名称以及地址
- 若该镜像要进行验证, 即基于该 id 配置仓库认证
<settings> ..... <mirrors> <mirror> <id>internal-repository</id> <name>Internal Repository Mananger</name> <url>http://192.168.1.100/maven2/</url> <mirrorOf>*</mirrorOf> </mirror> </mirrors> ..... </settings>
以上 mirrorOf 元素的值为 *, 表示该配置是所有 Maven 仓库的镜像, 对于任何远程仓库的请求都会被转至该指定的仓库, 如果镜像仓库需要验证, 则配置一个 id 为 internal-repository 的 <server> 即可
- <mirrorOf>*<mirrorOf>: 匹配所有远程仓库
- <mirrorOf>external:*<mirrorOf>: 匹配所有远程仓库, 使用 localhost 的除外, 使用 file:// 协议的除外, 也就是说, 匹配所有不在本机上的远程仓库
- <mirrorOf>repo1,repo2<mirrorOf>: 匹配仓库 repo1 和 repo2, 用逗号分隔多个仓库
- <mirrorOf>*,! repo1<mirrorOf>: 匹配所有的远程仓库, 除 repo1 外, 使用感叹号将仓库从匹配中排除
- 需要注意的是, 由于镜像仓库完全屏蔽了被镜像仓库, 当镜像仓库不稳定或停止服务时,Maven 仍将无法访问被镜像仓库, 因此无法下载构件
五. 生命周期和插件
5.1 Maven 的生命周期
在 Maven 出现之前, 项目构建的生命周期就已经存在, 但是不同的公司和开发人员虽然同样在做构件工作, 其对于不同的项目却不能够重用, 只能重新定制开发, 而 Maven 的生命周期就是为了对所有的构件过程进行抽象和统一
- 这个生命周期包含了项目的清理, 初始化, 编译, 测试, 打包, 集成测试, 验证, 部署和站点生成等几乎所有的构件步骤, 也就是说几乎所有项目的构件, 都能映射到这样一个生命周期上
Maven 的生命周期是抽象的, 这意味着生命周期本身不做任何实际的工作, 在 Maven 的设计中, 实际的任务 (如编译主代码) 都交由插件完成, 这种思想和设计模式的模方法类似, 在父类中定义算法的整体结构, 子类可以通过实现或重写父类的方法来控制实际的行为
public abstract class Template{public void build(){initialize(); compile(); test(); packagee(); integrationTest(); deploy();} protect abstract void initialize(); protect abstract void compile(); protect abstract void test(); protect abstract void packagee(); protect abstract void integrationTest(); protect abstract void deploy();}
- 在 Maven 的生命周期中抽象了各个步骤, 定义了他们的次序, 但是没有提供具体的实现, 而通过插件机制为每个构件步骤绑定一个或多个插件行为, 而且 Maven 为大多数构件步骤都绑定了默认的插件, 例如, 针对编译的 maven-compiler-plguin, 针对测试的 maven-surefire-plugin 等
- Maven 定义的生命周期和插件机制一方面保证了所有 Maven 项目有一致的构件标准, 另一方面又通过默认的插件简化和稳定实际项目的构件, 此外, 该机制还提供了足够的扩展, 用户可以通过配置现有的插件或自定义插件来自定义构件行为
三套声明周期
Maven 拥有三套相互独立的生命周期, 他们分别为 clean,default 和 site, 每个生命周期包含一些阶段, 这些阶段是有序的, 并且后面的阶段依赖于前面的阶段, 但是三套声明周期本身是互相独立的
clean 生命周期: 清理项目
- pre-clean: 执行一些清理前需要完成的工作
- clean: 清理上次构件生成的文件
- post-clean: 执行一些清理后需要完成的工作
default 声明周期: 定义了真正的构件所需执行的所有步骤
- validate
- initialize
- generate-sources
- process-sources: 处理项目主资源文件, 一般来说针对 /src/main/resources 目录的内容进行变量替换等工作后, 复制到项目输出的主 classpath 目录中
- compile: 编译项目的主源码, 一般来说针对 /src/main/java 目录下的 Java 文件至目录输出的主 classpath 目录中
- process-classes
- generate-test-sources
- process-test-sources: 处理项目测试资源文件, 一般来说针对 /src/test/resources 目录的内容进行变量替换工作后, 复制到项目输出的测试 classpath 目录
- test-compile: 编译项目的测试代码, 一般来说针对 /src/test/java 目录下的 java 文件至输出的测试 classpath 目录中
- test: 使用单元测试框架运行测试, 测试代码不会被打包或部署
- prepare-package
- package: 接收编译好的代码, 打包成可发布的格式, 如 jar
- pre-integration-test
- integration-test
- post-integration-test
- verify
- install: 将包安装到 Maven 本地仓库, 供本地其他 Maven 项目使用
- deploy: 将最终的包复制到远程仓库, 供其他开发人员和 Maven 项目使用
site 声明周期: 建立和发布项目站点,Maven 能够基于 POM 所包含的信息, 自动生成一个友好的站点供交流和发布项目信息
- pre-site: 执行一些在生成项目站点之前需要完成的工作
- site: 生成项目站点文档
- post-site: 执行一些在生成项目站点后需要完成的工作
- site-deploy: 将生成的项目站点发布到服务器上
命令行与生命周期
从命令行执行 Maven 任务最主要方式就是调用 Maven 的生命周期, 各个生命周期是相互独立的, 而生命周期的阶段是有前后依赖关系的
- mvn clean: 该命令调用 clean 生命周期的 clean 阶段, 实际执行阶段为 pre-clean 和 clean
- mvn test: 该命令调用 default 生命周期的 test 阶段, 实际执行的阶段为 default 生命周期的 validate,initialize 直到 test 的所有阶段, 这也解释为什么在执行测试的时候, 项目代码能够自动编译
- mvn clean install: 该命令调用 clean 生命周期的 clean 阶段和 default 生命周期的 install 阶段
- mvn clean deploy site-deploy: 该命令调用 clean 生命周期的 clean 阶段,default 生命周期的 deploy 阶段和 site 生命周期的 site-deploy 阶段
5.2 插件
5.2.1 插件目标和绑定
Maven 的核心仅仅定义了抽象的生命周期, 具体的任务是交由插件完成的, 插件以独立的构件形式存在
- 对于插件本身而言, 为了能够复用代码, 它往往能够完成多个任务, 为每个功能编写一个插件显然不合理, 因为这些任务背后有大量可复用的代码, 因此, 这些功能聚集到一个插件里面, 每个功能就是一个插件目标
Maven 的生命周期和插件相互绑定, 用以完成实际的构件任务, 具体而言, 是生命周期的阶段与插件的目标相互绑定, 以完成某个具体的构件任务
- 例如: 编译这一任务对应了 default 生命周期的 compile 这一阶段, 而 maven-compiler-plugin 这一插件的 compile 目标能完成此任务
自定义绑定
除了内置绑定以外, 用户还能够自己选择将某个插件目标绑定到生命周期的某个阶段上, 这种自定义绑定方式能让 Maven 项目在构件过程中执行更多更丰富的任务
当需要创建项目的源码 jar 包,maven-source-plugin 可以帮我们完成任务, 但是内置的插件绑定关系中并没有涉及这一任务, 因此需要自行配置, 它的 jar-no-fork 目标能将项目主代码打包成 jar 文件, 可以将其绑定到 default 生命周期的 verify 阶段, 在执行完集成测试和安装构件之前创建源码 jar 包
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>2.1.1</version> <executions> <execution> <id>attach-sources</id> <phase>verify</phase> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
在 POM 的 build 元素下的 plugins 子元素中声明插件的使用, 上例用到了是 maven-source-plugin, 其 groupId 为 org.apache.maven.plugins, 用户总是应该声明一个非快照版本, 这样可以避免由于插件版本变化造成的构件不稳定性
- 除了基本的插件坐标声明外, 还有插件执行配置,executions 下每个 execution 子元素可以用来配置执行一个任务, 该例配置了一个 id 为 attach-sources 的任务, 通过 phase 配置将其绑定到 verify 生命周期阶段上, 再通过 goals 配置指定要执行的插件目标, 在运行 mvn verify 时就会执行该任务
有时候. 即是不通过 phase 元素配置生命周期阶段, 插件目标也能绑定到生命周期中去, 原因是: 有很多插件的目标在编写时就已经定义了默认绑定阶段, 可以使用 maven-help-plugin 查看详细信息
mvn help:describe -Dplugin=org.apache.maven.plugins:maven-source-plugin:2.1 -Ddetail
- Bound to phase : package 默认绑定到 package 生命周期
- 当插件目标被绑定到不同生命周期阶段的时候, 其执行顺序会由生命周期阶段的先后顺序决定, 如果多个目标被绑定到同一个阶段, 他们的顺序就由插件声明的先后顺序决定
5.2.2 插件配置
用户可以通过配置插件目标的参数, 进一步调整插件目标所执行的任务, 几乎所有 Maven 插件的目标都可以配置参数, 用户可以通过命令行和 POM 配置等方式来配置这些参数
命令行插件配置
在日常的 Maven 使用中, 我们通常从命令行输入并执行 Maven 命令, 很多插件目标的参数都支持从命令行配置, 用户可以在 Maven 命令中使用 - D 参数, 并伴随一个参数键 = 参数值的形式, 来配置插件目标的参数
mvn install -Dmaven.test.skip = ture: 给 maven.surefire-plugin 提供一个 maventest.skip 参数, 当参数为 true 时, 就会跳过执行测试
- 参数 - D 是 Java 自带的, 其功能是通过命令行设置一个 Java 系统属性,Maven 简单地重用了该参数, 在准备插件的时候检查系统属性来实现插件参数的配置
POM 中插件全局配置
并不是所有的插件参数都适合用命令行配置, 有些参数的值从项目创建到项目发布都不会改变, 或者说很少改变, 这种情况下在 POM 文件中一次性配置比重复在命令行输入更合理
用户可以在声明插件的时候, 对插件进行一个全局的配置, 所有基于该插件的目标任务, 都会使用这些配置, 如:maven-compiler-plugin 来编译 1.8 版本的源文件, 生成与 JVM1.8 兼容的字节码文件
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifact>maven-compiler-plugin</artifact> <version>2.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
5.2.3 获取插件信息
当遇到一个构建任务时, 不仅需要知道去哪里找到合适的插件, 还需要详细的了解插件的配置点, 由于 Maven 的插件非常多, 其中大部分没有完善的文档, 因此, 通过帮助命令来了解插件显得非常重要
使用 maven-help-plugin 描述插件
除了访问在线的插件文档外, 可以借助 maven-help-plugin 来获取插件的详细信息
mvn help:decribe -Dplugin = org.apache.maven.plugins:maven-compiler-plugin:2.1
- 这里执行的是 maven-help-plugin 的 decribe 目标, 在参数 plugin 中输入需要描述插件的 groupId,artifactId 和 version,Maven 在命令行的输出 maven-compiler-plugin 的简要信息, 包括插件的坐标, 目标前缀和目标等信息
在描述插件时, 可以省去版本信息,Maven 会自动获取最新版本来进行表述, 并可以用目标前缀来替换坐标
- mvn help:describe -Dplugin=compiler
如果仅仅描述某个插件目标的信息, 则加上 goal 参数, 更详细的信息, 则加上 detail 参数
- mvn help:describe -Dplugin=compiler -Dgoal=compile -Ddetail
5.2.4 插件解析机制
为了方便用户使用和配置插件,Maven 不需要用户提供完整的插件坐标信息, 就可以解析得到正确的插件
插件仓库
- 与依赖构件一样, 插件构件同样基于坐标存储在 Maven 仓库中, 在需要的时候 Maven 先从本地仓库寻找, 如果不存在, 则从远程仓库查找, 找到插件之后, 再下载到本地仓库使用
不同于 repositories 以及 repository 子元素, 插件的远程仓库使用 pluginRepositories 和 pluginRepository 配置
<pluginRepositories> <pluginRepository> <id>central</id> <name>Maven Plugin Resository</name> <url>http://repo1.maven.org/maven2</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> <release> <enabled>true</enabled> <updatePolicy>never</updatePolicy> </release> </pluginRepository> </pluginRepositories>
插件默认的 groupId
在 POM 中配置插件时, 如果该插件时 Maven 的官方插件(即 groupId 为 org.apache.maven.plugins), 就可以省略 groupId,Maven 在解析该插件的时候, 会自动使用默认 groupId 不起
<build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.1</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin> </plugins> </build>
解析插件版本
同样为了简化插件的配置和使用, 在用户没有提供插件版本的情况下,Maven 会自动解析插件版本
- 首先 Maven 在超级 POM 中为所有核心插件设定了版本, 超级 POM 是所有 Maven 项目的父 POM, 所有项目都继承这个超级 POM 配置, 因此即使用户不加任何配置,Maven 在使用核心插件时, 它的版本就已经确定了
如果用户使用的某个插件没有设定版本, 并且这个插件也不属于核心插件,Maven 机会去检查所有仓库中可用的版本, 通过仓库元数据 groupId/artifactId/maven-metadata.xml 文件, 如 maven-compiler-plugin 插件为例, 它在 org/apache/maven/plugins/maven-compiler-plugin/maven-metadata.xml
<?xml version="1.0" encoding="UTF-8"?> <metadata> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <versioning> <latest>2.1</latest> <release>2.1</release> <versions> <version>2.0-beta-1</version> <version>2.1</version> <version>2.2</version> <version>2.3</version> </versions> <lastUpdated>20190102092331</lastUpdated> </versioning> </metadata>
- Maven 遍历本地仓库和所有远程仓库, 将仓库元数据合并后, 就能计算出 latest 和 release 的值,maven3 将版本解析到最新的非快照版, 如 2.1
5.2.5 解析插件前缀
Maven 命令支持使用插件前缀来简化插件的使用, 插件前缀和 groupId:artifact 是一一对应的, 这种匹配关系存储在仓库元数据中, 与之前提到的 groupId/artifactId/maven-metadata.xml 不同, 这里仓库元数据为 groupId/maven-metadata.xml, 一般插件都位于 /org/apache/maven/plugins/ 和 org/code-haus/mojo/, 可以通过 settings.xml 配置让 Maven 检查其他 groupId 上的插件仓库元数据
<settings> <pluginGroups> <pluginGroup>com.my.plugins</pluginGroup> </pluginGroups> </settings>
在仓库的元数据文件中可以看到插件的前缀定义
<metadata> <plugins> <plugin> <name>Maven Clean Plugin</name> <prefix>clean</prefix> <artifact>maven-clean-plugin</artifact> </plugin> <plugin> <name>Maven Compiler Plugin</name> <prefix>compile</prefix> <artifact>maven-compile-plugin</artifact> </plugin> <plugin> <name>Maven Dependency Plugin</name> <prefix>dependency</prefix> <artifact>maven-dependency-plugin</artifact> </plugin> </plugins> </metadata>
六. 聚合与继承
- Maven 的集合特性能够把项目各个模块聚合在一起构建, 而 Maven 的继承特性则能帮助抽泣各模块相同的依赖和插件等配置, 在简化 POM 的同时, 还能促进各个模块配置的一致性
6.1 集合
aggregator
<modelVersion>4.0.0</modelVersion> <groupId>com.lsy.project</groupId> <artifact>project-aggregator</artifact> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <name>Aggregator</name> <modules> <module>project-email</module> <module>project-register</module> </modules>
- 对于聚合模块, 其打包方式 packaging 的值必须为 pom
通过 modules 元素来实现聚合, 用户可以通过在一个打包方式为 pom 的 Maven 项目中声明任意数量的 module 元素来实现模块的聚合, 这里每个 module 的值都是一个当前 POM 的相对目录
- 如 aggregator 的 POM 路径为 …/project-aggregator/pom.xml, 那么 project-email 对应 的目录为 …/project-aggregator/project-email/, 并且目录中包含了 pom.xml,src/main/java,src/test/java 等内容, 离开了 project-aggregator 也能独立创建 =
聚合模块和其他模块的目录结构并非一定要父子关系, 也可以是平行关系
<modules> <module>../prject-email</module> <module>../project-register</module> </modules>
- 当在聚合模块中执行 mvn clean install 时,Maven 首先解析聚合模块的 POM, 分析要构件的模块, 并计算出一个反应堆构件顺序(Reactor Build Order), 然后根据这个顺序构件各个模块
聚合模块的构件顺序
- Maven 按序读取 POM 文件, 如果 POM 没有依赖模块, 那么就构件模块, 否则就先构件其依赖模块
6.2 继承
从以上的聚合模块和其他模块中可以看到, 多个被管理模块的 POM 文件中会有大量重复的相同配置, 他们有相同的 groupId 和 version, 相同的依赖, 相同的插件配置, 而通过 POM 文案的继承可以消除重复
project-parent 父模块
<modelVersion>4.0.0</modelVersion> <groupId>com.lsy.project</groupId> <artifactId>project-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <name>Parent</name>
- 父模块的 POM 文件使用与其他模块一直的 groupId 和 version, 它的 packaging 方式为 pom, 与聚合模块一致
- 父模块主要是为了消除配置的重复, 因此它本身不包含除 POM 文件之外的项目文件, 也就不需要 src/main/java 之类的文件夹了
project-email 子模块
<parent> <groupId>com.lsy.project</groupId> <artifactId>project-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <relativePath>../project-parent/pom.xml</relativePath> </parent> <artifactId>project-email</artifactId> <name>Email</name> <dependencies> ..... </dependencies>
使用 parent 元素声明父模块,parent 下子元素 groupId,artifactId,version 指定父模块的坐标, 元素 relativePath 表示父模块 POM 的相对路径, 表示 Email 模块和其父模块是在平行的目录下
- 当构件项目时,Maven 会首先根据 relativePath 检查父 POM 文件, 如果找不到再从本地仓库找,relativePath 的默认值为../pom.xml, 也就是说,Maven 默认父 POM 在上一层目录
- 子模块没有 groupId 和 version, 是因为子模块隐式的从父模块继承了这两个元素, 从而消除了不必要的配置
可继承的 POM 元素
- groupId: 项目组 ID, 项目坐标的核心元素
- version: 项目版本, 项目坐标的核心元素
- description: 项目的描述信息
- organization: 项目的组织信息
- inceptionYear: 项目的创始年份
- url: 项目的 URL 地址
- develops: 项目的开发者信息
- contributors: 项目贡献者信息
- distributionManagement: 项目的部署配置
- issueManagement: 项目的缺陷跟踪系统信息
- ciManagement: 项目的持续集成系统信息
- scm: 项目的版本控制系统信息
- mailingList: 项目的邮件列表信息
- properties: 自定义属性
- dependencies: 项目的依赖配置
- dependencyManagement: 项目的依赖管理配置
- repositories: 项目的仓库配置
- pluginRepositories: 项目的插件仓库配置
- build: 包括项目的源码目录配置, 输出目录配置, 插件配置, 插件管理配置等
- reporting: 项目的输出目录配置, 报告插件配置等
依赖管理
- dependencies 元素是可以被继承的, 说明依赖是会被继承的, 所以我们可以将子模块共有的依赖配置到父模块中, 子模块就可以移除这些依赖, 简化配置
上述方法是可行的, 但是可能将来会有新的模块并不需要父模块中的一些依赖, 这就会产生不合理的现象, 从而 Maven 提供了 dependencyManagement 元素, 既能让子模块继承到父模块的依赖配置, 又能保证子模块依赖使用的灵活性
- 在 dependencyManagement 元素下的依赖声明不会引入实际的依赖, 不过它能够约束 dependencies 下的依赖使用
<modelVersion>4.0.0</modelVersion> <groupId>com.lsy.project</groupId> <artifactId>project-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>pom</packaging> <name>Parent</name> <properties> <springframework.version>4.3.1</springframework.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${springframwork.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${springframwork.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${springframwork.version}</version> </dependency> </dependencies> </dependencyManagement>
* 这里使用 dependencyManagement 声明的依赖既不会给 parent 模块引入依赖, 也不会给子模块引入依赖, 不过这段配置是会被继承的 * 子模块 POM ```xml <parent> <groupId>com.lsy.project</groupId> <artifactId>project-parent</artifactId> <version>1.0.0-SNAPSHOT</version> <relativePath>../project-parent/pom.xml</relativePath> </parent> <artifactId>project-email</artifactId> <name>Email</name> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> </dependency> </dependencies> ``` * 使用这种依赖管理机制, 可以在父 POM 中使用 dependencyManagement 声明依赖能够统一项目规范中的依赖版本, 当版本在父 POM 中声明之后, 子模块使用依赖时就不需要声明了. 也不会发生多个子模块使用依赖版本不一致的情况 * scoper 元素的 import 依赖范围只在 dependencyManagement 元素下才有效果, 使用该范围的依赖通常指向一个 POM 文件, 作用是将 POM 中的 dependencyManagement 配置导入并合并到当前 POM 的 dependencyManagement 元素中 * 所以除了复制配置和继承父模块这两种方式外, 还可以通过 import 范围依赖导入这一配置 ```xml <dependencyManagement> <dependencies> <dependency>com.lsy.project</dependency> <artifactId>project-parent</artifactId> <version>1.0-SNAPSHOT</version> <type>pom</type> <scope>import</scope> </dependencies> </dependencyManagement> ```
插件管理
- Maven 提供了 dependencyManagement 元素帮助管理依赖, 类似的,Maven 也提供了 pluginManagement 元素管理插件, 该元素中配置的依赖同样不会造成实际的插件调用行为, 而 POM 文件中配置了真正的 plugin 元素, 并且 groupId 和 artifact 一致时, 才会产生实际的插件行为
父模块
<build> <pluginManagement> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-source-plugin</artifactId> <version>3.1.1</version> <executions> <execution> <id>attach-source</id> <phase>verify</phase> <goals> <goal>jar-no-fork</goal> </goals> </execution> </executions> </plugin> </plugins> </pluginManagement> </build>
子模块
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifact>maven-source-plugin</artifact> </plugin> </plugins> </build>
- 子模块使用了 maven-source-plugin 插件, 同时又继承了父模块的 pluginManagement 配置
集合与继承的关系
多模块的聚合与继承其实是两个概念, 其目的是完全不同的, 前者为了方便快速的构件项目, 后者主要为了消除重复配置
- 对于聚合模块来说: 它知道有哪些模块被聚合, 但是那些被聚合的模块并不知道这个聚合模块的存在
- 对于继承关系的父 POM 来说: 它不知道有哪些子模块继承于它, 但是那些子模块都必须知道自己的父模块是什么
- 在实际项目中, 一个 POM 往往即是聚合模块, 又是父模块
约定优于配置
Maven 提倡 ” 约定优于配置 ”,Maven 只需要一个简单的 POM 文件, 就可以完成清除, 构件等任务
- 源码目录:src/main/java/
- 编译输出目录为:target/classes/
- 打包方式为:jar
- 包输出目录为:target/
Maven 此机制的来源就是超级 POM 文件, 此文件在 $MAVEN_HOME/lib/maven-model-builder-x.x.x.jar 中的 org/apache/maven/model/pom-4.0.0.xml 路径下
<repositories> <repository> <id>central</id> <name>Maven Repository Swithboard</name> <url>http://repo1.maven.org/maven2</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>central</id> <name>Maven Plugin Repository</name> <url>http://repo1.maven.org/maven2</url> <layout>default</layout> <snapshots> <enabled>false</enabled> </snapshots> <releases> <updatePolicy>never</updatePolicy> </releases> </pluginRepository> </pluginRepositories> <build> <!--> 定义了项目的主输出目录 </!--> <directory>${project.basedir}/target</directory> <!--> 主代码输出目录 </!--> <outputDirectory>${project.build.directory}/classes</outputDirectory> <!--> 最终构件的名称格式 </!--> <finalName>${project.artifactId}-${project.version}</finalName> <!--> 测试代码输出目录 </!--> <testOutputDirectory>${project.build.directory}/test- classes</testOutputDirectory> <!--> 主源码目录 </!--> <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory> <!--> 脚本源码目录 </!--> <scriptSourceDirectory>src/main/scripts</scriptSourceDirectory> <!--> 测试源码目录 </!--> <testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory> <!--> 主资源目录 </!--> <resources> <resource> <directory>${project.basedir}/src/main/resources</directory> </resource> </resources> <!-- > 测试资源目录 </!--> <testResources> <testResource> <directory>${project.basedir}/src/test/resources</directory> </testResource> </testResources> <!--> 核心插件版本 </!--> <pluginManagement> <plugins> <plugin> <artifactId>maven-antrun-plugin</artifactId> <version>1.3</version> </plugin> <plugin> <artifactId>maven-assembly-plugin</artifactId> <version>2.3</version> </plugin> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>2.3</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.3</version> </plugin> ...... </plugins> </pluginManagement> </build>
七. Nexus 私服
7.1 Nexus 内置的仓库
Nexus 仓库有四种类型
- group: 仓库组
- hosted: 宿主仓库
- proxy: 代理仓库
- virtual: 虚拟仓库
- Maven 可以从宿主仓库下载构件,Maven 也可以从代理仓库下载构件, 而代理仓库会简介的从远程仓库下载并缓存构件
- 而一般为了方便, 我们会从仓库组下载构件, 而仓库组没有实际的内容, 它只是管理一组实际的仓库, 当它接收到请求时, 它会转向其包含的宿主仓库或代理仓库获得实际构件的内容
仓库的 Policy 属性
- Release: 发布版
- Snapshot: 快照版
7.2 配置 Maven 从 Nexus 下载构件
在 POM 文件中配置仓库和插件仓库 —— 只对当前项目有效
<project> ....... <repositories> <repository> <id>nexus</id> <name>Nexus</name> <url>http://localhost:8081/nexus/content/groupId/public/</url> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>nexus</id> <Name>Nexus</Name> <url>http://localhost:8081/nexus/content/groupId/public/</url> <release> <enabled>true</enabled> </release> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> ....... </project>
在 settings.xml 文件中配置 — 全局有效
- settings.xml 文件并不支持直接配置 repositories 和 pluginRepositories, 但是可以通过 Maven 提供的 Profile 机制, 让用户将仓库配置到 settings.xml 中的 Profile 中
<settings> .... <profiles> <profile> <id>nexus</id> <repositories> <repository> <id>nexus</id> <name>Nexus</name> <url>http://localhost:8081/nexus/content/groupId/public/</url> <release> <enabled>true</enabled> </release> <snapshots> <enabled>ture</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>nexus</id> <Name>Nexus</Name> <url>http://localhost:8081/nexus/content/groupId/public/</url> <release> <enabled>true</enabled> </release> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> </profile> </profiles> <!--> 激活指定 id 的 profile </!--> <activeProfiles> <activeProfile>nexus</activeProfile> </activeProfiles> </settings>
以上配置已经能让 Maven 项目从 Nexus 私服下载构件了, 但是 Maven 仍然会去访问中央仓库, 现在我们想要将所有请求都仅仅通过私服, 这就需要借助于 Maven 镜像了
<settings> ..... <mirrors> <mirror> <id>nexus</id> <mirrorOf>*</mirrorOf> <url>http://localhost:8081/nexus/content/groupId/public/</url> </mirror> </mirrors> <profiles> <profile> <id>nexus</id> <repositories> <repository> <id>central</id> <name>central</name> <url>http://central</url> <release> <enabled>true</enabled> </release> <snapshots> <enabled>ture</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>central</id> <Name>Nexus</Name> <url>http://central</url> <release> <enabled>true</enabled> </release> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> </profile> </profiles> <!--> 激活指定 id 的 profile </!--> <activeProfiles> <activeProfile>nexus</activeProfile> </activeProfiles> </settings>
- 这里仓库和插件仓库配置的 id 都为 central, 它们将会覆盖 POM 中央仓库的配置, 并且这里的 url 已经无关紧要, 因为所有的请求都会通过镜像访问私服地址
7.3 部署构件至 Nexus
如果只是为了代理外部公共仓库, 那么 Nexus 的代理仓库就已经完全足够了, 对于另一类 Nexus 仓库 — 宿主仓库来说, 他们主要作用是存储组织内部的, 或一些无法从公共仓库获得的第三方构件, 用户可以通过配置 Maven 自动部署构件至 Nexus 的宿主仓库
使用 Maven 部署构件至 Nexus
- 日常开发生成的快照版本构件可以直接部署到 Nexus 中策略为 Snapshot 的宿主仓库中, 项目正式发布的构件则应该部署到 Nexus 中策略为 Release 的宿主仓库中
POM 文件配置
<project> ..... <distributeManagement> <repository> <id>nexus-releases</id> <name>Nexus Release Repository</name> <url>http://localhost:8081/nexus/content/repositories/ release/</url> </repository> <snapshotRepository> <id>nexus-snapshots</id> <name>Nexus Snapshots Repository</name> <url>http://localhost:8081/nexus/content/repositories/ snapshots</url> </snapshotRepository> </distributeManagement> ..... </project>
Nexus 的仓库对于匿名用户只是可读的, 为了能够部署构件, 还需要在 settings.xml 中配置认证信息
<settings> .... <servers> <server> <id>nexus-releases</id> <username>admin</username> <password>admin123</password> </server> <server> <id>nexus-snapshots</id> <username>admin</username> <password>admin123</password> </server> </servers> .... </settings>
八. Profile
- 一个优秀的构件系统必须足够灵活, 它应该能够让项目在不同的环境下都能够成功的构件, 例如: 开发环境, 测试环境和产品环境, 这些环境的数据库配置不尽相同, 那么项目构建时就需要能够识别其所在的环境并正确使用配置
8.1 属性
<properties> <springframework.version>4.3.1</springframework.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${springframwork.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${springframwork.version}</version> </dependency> </dependencies> </dependencyManagement>
- 这是最常见的 Maven 属性使用的方式, 通过 properties 元素使得用户自定义一个或多个 Maven 属性, 然后在 POM 的其他地方使用 ${属性名称}的方式来引用该属性, 这种做法的最大意义是消除重复
Maven 的属性有六类
内置属性
- ${basedir}: 表示项目根目录, 即包含 pom.xml 文件的目录
- ${version}: 表示项目的版本
POM 属性: 用户可以使用该类属性引用 POM 文件中对应元素的值
${project.artifactId}: 对应 <project><artifactId> 元素的值
- ${project.build.sourceDirectory}: 项目的主源码目录. 默认为 /src/main/java/
- ${project.build.testSourceDirectory}: 项目的测试代码源码目录. 默认为 /src/test/java/
- ${project.build.directory}: 项目构件输出目录. 默认为 /target
- ${project.build.outputDirectory}: 项目主代码编译输出目录. 默认为 /target/classes
- ${project.build.testOutputDirectory}: 项目测试代码编译输出目录. 默认为 /target/test-classes
- ${project.build.groupId}: 项目的 groupId
- ${project.build.artifactId}: 项目的 artifactId
- ${project.build.build.finalName}: 项目打包输出文件的名称, 默认为 \${project.artifactId}
-${project.version}
- 自定义属性:<properties> 元素下自定义的 Maven 属性
- settings 属性: 与 POM 属性同理, 用户使用 settings. 来引用, 如:${settings.localRepository}指向用户本地仓库的地址
Java 系统属性: 所有 Java 系统属性都可以使用 Maven 属性引用, 如 ${user.home}指向用户目录
- 可以通过 mvn help:system 查看所有的 java 系统属性
环境变量属性: 所有环境变量都可以用 env. 来引用, 例如:${env.JAVA_HOME}
- 可以通过 mvn help:system 查看所有的环境变量
8.2 资源过滤
在不同的开发环境中, 项目的源码应该使用不同的方式进行构件, 最常见的就是数据库的配置了, 在开发中, 有些项目会在 src/main/resources/ 目录下放置不同环境下的数据库配置
database.jdbc.driverClass = com.mysql.jdbc.Driver database.jdbc.connectionURL = jdbc:mysql://localhost:3306/dev database.jdbc.username = dev database.jdbc.password = dev-passwd database.jdbc.driverClass = com.mysql.jdbc.Driver database.jdbc.connectionURL = jdbc:mysql://localhost:3306/test database.jdbc.username = test database.jdbc.password = test-passwd
为了应对环境的变化, 我们可以使用 Maven 属性将这些会发生变化的部分提取出来
database.jdbc.driverClass = ${db.driver} database.jdbc.connectionURL = ${db.url} database.jdbc.username = ${db.username} database.jdbc.password = ${db.password}
在 settings.xml 中通过 profile 定义不同环境下的配置数据
<profiles> <profile> <id>dev</id> <properties> <db.driver>com.mysql.jdbc.Driver</db.driver> <db.url>jdbc:mysql://localhost:3306/dev</db.url> <db.username>dev</db.username> <db.password>dev-passwd</db.password> </properties> </profile> <profile> <id>test</id> <properties> <db.driver>com.mysql.jdbc.Driver</db.driver> <db.url>jdbc:mysql://localhost:3306/test</db.url> <db.username>test</db.username> <db.password>test-passwd</db.password> </properties> </profile> </profiles>
需要注意的是:
- Maven 属性默认只会在 POM 文件中才会被解析, 也就是说 ${db.username}放到 POM 文件中会变成 dev 或 test, 但是在 src/main/resources/ 目录下仍然还是 ${db.username}, 因此, 需要让 Maven 解析资源文件中的 Maven 属性
- 资源文件处理的是 maven-reosurces-plugin 做事, 它默认的行为只是将项目主资源文件复制到主代码编译输出目录
开启资源过滤
- Maven 默认的主资源目录和测试目录的定义是在超级 POM 文件中
<resources> <resource> <directory>${project.basedir}/src/main/resources</directory> <filtering>true</filtering> </resource> </resources> <testResources> <testResource> <directory>${project.basedir}/src/test/resources</directory> <filtering>true</filtering> </testResource> </testResources>
- 通过 mvn clean install -Pdev : 通过 mvn 的 - P 参数表示在命令行激活一个 profile=dev 的 profile
8.3 Profile
激活 Pofile
命令行激活
用户可以使用 mvn 命令行参数 -P 加上 profile 的 id 来激活 profile, 多个 id 之间逗号分隔
- mvn clean install -Pdev,-Ptest
settings 文件显示激活
如果用户希望某个 profile 默认一直处于激活状态, 就可以配置 settings.xml 文件的 activeProfile 元素, 表示其配置的 profile 对所有项目都处于激活状态
<settings> ..... <activeProfiles> <activeProfile>dev</activeProfile> </activeProfiles> ..... </settings>
系统属性激活
用户可以配置当前某系统属性存在时, 自动激活 profile
<profiles> <profile> <activation> <property> <name>test</name> </property> </activation> ..... </profile> </profiles>
用户可以配置当前某系统属性存在且等于 a 时, 自动激活 profile
<profiles> <profile> <activation> <property> <name>test</name> <value>a</value> </property> </activation> ..... </profile> </profiles>
用户可以在命令行声明系统属性
- mvn clean install -Dtest=x
操作系统环境变量激活
Profile 还可以根据操作系统环境激活
<profiles> <profile> <activation> <os> <name>Windows10</name> </os> </activation> ..... </profile> </profiles>
文件存在与否激活
Maven 可以根据项目中某个文件是否存在来决定是否激活 profile
<profiles> <profile> <activation> <file> <missing>a.properties</missing> <exists>b.properties</exists> </file> </activation> ..... </profile> </profiles>
默认激活
用户可以在定义 profile 的时候指定其默认激活
<profiles> <profile> <id>dev</id> <activation> <activeByDefault>true</activeByDefault> </activation> ..... </profile> </profiles>
以上激活优先级从上之下依次减小
- 可以通过 mvn help:active-profiles 来查看当前激活的 profile
- 可以通过 mvn help:all-profiles 来查看所有的 profile
profile 的种类
根据具体的需要, 可以在一下位置声明 profile
- pom.xml: 很显然,pom.xml 中声明的 profile 只对当前项目有效
- 用户 settings.xml: 用户目录下.m2/settings.xml 中的 profile 只对本机该用户所有的 Maven 项目有效
- 全局 settings.xml:Maven 安装目录下 conf/settings.xml 中 profile 对本机所有 Maven 项目都有效
POM 中 profile 可使用的元素
<project> <repository></repository> <pluginRepository></pluginRepository> <distributionManagement></distributionManagement> <dependencies></dependencies> <dependencyManagement></dependencyManagement> <modules></modules> <properties></properties> <reporting></reporting> <build> <plugins></plugins> <defaultGoal></defaultGoal> <resources></resources> <testResources></testResources> <finalName></finalName> </build> </project>
* pom.xml: 很显然,pom.xml 中声明的 profile 只对当前项目有效 * 用户 settings.xml: 用户目录下.m2/settings.xml 中的 profile 只对本机该用户所有的 Maven 项目有效 * 全局 settings.xml:Maven 安装目录下 conf/settings.xml 中 profile 对本机所有 Maven 项目都有效
- POM 中 profile 可使用的元素