maven基础概念
使用入门
说到Maven的入门使用,其实是特别简单的,如果只是说就是能使用,会使用Maven,也许只要短短的一两个小时就OK了,不需要去理解Maven的那些概念,而这篇文章就是要教会你会使用Maven,而整个系列则是要让你明白整个Maven。这篇文章就是如此,仅仅就是告诉你怎么用Maven,仅此而已,会用是学习整个系列的前提。
编写pom
就像composer的composer.json、Make的makefile文件一样,Maven项目的核心是pom.xml文件。POM(Project Object Model,项目对象模型)定义了项目的基本信息,用于描述项目如何构建,声明项目依赖,等等。
现在我们不借助任何其它命令和IDE,来创建一个Maven项目。
首先,编写pom.xml文件。还是按照老规矩,从一个Hello World项目进行演示。以下就是创建项目的POM文件。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jellythink.HelloWorld</groupId>
<artifactId>hello-world</artifactId>
<version>1.0-SNAPSHOT</version>
<name>hello-world</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
对于POM文件,现在细说一下。
- 代码的第一行是XML头,指定了该xml文档的版本和编码方式。紧接着是project元素,project是所有pom.xml的根元素,它还声明了一些POM相关的命名空间及xsd元素;
- 根元素下的第一个子元素modelVersion指定了当前POM模型的版本,对于Maven 2和Maven 3来说,它只能是4.0.0;
- 接下来就是groupId、artifactId和version了,这三个是上述代码三个元素。这三个元素定义了一个项目的基本坐标,在Maven的世界里,所有的jar和war都是基于坐标进行区分的,下面面的文章还会细说坐标;
- groupId定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联,一般是使用组织或公司的域名;比如上面的groupId是com.jellythink.HelloWorld,其中com.jellythink就是我的网站域名倒过来写的,而HelloWorld则是整个项目的名称;
- artifactId定义了当前Maven项目在组中唯一的ID,一般一个大项目组下面可能会包含多个子项目或子模块,而这个artifactId就是子项目或者子模块的名称;
- version指定了这个项目当前的版本,下面的文章还会细说Maven中版本的含义;
- name元素声明了一个对于用户更为友好的项目名称,方便后期的管理;
- properties指定了Maven的一些重要属性,后续还会重点说这个属性的一些配置。
创建完pom.xml文件后,接下来就是创建代码文件了。在Maven中,有这样的一个约定,项目主代码都位于src/main/java目录,项目测试代码都位于src/test/java目录;接下来我们先按照这个约定分别创建目录,然后在代码目录创建com/jellythink/HelloWorld/App.java文件;在测试目录创建com/jellythink/HelloWorld/AppTest.java文件。
还是老规矩,我们在App.java中打印Hello World!,代码如下:
public class App {
public String sayHello() {
return "Hello World";
}
public static void main( String[] args ) {
System.out.println(new App().sayHello());
}
}
同理,对于AppTest.java中编写以下单元测试代码:
public class AppTest {
@Test
public void testSayHello() {
App app = new App();
String result = app.sayHello();
assertEquals("Hello World", result);
}
}
在Java中,我们进行单元测试时,基本上都是使用的JUnit,要使用JUnit这个包,我们就需要引入这个依赖包,此时,我们就需要在pom.xml中添加以下依赖内容:
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
</dependencies>
代码中添加了dependencies元素,该元素下可以包含多个dependency元素以声明项目的依赖。前面也说过,groupId、artifactId和version是任何一个Maven项目最基本的坐标,JUnit也不例外;scope表示依赖范围,后续的文章还会细说这个依赖和测试相关的内容。
编辑和测试
万事俱备,只欠东风。接下来我们编译和测试。
搞定代码后,使用Maven进行编译,在项目根目录下运行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的生命周期及插件,这个在后面还会有专题文章细说。
编译完成后,我们一般都会运行测试代码进行单元测试,虽然很多情况下,我们并没有这么做,但是我还是建议大家通过Maven做一些自动化的单元测试。
测试用例编写完毕之后就可以调用Maven执行测试,运行mvn clean test命令,输出如下:
从输出可以看到,Maven依次执行了clean:clean、resources:resources、compiler:compile、resources:testResources、compiler:testCompile和surefire:test。现阶段,我们需要明白这是Maven的生命周期的一个特性,这个生命周期后续还会细说。
到此,编译和测试均通过了,接着我们进行应用打包和运行。
打包运行
打包就是将我们编写的应用打成JAR包或者WAR包。在我们的HelloWorld示例程序POM中,并没有指定打包类型,Maven则默认打包成JAR包。我们执行mvn clean package命令就可以完成打包。mvn clean package命令的输出如下:
可以看到,Maven在打包之前会执行编译、测试等操作,最后通过jar:jar任务负责打包。实际上就是jar插件的jar目标将项目主代码打包成一个名为hello-world-1.0-SNAPSHOT.jar的文件,这个最终生成的包会保存在target目录下,它是根据artifact-version.jar规则进行命名的;当然了,我们可以使用finalName属性来自定义该文件的名称。
到现在,我们得到了这个JAR包,如果别的项目要引用这个JAR包时,我们将这个JAR包复制到其它项目的classpath中就OK了。但是这样拷贝就违背了我们当初想要自动解决依赖的问题,所以如何才能让其它的Maven项目直接引用这个JAR包呢?我们需要执行mvn clean install命令。
从输出可以看到,在打包之后,又执行了安装任务install:install,最后将项目输出的JAR包安装到了Maven本地仓库中,我们可以在本地的仓库文件夹中能看到这个示例项目的pom和jar包。
到目前为止,通过这个示例项目体验了mvn clean compile、mvn clean test、mvn clean package和mvn clean install。执行test之前会先执行compile的,执行package之前会先执行test的,而类似地,install之前会执行package。我们可以在任何一个Maven项目中执行这些命令。
最后,不要忘了,我们生成的JAR包是有main方法的,也就是说这个JAR包是可以单独运行的;但是,由于带有main方法的类信息没有添加到manifest中,所以默认打包生成的jar是不能够直接运行的(使用jd-gui打开jar文件中的META-INF/MANIFEST.MF文件,将无法看到Main-Class一行)。为了生成可执行jar文件,需要借助Apache Maven Shade Plugin来完成,我们需要在pom.xml文件中以下插件配置:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.jellythink.HelloWorld.App</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
接下来,我们再执行mvn clean install命令,待构建完成之后在target目录下可以看到hello-world-1.0-SNAPSHOT.jar和original-hello-world-1.0-SNAPSHOT.jar,前面的是带有Main-Class信息的可运行jar,后者是原始的jar。我们执行以下命令:
java -jar hello-world-1.0-SNAPSHOT.jar
就可以正常执行。
maven坐标
<groupId>com.jellythink.HelloWorld</groupId>
<artifactId>hello-world</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
上面代码中,关于坐标的各个坐标元素,这里重点说明一下:
- groupId:定义当前Maven项目隶属的实际项目;我们要明白的是Maven项目和实际项目不一定是一对一的关系。举一个最常见的例子,比如Spring Framework这个实际项目,其对应的Maven项目会有很多,如spring-core、spring-context等。这是由于Maven中模块的概念,因此实际项目往往会被划分成很多模块。当我们看到一个项目的groupId时,会觉的很熟悉,为什么?是不是和我们经常定义的Java包名很像,这个和我们在Java中定义顶级包名的规则是一样的,通常与公司或者组织的域名反向一一对应。
- artifactId:该元素定义实际项目中的一个Maven项目(模块),一般推荐的做法是使用实际项目名称作为artifactId的前缀,比如spring-core的前缀是spring一样。
- version:该元素定义Maven项目当前所处的版本;在Maven中定义了一整套完整的版本定义规范,后续会有专门的文章进行总结。
- packaging:该元素定义Maven项目的打包方式;打包方式通常与所编译生成的文件扩展名对应,但也不是绝对的,比如packaging为maven-plugin的构件扩展名为jar;packaging常见的是jar和war这两种类型;不同的打包方式会影响到构建的生命周期。很多时候,我们也会看到我们没有定义这个packaging元素,此时Maven会使用默认值jar。
- classifier:该元素用来帮助定义输出一些附属文件。附属输出文件与主输出文件是对应的,比如上面的主输出文件是hello-world-1.0-SNAPSHOT.jar,该项目可能还会通过使用一些插件生成如hello-world-1.0-SNAPSHOT-javadoc.jar、hello-world-1.0-SNAPSHOT-sources.jar这样的一些附属输出文件。需要我们注意的是,不能直接定义项目的classifier,因为附属输出文件不是项目直接默认生成的,而是由附加的插件帮助生成的。
maven依赖
<project>
...
<dependencies>
<dependency>
<groupId>...</groupId>
<artifactId>...</artifactId>
<version>...</version>
<type>...</type>
<scope>...</scope>
<optional>...</optional>
<exclusions>
<exclusion>
...
</exclusion>
</exclusions>
</dependency>
...
</dependencies>
...
</project>
- groupId、artifactId和version:依赖的基本坐标,对于任何一个依赖来说,基本坐标是最重要的,Maven根据坐标才能找到需要的依赖;
- type:依赖的类型,对应于项目坐标定义的packaging,大部分情况下,该元素不必声明,其默认值为jar;
- scope:依赖的范围,这个内容就比较多一点,下面会专门进行总结;
- optional:标记依赖是否可选,下面会专门进行总结;
- exclusions:用来排除传递性依赖,下面会专门进行总结。
依赖范围
我们需要知道,Maven在编译项目主代码的时候需要使用一套classpath。举例来说:
- 当Maven编译项目主代码的时候如果需要用到spring-core,该文件以依赖的方式被引入到classpath中;
- 当Maven编译和执行测试的时候会使用另外一套classpath,则junit文件也会以依赖的方式引入到测试使用的classpath中;
- 当Maven项目运行时,又会使用一套classpath。
所以依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系。在Maven中,我们可以针对scope要素设置以下依赖范围:
- compile:编译依赖范围。如果没有指定scope值,就会默认使用该依赖。使用该依赖范围的Maven依赖,对于编译、测试、运行三种classpath都有效;
- test:测试依赖范围。使用此依赖范围的Maven依赖,只对于测试classpath有效,在编译主代码或者运行项目时都无法使用此依赖。对于上面的junit例子,它只有在编译测试代码及运行测试的用例的时候才需要;
- provided:已提供依赖范围。使用此依赖范围的Maven依赖,对于编译和测试classpath有效,但在运行时无效。最典型的例子是servlet-api,编译和测试项目的时候需要该依赖,但在运行项目的时候,由于容器已经提供,就不需要Maven重复地引入一遍;
- runtime:运行时依赖范围。使用此依赖范围的Maven依赖,对于测试和运行classpath有效,但在编译主代码时无效。最典型的例子就是JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或者运行项目的时候才需要实现上述接口的具体JDBC驱动。
- system:系统依赖范围。该依赖与三种classpath的关系和provided依赖范围完全一致。但是,使用system范围的依赖时必须通过systemPath元素显式地指定依赖文件的路径。由于此类依赖不是通过Maven仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此谨慎使用。
| centered 文本居中 | right-aligned 文本居右 |
依赖范围 | 对于编译的classpath有效 | 对于测试的classpath有效 | 对于运行的classpath有效 | 列子 |
---|---|---|---|---|
compile | Y | Y | Y | spring-core |
test | - | Y | - | junit |
provided | Y | Y | - | servlet-api |
runtime | - | Y | Y | JDBC驱动实现 |
system | Y | Y | - | 笨的的,Maven仓库之外的类库文件 |
依赖传递的注意事项
- 什么是依赖传递?
在实际的项目中,一定会遇到下面所示的这种依赖情况
A–>B–>C
A依赖B,B又依赖C。由于基于Maven创建的项目,有了传递性依赖机制,在使用A的时候就不用去考虑A依赖了什么,也不用担心引入多余的依赖。Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。 - 依赖传递的范围
在传递性依赖中,如上图所示的A依赖B,B依赖C,我们就说A与B是第一直接依赖,B与C是第二直接依赖,C对于A是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围
对于上面的图,最左面的一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围。比如A对B的依赖scope是compile,B对C的依赖scope是runtime,那么A对C的依赖scope就是runtime。 - 依赖调解
在实际开发中,可能会遇到一下两种情况的依赖需要调解:
第一种如下:
第二种如下:
从上面可以看到,情况一A的两条依赖链长度不同 依赖的x的不同版本。情况二B的两条依赖链长度相同,依赖X的不同版本。
遇到这种情况,maven就会运用内置的两个调解原则,确定到底哪个依赖会被解析使用。内置的调解原则如下
1、路径最短者优先:对于第一种情况,使用这个原则就会选择x3.0
2、第一声明者优先:对于情况二,在依赖路径长度相等的情况下,在pom文件中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜。
- 可选依赖
在实际情况中,可能还有这样的情况
首先需要知道,可选依赖,依赖将不会进行依赖范围传递,也就是当项目A需要用到M和N的时候还需要自己引入依赖。
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>9.1-901-1.jdbc4</version>
<optional>true</optional>
</dependency>
如上,使用optional标签可以设置依赖为可选依赖。
maven仓库
什么是maven仓库
现在大家想一下之间开发的非Maven项目,是不是在每个项目下面都有一个lib目录。是的,你不用去翻看你以前做的项目了,没有错,没有Maven之前,我们项目依赖的包,我们都会下载下来,统一放到对应项目的lib目录下去。同一个包,比如Spring框架的包,项目A要使用,就拷贝一份到项目A的lib目录下去;项目B也要使用,那就再拷贝一份到项目B的目录下去。这样下去,你会发现同样的依赖包,需要拷贝N份,这样不仅造成了磁盘空间的浪费,而且也难于统一管理。
现在好了,有了Maven,基于Maven的坐标机制,任何Maven项目使用任何一个构件的方式都是完全相同的。在此基础上,Maven可以在某个位置统一存储所有Maven项目共享的包,而这个统一存放依赖包的位置就是仓库。说白了,Maven仓库就是存放依赖包的地方。
有了这个Maven仓库后,上面的问题就有了一个完美的解决方案。基于Maven开发的项目不再各自存储其依赖文件,它们只需要声明这些依赖的坐标,在需要的时候,Maven会自动根据坐标找到仓库中的包,并正确使用它们。
仓库分类
在maven中,仓库分为以下两类
- 本地仓库
- 远程仓库
maven根据依赖坐标去仓库中找对应得包,是遵循以下的轨迹:
- 首先去本地仓库查找,如果本地仓库有对应得依赖包,则使用;
- 如果本地仓库不存在对应包时,或者需要查看是否有更新的包版本时吗,maven就会去远程仓库查找,发现需要的依赖后,下载到本地仓库使用;
- 如果本地和远程都没有需要的包,maven就会报错。
上面讲maven仓库做了简单的分类,但是,对于远程仓库,它又分了好几种
- 本地仓库
上面也说到了Maven根据依赖坐标去仓库中找对应的包是有遵循的轨迹的。Maven最开始都是从本地仓库寻找依赖的包。默认情况下,不管是在Windows还是在Linux上,每个用户在自己的用户目录下都有一个路径名为.m2/repository的仓库目录。这个就是默认的本地仓库地址。
有的时候,用户可能会自定义本地仓库的目录地址(我一般都会这么干)。此时,可以通过编辑~/.m2/settings.xml,设置localRepository元素的值就OK了,如下
<localRepository>E:/repository</localRepository>
- 中央仓库
由于最开始的本地仓库是空的,Maven必须知道至少一个可用的远程仓库,这样才能在执行Maven命令的时候下载到需要的构件。中央仓库就是这样一个默认的远程仓库,Maven的安装文件中自带了中央仓库的配置。使用解压缩工具打开$M2_HOME/lib/maven-model-builder-3.5.0.jar文件,在org\apache\maven\model目录下有一个pom-4.0.0.xml文件,该文件里面有这么一段代码,它配置了默认的中央仓库:
<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<layout>default</layout>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
pom-4.0.0.xml文件是所有Maven项目都会继承的超级POM,这段配置使用id central对中央仓库进行唯一标识,同时设置snapshots元素,其子元素enabled的值为false,表示不从该中央仓库下载快照版本的包。
中央仓库是一个大而全的包仓库,它包含了这个世界上绝大多数流行的开源Java包,以及源码等信息。一般来说,一个简单Maven项目所需要的依赖包都能从中央仓库下载到,这也就解释了为什么Maven能做到“开箱即用”。
- 私服
玩游戏的时候,经常会听到私服。但是在学习Maven的时候,也听到私服,这个就比较特殊了。在Maven中,私服是一种特殊的远程仓库,它是架设在局域网内的仓库服务,私服代理广域网上的远程仓库,供局域网内的Maven用户使用。整体架构如下图所示:
当Maven需要下载依赖包的时候,它从私服请求,如果私服上不存在该依赖包,则从外部的远程仓库下载,缓存到私服上之后,再为Maven的下载请求提供服务。另外,一些无法从外部仓库下载到的依赖包也能从本地上传到私服上供大家使用。
为啥要用私服呢?肯定是有少好处的。像在我们公司,在全国31个省都有分公司,同时总部研发中心还会开发一堆的公共JAR包,给31个分公司使用,这样通过私服就可以很好的解决研发中心和分公司之间的公共包分发等问题。对于使用私服,它有以下这些优点:
1、加快Maven构建;我们知道,不停的连接外部仓库下载依赖包是一件非常耗费时间的事情,而私服部署在局域网,则可以大大的降低依赖包的下载时间,提高Maven构建效率;
2、部署第三方包;比如我们公司的研发中心,会开发很多公共的包,而这些包又无法上传至中央仓库,所以这些包部署在私服就再适合不过了。
远程仓库配置
没有一个平台能够大而全到包含所有的东西,同理,中央仓库也是这样的,虽然它包含了我们需要的大部分的依赖包,但是还是有一些包在中央仓库中是找不到的。这个时候,我们就需要配置一些其它远程仓库来补充中央仓库中没有的依赖包,与中央仓库配合完成工作,当中央仓库也没有对应的依赖包时,Maven则遍历所有的远程仓库。
我们需要在pom.xml中配置即可,比如这样:
<project>
......
<!-- 配置远程仓库 -->
<repositories>
<repository>
<id>jboss</id>
<name>JBoss Repository</name>
<url>http://repository.jboss.com/maven2/</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
<checksumPolicy>warn</checksumPolicy>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
......
</project>
- repository:在repositories元素下,可以使用repository子元素声明一个或者多个远程仓库。
- id:仓库声明的唯一id,尤其需要注意的是,Maven自带的中央仓库使用的id为central,如果其他仓库声明也使用该id,就会覆盖中央仓库的配置。
- name:仓库的名称,让我们直观方便的知道仓库是哪个,暂时没发现其他太大的含义。
- url:指向了仓库的地址,一般来说,该地址都基于http协议,Maven用户都可以在浏览器中打开仓库地址浏览构件。
- releases和snapshots:用来控制Maven对于发布版构件和快照版构件的下载权限。需要注意的是enabled子元素,该例中releases的enabled值为true,表示开启JBoss仓库的发布版本下载支持,而snapshots的enabled值为false,表示关闭JBoss仓库的快照版本的下载支持。根据该配置,Maven只会从JBoss仓库下载发布版的构件,而不会下载快照版的构件。
- layout:元素值default表示仓库的布局是Maven2及Maven3的默认布局,而不是Maven1的布局。基本不会用到Maven1的布局。
- 其他:对于releases和snapshots来说,除了enabled,它们还包含另外两个子元素updatePolicy和checksumPolicy。
元素updatePolicy用来配置Maven从远处仓库检查更新的频率,默认值是daily,表示Maven每天检查一次。其他可用的值包括:never-从不检查更新;always-每次构建都检查更新;interval:X-每隔X分钟检查一次更新(X为任意整数)。
元素checksumPolicy用来配置Maven检查校验和文件的策略。当构建被部署到Maven仓库中时,会同时部署对应的检验和文件。在下载构件的时候,Maven会验证校验和文件,如果校验和验证失败,当checksumPolicy的值为默认的warn时,Maven会在执行构建时输出警告信息,其他可用的值包括:fail-Maven遇到校验和错误就让构建失败;ignore-使Maven完全忽略校验和错误。
大部分公共的远程仓库无须认证就可以直接访问,但我们在平时的开发中往往会架设自己的Maven远程仓库,出于安全方面的考虑,我们需要提供认证信息才能访问这样的远程仓库。配置认证信息和配置远程仓库不同,远程仓库可以直接在pom.xml中配置,但是认证信息必须配置在settings.xml文件中。这是因为pom往往是被提交到代码仓库中供所有成员访问的,而settings.xml一般只存在于本机。因此,在settings.xml中配置认证信息更为安全。比如这样配置:
<servers>
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>
</servers>
部署至远程仓库
很多时候,我们编译完成后,会将我们的负责的模块包部署至私服,以供其它团队成员使用。那如何将我们的包部署到远程仓库呢?
我们配置项目的pom.xml文件即可,配置如下所示:
<project>
......
<distributionManagement>
<repository>
<id>releases</id>
<name>public</name>
<url>http://59.50.95.66:8081/nexus/content/repositories/releases</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<name>Snapshots</name>
<url>http://59.50.95.66:8081/nexus/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>
......
</project>
distributionManagement包含repository和snapshotRepository子元素,前者表示发布版本(稳定版本)构件的仓库,后者表示快照版本(开发测试版本)的仓库。
配置正确后,运行mvn clean deploy命令,Maven就会将项目构建输出的构件部署到配置对应的远程仓库,如果项目当前的版本是快照版本,则部署到快照版本的仓库地址,否则就部署到发布版本的仓库地址。
快照版本
如果去你的本地仓库,你可能会看到以这种类似于1.0.0、1.3-alpha-1、2.0、2.1-SNAPSHOT或者2.1-20190412.221213-52这样的版本号命名的JAR包。其中,1.0.0、1.3-alpha-1和2.0是稳定的发布版本,而2.1-SNAPSHOT和2.1-20190412.221213-52是不稳定的快照版本。
Maven为什么要区分发布版本和快照版本呢?为什么还要有2.1-SNAPSHOT和2.1-20190412.221213-52这样的命名区分呢?
现在我们来思考一种现实的开发场景。现在软件开发都是多人分模块开发,比如小明开发A模块,你开发B模块,而你的B模块是需要依赖A模块的,在开发过程中,小明需要经常将自己最新的代码提交编译,生成A模块包,交给你,供你开发和集成调试。这种开发场景,如何完美的实现多人协同开发、模块开发呢?
你可能想你自己每次单独从代码更把A模块代码Pull下来,自己编译,生成自己需要的依赖包。这么做没有问题,但是带来的问题是你需要去拉代码,还要去编译别人的模块,如果编译不顺利,各种错误,你可能一脸懵逼,只能去找小明解决了。这样无形的就把工作流程搞砸了,你本来就只想要一个A模块的包,但是你却干了一堆不相干的事情,浪费时间。
你可能又想,小明每次将A模块编译完成后,使用mvn clean deploy命令部署到私服,这样你在编译你的模块时,Maven就会自动的去私服下载对应的依赖包。想起来挺美的,但是在Maven中,同样的版本和同样的坐标就意味着同样的包,所以,如果你的本地仓库中已经包含了模块A的某个版本的包,Maven就不会再对照远程仓库进行更新。除非你每次执行Maven命令之前,清除本地仓库,但这种要求手工干预的做法显然也是不可取的。
此时你可能又想,小明不断的修改A模块的版本号,你也按照小明提供的版本号,修改A模块的依赖版本。是的,这样是可以的,这就需要你和小明频繁的修改POM,如果有更多的模块依赖,这个小问题就会变成一个大问题,而且还很容易出错。
Maven的快照机制就是为了解决上述问题。在这个开发场景中,小明只需要将模块A的版本设定为2.1-SNAPSHOT,然后发布到私服中,在发布的过程中,Maven会自动为包打上时间戳。比如上面的2.1-20190412.221213-52就表示2019年4月12日22点12分13秒的第52次快照。有了这个时间戳,Maven就能随时找到仓库中该包2.1-SNAPSHOT版本最新的文件。当你编译B模块时,Maven会自动从仓库中检查模块A模块的2.1-SNAPSHOT的最新输出包,当发现有更新时便进行下载。
基于快照版本机制,在不需要额外手工操作的情况下,就能完美的解决上述问题。在知道了快照的原理之后,我们的项目不应该依赖任何快照版本的依赖包,由于快照版本的不稳定性,这样的依赖会造成潜在的危险。
镜像
如果仓库X可以提供仓库Y存储的所有内容,那么就可以认为X是Y的一个镜像。也就是说,任何一个可以从仓库Y获得的依赖包,都能够从它的镜像中获取。比如http://maven.aliyun.com/nexus/content/groups/public是阿里提供的中央仓库的镜像,由于地理位置等其它因素,该镜像往往能够提供比中央仓库更快的服务。因此,一般情况下,我们会配置镜像来替代中央仓库。编辑settings.xml文件即可,比如配置阿里提供的镜像:
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
</mirrors>
mirrorOf的值为central,表示该配置为中央仓库的镜像,任何对于中央仓库的请求都会转至该镜像;id、name和url的配置与一般仓库配置一样,表示该镜像仓库的唯一标识符、名称以及地址。
生命周期
在Maven中,核心概念一共有五个,包括前面已经总结完成的坐标、依赖和仓库,以及这里要讲的生命周期,还有将在下一篇要讲的插件。掌握了这五个核心概念,也就基本上把握了Maven的命脉。不夸张的说,把握了这五大核心概念,你可以自豪的说你在Maven的掌握上,超过了80%的人。而除了这五大概念,我总结的其它Maven系列的文章,无法就是Maven的具体场景应用,大体上都离不开这五大核心概念。
什么是生命周期
人生老病死,这是一个生命周期。在Maven中,也有一个生命周期的概念。不管是刚刚入门的开发菜鸟,还是做了多年开发的大佬,每天干的工作无非就是对自己负责的项目进行清理、编译、测试和部署。虽然大家每天都在做这些工作,但是公司和公司之间、项目与项目之间,往往使用不同的方式做这些工作。有的是手工来搞定这些,有的人可能会聪明一些,写一些自动化脚本来搞定这些。不管大家怎么搞,都是能满足自己当下的工作需要,很好的完成自己当下的工作。可能换了个公司,或者换了个项目,把自己之前写的脚本,改吧改吧,接着来。你改吧改吧,能用就好,无可厚非,但是的确很麻烦,搞不好改了半天,发现之前的脚本不好用,可能还要重写。是的,你的痛点问题,Maven也知道,Maven说了,它来帮你搞定这些问题。所以就提出了Maven生命周期的概念。
Maven生命周期就是为了对所有的构建过程进行抽象和统一,开发了一套高度完善的、易扩展的生命周期。这个生命周期包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有构建步骤。换句话说,几乎所有项目的构建,都能映射到这样一个生命周期上。
Maven的生命周期是抽象的,也就是说生命周期本身不做任何实际的工作,在Maven的设计中,实际的任务都交给插件来完成。这种思想与设计模式中的模板方法非常相似。对模板方法设计模式不清楚的伙计可以看这里。Maven采用这样的设计,既保证了Maven整体框架的轻便,也最大程度的扩展性。
Maven生命周期抽象了构建的各个步骤,明确了它们的逻辑次序,但没有提供具体的实现。Maven通过插件机制,这些插件来完成实际的工作,同时每个构建步骤都可以绑定一个或者多个插件行为。为了让Maven开箱即用,Maven为大多数构建步骤编写并绑定了默认插件。比如针对编译的插件有maven-compiler-plugin,针对测试的插件有maven-surefire-plugin等。虽然在大多数时间里,用户几乎都不会感觉到插件的存在,而Maven如此的强大,都是因为在幕后有功能强大的插件,这一切实际的工作都是由这些插件来完成的。
通过Maven定义的生命周期和插件机制保证了所有Maven项目有一致的构建标准,简化了项目的构建工作。
详解maven生命周期
在maven中,有三套相互独立的生命周期,分别是clean、default、site。
- clean : 目的是清理项目
- default : 目的是构建项目
- site : 目的是建立项目站点
每个生命周期包含一些阶段(phase),这些阶段是有顺序的,并且后面的阶段依赖于前面的阶段。我们和Maven最直接的交互方式就是通过调用这些生命周期阶段。以clean生命周期为例,它包含的阶段有pre-clean、clean和post-clean。当我们调用pre-clean的时候,只有pre-clean阶段后执行;当我们调用clean的时候,pre-clean和clean阶段会按顺序执行;当我们调用post-clean的时候,pre-clean、clean和post-clean都会按顺序执行。
和生命周期阶段的前后依赖关系相比,clean、default和site这三套生命周期本身是相互独立的,我们可以仅仅调用clean生命周期的某个阶段,或者仅仅调用default生命周期的某个阶段,而不会对其它生命周期产生任何影响。
常用命令详解
在上面,讲到了一些通过命令行来编译、测试和打包程序的命令,现在总结完了生命周期,再回过头去看这些命令,你将会有更深刻的认识。
- mvn clean:调用实际插件完成clean生命周期的clean阶段的操作,实际调用的是pre-clean和clean两个阶段;
- mvn test :调用default生命周期对应的阶段的插件,完成从validate到test阶段的所有操作;
- mvn clean install:调用clean周期的clean阶段和default的install阶段,实际调用的是pre-clean、clean以及validate到install阶段;
- mvn clean deploy site-deploy:调用完整的三个生命周期所有阶段(post-clean不被调用)。
插件
上文中也讲到了,Maven中通过模板方法这样的设计模式,生命周期只“立牌坊”,而实际上是插件在背后默默的奉献着。也就是说,在Maven中,真正的完成生命周期中那些阶段该干的活,都是由插件来做的。所以,从上面的描述,大家也能感受到Maven中插件的重要性了;插件非常重要,所以它的知识点肯定也少不了,对吧;然而,我的这篇关于插件的文章,我并不打算长篇大论的铺开来将Maven中的插件,因为我觉的有些东西,并不是那样的重要,所以,这篇关于插件的文章,点到为止,意会即可!
插件目标
在继续下面的总结之前,我们需要先来理解“插件目标”这个概念。根据我们的开发经验,在开发一个软件时,这个软件肯定会包含很多的功能,并不会一个功能搞一个软件;同理,Maven中的插件也是这样的,比如下图所示:
而这里的每一个功能就对应一个插件目标。比如maven-dependency-plugin插件有十多个目标,每个目标对应一个功能。在《Maven基础教程之依赖》中讲到的dependency:list、dependency:tree和dependency:analyze这些都是插件目标,这是一种通用的写法,冒号前面是插件前缀,冒号后面是该插件的目标。
插件绑定
说完了插件目标,那这个插件目标到底如何使用呢?上一篇说了生命周期,这个生命周期和插件是相互独立存在的,如果需要插件完成生命周期对应阶段的任何,那就需要将生命周期与插件相互绑定,也就是说需要将生命周期的阶段与插件的目标相互绑定,这样才能完成某个具体的构建任务。
就如上图所示,default生命周期的compile阶段与maven-compiler-plugin插件的compile插件目标,到时候,就由compile插件目标完成default生命周期的compile阶段对应的实际操作。
关于插件绑定,主要分为内置绑定和自定义绑定两大类。
- 内置绑定
我们都知道,为了让Maven开箱即用,Maven开发了很多默认的插件来完成每个生命周期对于阶段的一些工作,同时,也将这些生命周期的一些主要的阶段和这些默认插件的插件目标进行了绑定,这就是内置绑定。对于内置绑定,我们知道有这么一回事,知道Maven的实现原理即可,至于哪个插件的插件目标和那个生命周期的阶段进行内置绑定,有了问题再查也OK的。 - 自定义绑定
为了能补充内置绑定的不足,完成更多个性化的任务,Maven社区的大牛开发了很多的插件,当然了,我们自己也可以开发,后面会讲到的。那这些插件如何和Maven的生命周期的阶段进行绑定呢,这就是我们要说的自定义绑定。下面我们通过一个例子来说明自定义绑定:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.jellythink.HelloWorld.App</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
在POM的build元素下的plugins子元素中声明插件的使用,插件在Maven中同其它包一样,也是作为独立的构建存在,所以也需要通过指定groupId、artifactId和version这三个坐标元素去仓库中定位插件。除了基本的插件坐标声明外,还有插件执行配置,executions下每个execution子元素可以用来配置执行一个任务。上述例子中通过phase配置,将其绑定到package生命周期阶段上,再通过goals配置指定要执行的插件目标,这样自定义插件绑定就完成了。
执行mvn clean install命令,我们就可以看到这样的输出:
[INFO] --- maven-shade-plugin:3.2.1:shade (default) @ hello-world ---
[INFO] Replacing original artifact with shaded artifact.
[INFO] Replacing E:\Code\Spring\helloworld\target\hello-world-1.0-SNAPSHOT.jar with E:\Code\Spring\helloworld\target\hello-world-1.0-SNAPSHOT-shaded.jar
可以看到执行了maven-shade-plugin插件的shade插件目标。
有的时候,你会看到有的插件不通过phase元素配置生命周期阶段,插件目标也能够绑定到生命周期中去。这个时候也不要惊讶,这主要是很多插件的目标在编写时已经定义了默认绑定阶段,我们可以通过maven-help-plugin查看插件的详细信息,了解插件目标的默认绑定阶段。执行mvn help:describe -Dplugin=org.apache.maven.plugins:maven-shade-plugin:3.2.1 -Ddetail就可以看到插件的完整信息,比如这个插件有几个插件目标,有哪些参数,默认绑定阶段等,通过查找Bound to phase: package,我们就可以看到默认绑定到哪个阶段。
插件配置
我们在实现一个功能时,也会想着通过传递参数来实现更强大的功能。Maven插件也是这样的,我们可以配置插件目标的参数,满足我们对插件更加个性化的要求。对于插件的参数配置,有以下两种常用方式:
- 通过命令行进行插件配置
我们经常看到以下这个命令:
mvn clean install -Dmaven.test.skip=true
这个就是典型的通过命令行进行插件配置,maven.test.skip是maven-surefire-plugin提供的一个参数,我们通过命令行传入一个true参数,表示跳过执行测试。
参数-D是Java自带的,其功能是通过命令行设置一个Java系统属性。Maven简单的重用了该参数。
- 通过POM文件进行插件全局配置
比如有些参数配置,从项目创建到项目发布都不会改变,或者基本上很少改变。对于这种场景,就更适合通过POM文件进行插件配置。比如以下的配置我们经常在一些项目中看到:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
</configuration>
</plugin>
</plugins>
</build>
这样就是一个全局的配置,也就是说,所有使用该插件目标的任务,都会使用这些配置。再回头看看上面插件绑定中自定义绑定里的那个例子,那个参数就是插件特有的参数,我们就可以通过参数实现一个不一样的功能。
聚合
我们的需求
在前后台分离,微服务大行其道的今天,我们的应用不再是一个超级大的包了,而是分成了多个子项目模块,每个子项目模块都是一个单独的工程项目。这就出现应用多的情况,此时如果你负责一个功能开发时,需要修改多个子项目模块时,就需要去修改不同的工程项目,当你开发完进行联调时,就需要一次构建多个工程项目,当出现问题时,又可能同时修改多个工程项目,此时你是一个一个工程项目的去构建呢?还是希望有一种办法一次性可以构建多个工程项目。
当然了,我们肯定希望存在一种办法,我们通过点击某个按钮就可以开始构建多个工程项目,然后我们去喝杯咖啡的。那我们的这种需求在Maven中是否能实现呢?毫无疑问,Maven是可以搞定这个问题的,这就是Maven中的聚合,通过聚合我们就可以解决这个痛点问题。下面我就通过实际的项目来说说Maven中的聚合到底是个什么鬼。
聚合实战
现在我准备了两个基于Maven的子工程项目,分别是Project-A和Project-B。这两个项目都可以单独编译,单独构建。但是为了能够使用一条命令就可以构建Project-A和Project-B这两个子工程项目,我们需要创建一个额外的名为Project-Aggregator的工程项目,然后通过该模块构建整个项目的所有模块。由于这个Project-Aggregator的工程项目是一个聚合项目,它是不需要src和test目录的,只需要有一个POM就OK了,下面就是这个Project-Aggregator工程项目的POM文件内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jellythink.AggregatorDemo</groupId>
<artifactId>Project-Aggregator</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Project-Aggregator</name>
<modules>
<module>../Project-A</module>
<module>../Project-B</module>
</modules>
</project>
接下来,我们在Project-Aggregator工程项目目录下执行mvn clean package命令,就会看到以下输出:
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Build Order:
[INFO]
[INFO] Project-A
[INFO] Project-B
[INFO] Project-Aggregator
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Project-A 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ Project-A ---
[INFO] Deleting E:\Code\Spring\Project-A\target
[INFO]
[INFO] --- maven-resources-plugin:3.0.2:resources (default-resources) @ Project-A ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory E:\Code\Spring\Project-A\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ Project-A ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to E:\Code\Spring\Project-A\target\classes
[INFO]
[INFO] --- maven-resources-plugin:3.0.2:testResources (default-testResources) @ Project-A ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory E:\Code\Spring\Project-A\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:testCompile (default-testCompile) @ Project-A ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to E:\Code\Spring\Project-A\target\test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.22.1:test (default-test) @ Project-A ---
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.jellythink.AggregatorDemo.AppTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.032 s - in com.jellythink.AggregatorDemo.AppTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- maven-jar-plugin:3.0.2:jar (default-jar) @ Project-A ---
[INFO] Building jar: E:\Code\Spring\Project-A\target\Project-A-1.0-SNAPSHOT.jar
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Project-B 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:3.1.0:clean (default-clean) @ Project-B ---
[INFO] Deleting E:\Code\Spring\Project-B\target
[INFO]
[INFO] --- maven-resources-plugin:3.0.2:resources (default-resources) @ Project-B ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory E:\Code\Spring\Project-B\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:compile (default-compile) @ Project-B ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to E:\Code\Spring\Project-B\target\classes
[INFO]
[INFO] --- maven-resources-plugin:3.0.2:testResources (default-testResources) @ Project-B ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory E:\Code\Spring\Project-B\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.0:testCompile (default-testCompile) @ Project-B ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to E:\Code\Spring\Project-B\target\test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.22.1:test (default-test) @ Project-B ---
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.jellythink.AggregatorDemo.AppTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.046 s - in com.jellythink.AggregatorDemo.AppTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- maven-jar-plugin:3.0.2:jar (default-jar) @ Project-B ---
[INFO] Building jar: E:\Code\Spring\Project-B\target\Project-B-1.0-SNAPSHOT.jar
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] Building Project-Aggregator 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ Project-Aggregator ---
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO]
[INFO] Project-A .......................................... SUCCESS [ 3.513 s]
[INFO] Project-B .......................................... SUCCESS [ 1.424 s]
[INFO] Project-Aggregator ................................. SUCCESS [ 0.047 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 5.093 s
[INFO] Finished at: 2019-04-22T21:54:08+08:00
[INFO] Final Memory: 19M/274M
[INFO] ------------------------------------------------------------------------
从输出可以看到,我们在构建Project-Aggregator项目时,Project-A和Project-B就会一同被构建,在存在多个项目时,这是非常方便的。
继承
通过上面的聚合内容,大家可能会发现这么个问题,Project-A和Project-B项目的POM文件,可能有很多相同的部分。通过以往的开发经验,如果多个模块有相同的部分,那就意味着我们可以把相同的部分抽取出来,作为公共的部分进行使用,比如在Java中,我们可以把相同的部分放在父类中,子类继承父类,就搞定了。在Maven的世界里,也有类似的机制能让我们提取出重复的配置,这就是POM的继承,而这篇文章就对Maven中的POM继承进行详细的总结。
小试牛刀
这里我还是将通过一个例子来了解一下Maven继承的初步使用配置。还是使用三个工程项目Project-Parent、Project-C和Project-D来进行说明,三个项目关系如下:
Project-Parent工程POM文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jellythink.ExtendDemo</groupId>
<artifactId>Project-Parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Project-Parent</name>
</project>
看这个POM文件,会发现和聚合有几分相像,只是没有modules节点,同样需要注意的是packaging的取值,必须使用pom。
Project-C的POM文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jellythink.ExtendDemo</groupId>
<artifactId>Project-Parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../Project-Parent/pom.xml</relativePath>
</parent>
<artifactId>Project-C</artifactId>
<name>Project-C</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
</dependencies>
</project>
Project-D的POM文件如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jellythink.ExtendDemo</groupId>
<artifactId>Project-Parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../Project-Parent/pom.xml</relativePath>
</parent>
<artifactId>Project-D</artifactId>
<name>Project-D</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.1.6.RELEASE</version>
</dependency>
</dependencies>
</project>
在Project-C和Project-D工程中都使用了parent元素声明父模块,parent下的坐标元素groupId、artifactId和version是必须的,它们指定了父模块的坐标;元素relativePath表示父模块POM的相对路径。在项目构建时,Maven会首先根据relativePath检查父POM。
同时,在Project-C和Project-D工程中,我们都没有在POM中指定groupId和version值,但是在构建时,依然可以成功构建。实际上,在Project-C和Project-D工程从父模块继承了这两个元素,这也就消除了一些不必要的配置。在这个例子中,子模块同父模块使用了同样的groupId和version值,如果遇到子模块需要使用和父模块不一样的groupId或者version的情况,我们则完全可以再子模块中显示声明。
可继承的POM元素
在上面,可以看到groupId和version是可以被继承的,那么还有哪些POM元素可以被继承呢?这里我将一些我们经常用作继承的元素做一下汇总,并进行简单的说明:
- groupId:项目组ID
- version:项目版本
- distributionManagement:项目的部署配置
- properties:自定义的Maven属性
- dependencies:项目的依赖配置
- dependencyManagement:项目的依赖管理配置
- build:包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等
依赖管理
上面说到dependencies是可以被继承的,那我们在Project-C和Project-D工程中很多公共的部分就可以提取出来,统一放到Project-Parent工程中去了,这样就可以移除公共配置,简化配置。
上面说的做法是可行的,但是会存在问题。我们考虑一下这样的一个场景。Project-C和Project-D工程中公共的部分都提取到Project-Parent工程中去了,如果此时新增了一个Project-E工程,而Project-E工程有80%的依赖在Project-Parent工程中可以找到,另外那20%的依赖是个性化的;也就是说,Project-Parent工程中有20%的依赖对于Project-E工程来说是没有用的。这个时候如果Project-E工程继承Project-Parent工程,则会引入无用的依赖,违背了Maven的使用原则。对于这种场景,Maven中也有很完美的解决方案。
Maven提供的dependencyManagement元素既能让子模块继承到父模块的依赖配置,又能保证子模块依赖使用的灵活性。在dependencyManagement元素下的依赖声明不会引入实际的依赖,不过它能够约束dependencies下的依赖使用。现在将Project-Parent工程的POM文件修改如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.jellythink.ExtendDemo</groupId>
<artifactId>Project-Parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<name>Project-Parent</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<springframework.version>5.1.6.RELEASE</springframework.version>
<junit.version>4.11</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${springframework.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
这里使用dependencyManagement,将springframework和junit依赖提取了出来,放到了Project-Parent工程中。这样声明的依赖既不会给Project-Parent引入依赖,也不会给它的子模块引入依赖,不过这段配置是会被继承的。现在,我们将Project-C工程的POM修改为以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.jellythink.ExtendDemo</groupId>
<artifactId>Project-Parent</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../Project-Parent/pom.xml</relativePath>
</parent>
<artifactId>Project-C</artifactId>
<name>Project-C</name>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
</dependencies>
</project>
Project-D项目和Project-C类似,这里不再累述。上面的POM中依赖配置比原来简单了不少,所有的springframework依赖只配置了groupId和artifactId,省去了version。这些依赖的配置都在Project-Parent工程中有了配置,子模块只需要简单的配置groupId和artifactId即可。如果在子模块不声明依赖的使用,即使该依赖已经在父POM的dependencyManagement中声明了,也不会产生任何实际的效果。
插件管理
Maven提供了dependencyManagement元素帮助管理依赖,类似地,Maven也提供了pluginManagement元素帮助管理插件。同dependencyManagement一样,在pluginManagement元素中配置的依赖不会造成实际的插件调用行为,当POM中配置了真正的plugin元素,并且其groupId和artifactId与pluginManagement中配置的插件匹配时,pluginManagement的配置才会影响实际的插件行为。
关于聚合和继承,还有一些比较冷门的只是带你,比如反应堆的裁剪等。。。
测试
多环境构建
什么是多环境构建
一般我们的项目都会有开发环境、测试环境和生产环境,这些环境的数据库等配置基本上都是不一样的,那么我们在进行项目构建的时候就需要能够识别所在的环境并使用正确的配置数据。对于多个环境,我们如何能够灵活的使用不同的配置数据呢?
在Maven中,为了灵活的支持这种场景,内置了三大特性,即属性、Profile和资源过滤。下面我们将通过具体的代码示例来细说这三大特性。
maven属性
对于Maven属性,在前面的文章我们也接触过,比如之前是这样用的:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.jellythink.BookStore</groupId>
<artifactId>project-A</artifactId>
<version>1.0.0</version>
<properties>
<springframework.version>5.1.6.RELEASE</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${springframework.version}</version>
</dependency>
</dependencies>
</project>
通过元素,我们可以自定义一个或多个Maven属性,然后在POM的其它地方使用${属性名称}的方式引用该属性,这种做法的最大意义在于消除重复,便于后期统一修改。但是这不是Maven属性的全部,实际上,在Maven中包含以下六类属性:
-
内置属性
主要有以下两个常用内置属性:- ${basedir}表示项目根目录,即包含pom.xml文件的目录
- ${version}表示项目版本
-
POM属性
我们可以使用POM属性引用POM文件中对应元素的值,比如${project.artifactId}就对应了元素的值,常用的POM属性包括:- ${project.build.sourceDirectory}:项目的主源码目录,默认为src/main/java/
- ${project.build.testSourceDirectory}:项目的测试源码目录,默认为src/test/java/
- ${project.build.directory}:项目构建输出目录,默认为target/
- ${project.outputDirectory}:项目主代码编译输出目录,默认为target/classes/
- ${project.testOutputDirectory}:项目测试代码编译输出目录,默认为target/test-classes/
- ${project.groupId}:项目的groupId
- ${project.artifactId}:项目的artifactId
- p r o j e c t . v e r s i o n :项目的 v e r s i o n ,与 {project.version}:项目的version,与 project.version:项目的version,与{version}等价
- p r o j e c t . b u i l d . f i n a l N a m e :项目打包输出文件的名称,默认为 {project.build.finalName}:项目打包输出文件的名称,默认为 project.build.finalName:项目打包输出文件的名称,默认为{project.artifactId}-${project.version}
这些属性都对应一个POM元素,有一些属性的默认值都是在超级POM中定义的。
- 自定义属性
自定义属性就是通过元素定义的属性,最开始的例子就已经讲的很明白了。 - Settings属性
大家还记得Maven中的settings.xml文件吗?不记得的伙伴可以去看下这篇《Maven基础教程之安装与配置》。而这个Settings属性就表示我们可以使用以settings.开头的属性引用settings.xml文件中XML元素的值,比如我们可以使用${settings.localRepository}来引用用户本地仓库的地址。 - Java系统属性
所有的Java系统属性都可以使用Maven属性引用,比如${user.home}指向用户的目录。我们可以使用mvn help:system查看所有的Java系统属性。 - 环境变量属性
所有环境变量都可以使用以env.开头的Maven属性引用。比如${env.JAVA_HOME}指向了JAVA_HOME环境变量的值。我们可以使用mvn help:system查看所有的Java系统属性。
正确的使用这些Maven属性可以帮助我们简化POM的配置和维护工作。
资源过滤
在我们开发过程中,经常会碰到这样的配置文件:
database.jdbc.driverClass = com.mysql.jdbc.driverClass
database.jdbc.connectionURL = jdbc:mysql://localhost:3306/dev
database.jdbc.username = develop
database.jdbc.password = develop-password
上面的配置数据只是对开发人员的,如果测试人员进行时,则使用的如下这样的一套配置文件:
database.jdbc.driverClass = com.mysql.jdbc.driverClass
database.jdbc.connectionURL = jdbc:mysql://localhost:3306/test
database.jdbc.username = test
database.jdbc.password = test-password
也就是说,在开发环境和测试环境,我们需要使用不同的配置文件,在没有使用Maven之前,我们都是手动的修改对应的配置数据,话又说回来了,这样很麻烦,还很容易出错。现在有了Maven,我们需要作出一点改变。
为了应对不同的使用环境,我们需要将配置文件中变化的部分使用Maven属性替换,比如上面的配置文件,我们需要修改成这个样子:
database.jdbc.driverClass = ${db.driver}
database.jdbc.connectionURL = ${db.url}
database.jdbc.username = ${db.username}
database.jdbc.password = ${db.password}
我们在配置文件中定义了四个Maven属性:db.driver、db.url、db.username和db.password。接下来,我们就需要在某个地方定义这些属性。在Maven中,我们只需要使用一个额外的profile来定义这些属性就可以了。
<profiles>
<profile>
<id>dev</id>
<properties>
<db.driver>com.mysql.jdbc.driverClass</db.driver>
<db.url>jdbc:mysql://localhost:3306/dev</db.url>
<db.username>develop</db.username>
<db.password>develop-password</db.password>
</properties>
</profile>
</profiles>
这里通过profile定义了这些属性,并使用了一个id为dev的值来区别这个profile,这样以后我们就可以针对不同的环境定义不同的profile,就可以非常的灵活。
有了属性定义,配置文件中也使用了这些属性,这样就可以了吗?不是这么简单的!我们都知道,Maven属性默认只有在POM中才会被解析。也就是说, d b . u s e r n a m e 放到 P O M 中会被解析成 d e v e l o p ,但是如果放到 s r c / m a i n / r e s o u r c e s / 目录下的文件中,构建的时候它还是 {db.username}放到POM中会被解析成develop,但是如果放到src/main/resources/目录下的文件中,构建的时候它还是 db.username放到POM中会被解析成develop,但是如果放到src/main/resources/目录下的文件中,构建的时候它还是{db.username}。所以,我们需要让Maven解析资源文件中的Maven属性。
资源文件的处理其实是maven-resources-plugin的工作,但是它默认的行为只是将项目主资源文件复制到主代码编译输出目录中,将测试资源文件复制到测试代码编译输出目录中。我们只需要开启资源过滤,这个插件就能够解析资源文件中的Maven属性。
为主资源目录开启过滤:
<build>
<!-- 为主资源目录开启过滤 -->
<resources>
<resource>
<directory>${project.basedir}/src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<!-- 为测试资源目录开启过滤 -->
<testResources>
<testResource>
<directory>${project.basedir}/src/main/resources</directory>
<filtering>true</filtering>
</testResource>
</testResources>
</build>
我们通过mvn clean package -Pdev命令进行构建。其中-P参数表示在命令行激活一个profile。对于profile没有看懂,不要紧,下面我们再细说。构建完成后,输出目录中的数据库配置就是开发环境的配置了:
database.jdbc.driverClass = com.mysql.jdbc.driverClass
database.jdbc.connectionURL = jdbc:mysql://localhost:3306/dev
database.jdbc.username = develop
database.jdbc.password = develop-password
maven profile
上面说到profile,对于profile大家可能还非常的懵,这里就对profile进行详细的总结。profile是一个非常有用的功能,至少在我们公司的项目中大量使用。profile是专为不同的环境,实现无缝迁移而定制的。我们可以不同的环境配置不同的profile,比如上面提到的开发环境和测试环境两种环境下不同的配置信息,我们可以通过profile进行配置:
<profiles>
<profile>
<id>dev</id>
<properties>
<db.driver>com.mysql.jdbc.driverClass</db.driver>
<db.url>jdbc:mysql://localhost:3306/dev</db.url>
<db.username>develop</db.username>
<db.password>develop-password</db.password>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<db.driver>com.mysql.jdbc.driverClass</db.driver>
<db.url>jdbc:mysql://localhost:3306/test</db.url>
<db.username>test</db.username>
<db.password>test-password</db.password>
</properties>
</profile>
</profiles>
可以看到,同样的属性在两个profile中的值是不一样的;同样的,我们还可以添加更多的profile配置,比如生产配置等。接下来,我们在构建应用时,可以使用-Pdev激活dev profile,使用-Ptest激活test profile。