深入了解maven

前言

虽然之前有简单了解过maven,但是项目中实际使用的时候发现还是存在很多问题,打算深入了解下,于是就选择了《maven实战》这本书。所以这篇其实也可以认为是一篇读书笔记。

第一章

主要是对maven的简单介绍。我这里就先略过了。

第二章

下载和安装

这部分我在之前的文章里面就写了,这里跳过。

安装目录分析

下载下来的maven的目录介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
├── bin
│   ├── m2.conf
│   ├── mvn // 执行程序
│   ├── mvnDebug // 和mvn一致,只是会maven在运行时调试mvn
│   └── mvnyjp
├── boot
│   ├── plexus-classworlds-2.6.0.jar // 类加载器
│   └── plexus-classworlds.license
├── conf
│   ├── logging
│   ├── settings.xml // 修改该文件就可以修改maven全局配置,推荐把这个放到用户目录下
│   └── toolchains.xml
└── lib // lib下面超多jar包,这里省略

运行一下mvn help:system,就可以让maven自动下载一些插件,然后能够打印出系统的各种环境变量。这个插件就在~/.m2/repository/org/apache/maven/plugins下面可以看到。

设置代理

可以通过ping repo1.maven.org来查看目前本机是否能和中央仓库来通信。由于一些特殊的原因,有的时候我们需要设置HTTP代理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<proxies>
<!-- proxy
| Specification for one proxy, to be used in connecting to the network.
|
<proxy>
<id>optional</id>
<active>true</active>
<protocol>http</protocol>
<username>proxyuser</username>
<password>proxypass</password>
<host>proxy.host.net</host>
<port>80</port>
<nonProxyHosts>local.net|some.host.com</nonProxyHosts>
</proxy>
-->
</proxies>

安装最佳实践

这些东西不是必须的,但是我个人觉得还是非常有必要的。

MAVEN_OPTS

因为maven其实本质上是Java程序,所以我们可以通过指定这个环境变量来控制分配的内存,比如-Xms128m -Xmx512m来分配更多的内存空间。

settings.xml

推荐在用户范围来进行设置。

IDE集成

以我目前使用的idea为例,请千万不要使用内嵌的,而是使用自己机器上下载的。这主要是怕如果之后手动使用maven,可能会因为版本之间的差异导致构建行为的不一致,最终可能会导致问题的出现。

idea的设置非常简单,找到maven的安装路径即可。

第三章-使用入门

以下的内容只是初步介绍,详细介绍请见第五章。

  • groupId:一般是公司或者组织的唯一区别号加上实际项目,非常不推荐只写对应的组织和公司。
  • artifactId:定义了项目在当前组里的唯一ID
  • version:版本号
  • name:虽然是非必需的,但是推荐写上该项目的介绍
  • scope:依赖范围,如果是test则该依赖只对测试有效,也就是在测试代码里使用没问题,但是到了主代码里面就会编译错误。

maven的约定:maven会自动寻找src/main/java下面的代码,而不需要额外的配置。同样,我们需要结合POM中定义的groupId和atrifactId来创建我们的包结构。

同样的,测试代码的目录是src/test/java,且约定测试方法都以test为开头。

由于历史原因,maven的compiler插件默认只支持编译Java1.3,所以需要通过以下的代码让其支持更高层级的代码。(现在大部分都是1.8)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<project>
<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>
</project>

第四章-之后的项目介绍

就是一个注册的简单系统介绍。这里跳过。

第五章-坐标与依赖

首先需要明确一个叫“构件” 的概念,其实其本质上就是一个jar包,或者一个war包,任何一个依赖、插件、项目的输出都可以叫做构件。maven通过了唯一标识来定位它们,这些元素包括了groupId,artifactId,version,packaging和classifier。下面需要着重了解一下它们。

  • groupId:定义目前maven项目所属的实际项目。首先需要了解,虽然说了推荐是一一对应,但是实际上是可以不用一一对应的。比如知名的spring项目,其实它对应了spring-context,spring-core…..其次是,这个字段请不要只写公司或者组织,因为公司会有很多项目,而下面的artifaceId只能定义模块,那么项目就没地方写了。所以最最推荐的就是:公司的域名反写+项目名字作为你的groupId。
  • artifactId:定义了一个实际的模块。比较推荐的是,使用实际项目名字作为前缀,例子就是spring-context来作为artifactId而不是仅仅是context。这是因为默认情况下,最后生成的jar包是用artifactId来作为开头的,所以如果加上了项目名字,就能很好的区分了。但是老实说我实际中就只是用模块名字来定义,问题也不是很大..
  • version:emmm 其实还是很复杂的,但是这里先不展开了。
  • packaging:平时基本不写,因为默认就是jar的方式来打包。
  • classifier:注意这个是不能直接写到文件里面的。它的作用是用来帮助输出一些附属的构件,比如xxx-javadoc.jar这种的。

接下来是依赖声明可以包含的元素,注意,上面的是自己定义的,而下面的这些是你要去引用别人的构件的时候索要用到的:

  • groupId,artifactId和version:这三个的意思和上面的一样。
  • type:依赖的类型,默认是jar
  • scope:依赖的范围。由于maven需要控制依赖和三种classpath(编译、测试和运行)之间的关系,所以需要不同的范围。
    • compile:编译依赖范围。默认就是它,对上面三种classpath都有效。
    • test:测试依赖范围。只对测试classpath有效。在编译主代码的时候或者是运行项目的时候就无法使用,最最典型的使用就是JUnit。
    • provided:已提供的依赖范围。对于编译和测试有效,到了真正运行项目的时候不需要。最最幽冥的例子就是servlet-api,由于真正运行的时候有了tomcat等容器的提供,所以不需要。
    • runtime:运行时的依赖范围。对于测试和运行有效,但是对于编译无效。最典型的例子就是jdbc了,编译的时候不需要它,只有当测试和运行才需要。
    • system:和provided一致。但是它和本机系统绑定,可以通过<systemPath>来引用环境变量。所以如果要可移植的话,请不要使用它。
    • import:请见第八章。
  • optional:依赖是否可选。假设有一个项目依赖了两个项目A和B,并将它们设置成可选的,那么当别的项目来依赖这个项目的时候,A和B都不会被传递。在理想情况下,不应该使用这个特性,因为其实它违背了单行职责原则。
  • exclusions:用来排除传递性依赖。传递性依赖,就是用来解决套娃问题的。假设A依赖于B,B依赖于C,那么就说A对于B是第一直接依赖,B是C第二直接依赖,A对C是传递性依赖。而根据第一和第二直接依赖的不同,就有了传递性依赖的不同范围。但是这个规则比较复杂,所以其实大部分情况下我们只需要关注项目直接依赖什么,而不需要去理会传递依赖。

最佳实践

在使用spring的时候,会用到很多spring下面的依赖,而且它们的版本号是一致的。如果未来需要升级,那么我们肯定希望修改一处就能便利的升级,而不是对每一个依赖都修改它们的版本号。所以我们可以这么做:

1
2
3
4
5
6
7
8
9
10
<properties>
<mysql.version>5.7</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
</dependencies>

maven能够确保,任何一个构件只有唯一的版本在依赖中存在。可以通过mvn dependency:list看到当前项目的已解析的依赖;或者是通过mvn dependency:tree来查看树形结构。不过idea里面有更加优秀的可视化图形界面显示。当然你还可以通过mvn dependency:analyze来分析项目的依赖。

第六章-仓库

仓库的存在,就是为了能够复用这些组件。

仓库的布局

首先需要看下,我们通过声明了groupId,artifactId,version,packaging之后,它们是如何转换成我们的构件的。

1
2
3
4
5
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>5.8</version>
</dependency>
  1. 首先把groupId中的点号,变成路径分隔符,于是我们得到了org/testng/
  2. 继续加上artifactId,于是就有了org/testng/testng/
  3. 继续加上版本信息,org/testng/testng/5.8/
  4. 加上artifactId和版本信息,它们两者之间用构件分隔符来分割,org/testng/testng/5.8/testng-5.8注意,这里最后目前还没有斜杠
  5. 如果有classifier,那么就继续在后面加上构件分隔符和classifier,这里我们是存在的,那么就变成了org/testng/testng/5.8/testng-5.8-jdk5
  6. 通过packaging来为其加上最后的扩展名,由于默认是jar,所以这个构件最后的结果就是org/testng/testng/5.8/testng-5.8-jdk5.jar

仓库的分类

对于maven来说,只存在两大分类:本地仓库和远程仓库,而且会优先在本地仓库中查找,如果本地仓库有,就直接使用;如果本地仓库没有,或者这个构件有更新的版本,那么就会去远程仓库找,并且下载到本地仓库,方便之后使用;如果本地仓库和远程仓库都没有,就报错。

本地仓库

不论是windows还是linux下,默认在用户家目录下面都有一个~/.m2/repository的目录,用来作为本地仓库使用。当然你可以通过修改settings(复制全局的settings到本地,别修改全局的)来修改这个目录:

1
2
3
4
5
6
<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->

远程仓库

中央仓库

由于最开始下载maven的时候,本地仓库里面肯定是空的,所以需要去远程仓库下载。中央仓库就是这么一个默认的远程仓库,可以找到$M2_HOME/lib/maven-model-builder-x.x.x.jar(取决于你的maven版本),然后通过jar -xvf xxx.jar解压这个jar包,然后通过org/apache/maven/model/pom-4.0.0.xml来看到:

1
2
3
4
5
6
7
8
9
10
11
<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>
私服

这是一个架设在局域网内的“中央仓库”,书中推荐,就算是只有一个人,也推荐使用私服。因为maven在使用的时候会去检查远程仓库的数据(就算你本地有构件),如果用了私服会极大提升速度。下面是一段在pom文件里使用远程仓库的配置(注意不是在settings.xml里面,当然settings.xml里面也可以,注意修改下顶级标签即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<project>
<repositories>
<repository>
<id>central</id>
<name>Central Repository</name>
<url>https://repo.maven.apache.org/maven2</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
<layout>default</layout>
</repository>
</repositories>
</project>

这段声明了不使用快照版的构件,只使用发布版的构件。同时还可以指定maven每隔多久去远程仓库检查更新和文件校验。

同时如果远程仓库需要账号和密码信息,那么必须在settings.xml文件里面进行配置。

1
2
3
4
5
6
7
<servers>
<server>
<id>deploymentRepo</id>
<username>repouser</username>
<password>repopwd</password>
</server>
</servers>

注意,上面的这个id,就和仓库的id进行一一对应。

最后,你可能还希望自己的构件能够发布到远程仓库,那就需要去配置distributionManagement,具体配置这里略过,因为没有这个需求。

镜像

如果任何一个能从Y得到的构件,都能从X中获得,那么就成X是Y的镜像。其中需要注意的是<mirrorOf>这个标签,配置成了central,说明任何对中央仓库的请求都会转到这里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/repositories/central</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>

当然镜像其实一般都可以配合私服使用。因为私服,其实就是所有远程仓库的镜像,那我们可以直接在<mirrorOf>这个标签里写上星号,代表任何。

搜索服务

可以通过以下网站,找到你心仪的jar包:

第七章-生命周期和插件

首先要明确,生命周期是一个抽象概念,实际中是靠maven的一个一个插件来完成的,所以你当然可以编写自己的插件,来自己接手某一个或者多个流程。

生命周期

maven有三套生命周期,而且彼此之间互相独立,分别是clean,default和site。显然clean的作用是清理项目,default用来构建项目,而site则是建立项目的站点。再次重申:三个生命周期之间,是彼此独立的,不相互影响的;而在同一个生命周期里,执行某一个步骤,会执行之前的所有步骤。

clean

  1. pre-clean:执行一些清理前需要完成的工作
  2. clean:清理上一次构建所生成的文件
  3. post-clean:清理完成后需要执行的工作

default

  1. validate
  2. initialize
  3. generate-sources
  4. process-sources:这个步骤,就把src/main/resources下面的内容进行变量替换等操作,然后复制classpath中
  5. generate-resources
  6. process-resources:复制主资源文件到主输出目录里。
  7. compile:编译项目的主源码,即编译src/main/java下面的.java文件到项目输出的主classpath中
  8. process-classes
  9. generate-test-sources
  10. process-test-sources:看名字就懂了吧,把src/test/resources下面的内容进行操作然后复制到项目输出的测试classpath中
  11. generate-test-resources
  12. process-test-resources
  13. test-compile:编译项目的测试代码,即编译src/test/java下面的.java文件到项目输出的测试classpath中
  14. process-test-classes
  15. test:使用单元测试框架运行测试,而测试的代码不会被打包或者部署
  16. prepare-package
  17. package:接受编译好的代码,打包
  18. pre-integration-test
  19. integration-test
  20. post-integration-test
  21. verify
  22. install:安装到本地的maven仓库里
  23. deploy:复制到远程仓库,供其它项目使用。

site

site的主要目的是建立和发布项目的站点,简单来说就是maven根据你的pom文件来生成一个站点。

  1. pre-site
  2. site
  3. post-site
  4. site-deploy:将生成的站点发布到服务器上。

可以看到,这三个生命周期里面并没有任何一个和另外的重复,所以你完全可以通过mvn clean deloy site-deploy来调用它们。

而之前用到的命令,比如mvn dependency:list,用的是某一个插件的某一个功能。所以一个插件可以对应多个生命周期。

当然我们可以自定义绑定插件到生命周期里。在Pom文件里的build元素下的plugins元素中就可以指定。

而且我们也可以修改某些插件的默认属性,比如我希望用Java的1.8版本来进行编译,就可以通过修改编译的插件,来进行告知。

接下来的内容是关于插件的介绍以及如何寻找等详细内容,我目前没有这方面的需求,就暂时跳过了。

第八章-聚合与继承

聚合

在实际项目中,我们会遇到多个模块依赖同一个第三方jar包的情况,比如我之前做商城的时候就遇到了,解决方法就是利用maven的聚合特性,把这些大家都依赖的第三方给jar包给聚合到一个模块中,然后通过继承的特性“分发”下去。

由于聚合模块本身也是一个maven项目,所以它也有自己的pom文件,在里面也有对应的groupId、artifaceId和version,不同的是它还多了一个<modules>的标签,里面通过一个又一个的<module>标签来指定了其他项目的名字。与此同时别忘记把packaging改为pom,因为这个maven项目是不需要打包成jar的,或者说,聚合模块的packaging必须是pom。

关于聚合,有两种项目结构,一种是聚合的那个项目作为父目录,然后其他项目作为子目录;另外一种是聚合项目和其它项目作为平行项目,这些都是可以的。

如果是希望使用父子目录,那么可以这么写:

1
2
3
4
5
<modules>
<module>mall-xxx1</module>
<module>mall-xxx2</module>
<module>mall-xxx3</module>
</modules>

如果是希望平行目录,那么可以这么写:

1
2
3
4
5
<modules>
<module>../mall-xxx1</module>
<module>../mall-xxx2</module>
<module>../mall-xxx3</module>
</modules>

聚合的好处就在于,maven首先会解析聚合模块的pom,然后就能知道需要哪些模块,然后就能依次去构造它们。

继承

我们一般会使用一个公共的模块来管理项目的依赖关系,比如我的项目里面叫mall-common

然后在需要引用这个公共模块的地方,直接使用<parent>标签即可,在里面指定好groupId,artifactId和version,这三个是必须的;最后还推荐加上一个relativePath标签来指定父模块的pom文件所在地址。这么做的理由是,为了能够在本地仓库没有父模块的时候也能够找到父模块。


我们可以发现,聚合和继承是完完全全不同的两个概念,目的完完全全不同,如果非要说什么相同的话,大概就是它们的packaging都必须是pom吧~

聚合是为了方便快速构建——你在聚合模块中直接mvn install就相当于对所有被聚合项目来一发mvn install,而继承是为了减少重复配置——减少那些重复的依赖声明。

但是在实际中,你会发现其实是把这两种作为一个使用的,既声明了包含了哪些模块,同时又有依赖的指定。

约定大于配置

为了能够简单使用,那么就必须要遵守大家的约定,这样才能简单开发。当然,可能会有历史遗留的代码,比如人家就不是把源码写在src/main/java下面,而是写在了src/java下,这个时候你是可以通过配置maven来妥协的。但是除了这种情况,其它任何情况都推荐使用约定的配置。

第九章-使用Nexus创建私服

说实话这部分我不感兴趣,毕竟直接使用阿里云的镜像,它不香吗?