前言
虽然之前有简单了解过maven,但是项目中实际使用的时候发现还是存在很多问题,打算深入了解下,于是就选择了《maven实战》这本书。所以这篇其实也可以认为是一篇读书笔记。
第一章
主要是对maven的简单介绍。我这里就先略过了。
第二章
下载和安装
这部分我在之前的文章里面就写了,这里跳过。
安装目录分析
下载下来的maven的目录介绍:
1 | . |
运行一下mvn help:system,就可以让maven自动下载一些插件,然后能够打印出系统的各种环境变量。这个插件就在~/.m2/repository/org/apache/maven/plugins下面可以看到。
设置代理
可以通过ping repo1.maven.org来查看目前本机是否能和中央仓库来通信。由于一些特殊的原因,有的时候我们需要设置HTTP代理。
1 | <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 | <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:其实还是很复杂的,但是这里先不展开了。
- 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 | <properties> |
maven能够确保,任何一个构件只有唯一的版本在依赖中存在。可以通过mvn dependency:list看到当前项目的已解析的依赖;或者是通过mvn dependency:tree来查看树形结构。不过idea里面有更加优秀的可视化图形界面显示。当然你还可以通过mvn dependency:analyze来分析项目的依赖。
第六章-仓库
仓库的存在,就是为了能够复用这些组件。
仓库的布局
首先需要看下,我们通过声明了groupId,artifactId,version,packaging之后,它们是如何转换成我们的构件的。
1 | <dependency> |
- 首先把groupId中的点号,变成路径分隔符,于是我们得到了
org/testng/。 - 继续加上artifactId,于是就有了
org/testng/testng/ - 继续加上版本信息,
org/testng/testng/5.8/ - 加上artifactId和版本信息,它们两者之间用构件分隔符来分割,
org/testng/testng/5.8/testng-5.8。注意,这里最后目前还没有斜杠 - 如果有classifier,那么就继续在后面加上构件分隔符和classifier,这里我们是存在的,那么就变成了
org/testng/testng/5.8/testng-5.8-jdk5。 - 通过packaging来为其加上最后的扩展名,由于默认是jar,所以这个构件最后的结果就是
org/testng/testng/5.8/testng-5.8-jdk5.jar
仓库的分类
对于maven来说,只存在两大分类:本地仓库和远程仓库,而且会优先在本地仓库中查找,如果本地仓库有,就直接使用;如果本地仓库没有,或者这个构件有更新的版本,那么就会去远程仓库找,并且下载到本地仓库,方便之后使用;如果本地仓库和远程仓库都没有,就报错。
本地仓库
不论是windows还是linux下,默认在用户家目录下面都有一个~/.m2/repository的目录,用来作为本地仓库使用。当然你可以通过修改settings(复制全局的settings到本地,别修改全局的)来修改这个目录:
1 | <!-- 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 | <repositories> |
私服
这是一个架设在局域网内的“中央仓库”,书中推荐,就算是只有一个人,也推荐使用私服。因为maven在使用的时候会去检查远程仓库的数据(就算你本地有构件),如果用了私服会极大提升速度。下面是一段在pom文件里使用远程仓库的配置(注意不是在settings.xml里面,当然settings.xml里面也可以,注意修改下顶级标签即可):
1 | <project> |
这段声明了不使用快照版的构件,只使用发布版的构件。同时还可以指定maven每隔多久去远程仓库检查更新和文件校验。
同时如果远程仓库需要账号和密码信息,那么必须在settings.xml文件里面进行配置。
1 | <servers> |
注意,上面的这个id,就和仓库的id进行一一对应。
最后,你可能还希望自己的构件能够发布到远程仓库,那就需要去配置distributionManagement,具体配置这里略过,因为没有这个需求。
镜像
如果任何一个能从Y得到的构件,都能从X中获得,那么就成X是Y的镜像。其中需要注意的是<mirrorOf>这个标签,配置成了central,说明任何对中央仓库的请求都会转到这里。
1 | <mirrors> |
当然镜像其实一般都可以配合私服使用。因为私服,其实就是所有远程仓库的镜像,那我们可以直接在<mirrorOf>这个标签里写上星号,代表任何。
搜索服务
可以通过以下网站,找到你心仪的jar包:
第七章-生命周期和插件
首先要明确,生命周期是一个抽象概念,实际中是靠maven的一个一个插件来完成的,所以你当然可以编写自己的插件,来自己接手某一个或者多个流程。
生命周期
maven有三套生命周期,而且彼此之间互相独立,分别是clean,default和site。显然clean的作用是清理项目,default用来构建项目,而site则是建立项目的站点。再次重申:三个生命周期之间,是彼此独立的,不相互影响的;而在同一个生命周期里,执行某一个步骤,会执行之前的所有步骤。
clean
- pre-clean:执行一些清理前需要完成的工作
- clean:清理上一次构建所生成的文件
- post-clean:清理完成后需要执行的工作
default
- validate
- initialize
- generate-sources
- process-sources:这个步骤,就把
src/main/resources下面的内容进行变量替换等操作,然后复制classpath中 - generate-resources
- process-resources:复制主资源文件到主输出目录里。
- compile:编译项目的主源码,即编译
src/main/java下面的.java文件到项目输出的主classpath中 - process-classes
- generate-test-sources
- process-test-sources:看名字就懂了吧,把
src/test/resources下面的内容进行操作然后复制到项目输出的测试classpath中 - generate-test-resources
- process-test-resources
- test-compile:编译项目的测试代码,即编译
src/test/java下面的.java文件到项目输出的测试classpath中 - process-test-classes
- test:使用单元测试框架运行测试,而测试的代码不会被打包或者部署
- prepare-package
- package:接受编译好的代码,打包
- pre-integration-test
- integration-test
- post-integration-test
- verify
- install:安装到本地的maven仓库里
- deploy:复制到远程仓库,供其它项目使用。
site
site的主要目的是建立和发布项目的站点,简单来说就是maven根据你的pom文件来生成一个站点。
- pre-site
- site
- post-site
- 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 | <modules> |
如果是希望平行目录,那么可以这么写:
1 | <modules> |
聚合的好处就在于,maven首先会解析聚合模块的pom,然后就能知道需要哪些模块,然后就能依次去构造它们。
继承
我们一般会使用一个公共的模块来管理项目的依赖关系,比如我的项目里面叫mall-common。
然后在需要引用这个公共模块的地方,直接使用<parent>标签即可,在里面指定好groupId,artifactId和version,这三个是必须的;最后还推荐加上一个relativePath标签来指定父模块的pom文件所在地址。这么做的理由是,为了能够在本地仓库没有父模块的时候也能够找到父模块。
我们可以发现,聚合和继承是完完全全不同的两个概念,目的完完全全不同,如果非要说什么相同的话,大概就是它们的packaging都必须是pom吧~
聚合是为了方便快速构建——你在聚合模块中直接mvn install就相当于对所有被聚合项目来一发mvn install,而继承是为了减少重复配置——减少那些重复的依赖声明。
但是在实际中,你会发现其实是把这两种作为一个使用的,既声明了包含了哪些模块,同时又有依赖的指定。
约定大于配置
为了能够简单使用,那么就必须要遵守大家的约定,这样才能简单开发。当然,可能会有历史遗留的代码,比如人家就不是把源码写在src/main/java下面,而是写在了src/java下,这个时候你是可以通过配置maven来妥协的。但是除了这种情况,其它任何情况都推荐使用约定的配置。
第九章-使用Nexus创建私服
说实话这部分我不感兴趣,毕竟直接使用阿里云的镜像,它不香吗?