手工安装构件(jar包)到本地仓库
mvn install:install-file -Dfile=jar_path(whereever) -DgroupId=xxx -DartifactId=xxx -Dversion=xxx -Dpackaging=jar
Maven简介
何为Maven
主要服务于基于Java平台的项目构建、依赖管理、项目信息管理。
适用各种规模的项目的构建;无论是传统的瀑布式开发,还是流行的敏捷开发,Maven都能大显身手。
何为构建
除了编写源码,我们每天有相当一部分时间花在了编译、单元测试、生成文档、打包、部署等不起眼的工作上,这就是构建。于是有人用软件的方法让这一系列工作完全自动化,使得项目的构建可以像自动流水线一样,只需一条简单命令,所有繁琐的步骤都能自动完成。
Maven不仅仅是构建工具
它还能帮助我们管理原本分散在项目中各个角落的项目信息,包括项目描述、开发者列表、版本控制系统地址、许可证、bug管理系统地址等,虽然这些微小工作看起来不起眼,却不知不觉为我们节省了大量查找信息的时间。
使用Maven还能享受一个额外的好处:对于项目目录结构、测试用例命名方式等都有既定的规则,只要遵循了这些成熟的规则,用户在Maven项目间切换的学习成本几乎为零,可以说是约定优于配置(COC)。
Maven与极限编程
首先看一下Maven如何帮助XP团队实现一些核心价值:
简单。Maven暴露了一组一致、简单的操作接口,使开发人员从高度自定义、复杂的构建系统中解脱出来。
交流与反馈。与版本控制系统结合,自动生成项目报告。了解项目状态,促进团队交流。
测试驱动开发(TDD)。TDD强调测试先行,所有产品都应该由测试用例覆盖。Maven有现成的成熟的插件支持业界流行的测试框架,如Junit和TestNG。
十分钟构建。强调我们能够随时快速地从源码构建出最终的产品。这正是Maven所擅长的,只需做一些简单的配置,用一条命令就可以让Maven帮你清理、编译、测试、打包、部署,最后得到最终产品。
持续集成(CI)。CI的前提是版本管理系统和构建系统。目前业界流行的CI服务器Hudson和CruiseControl都能很好地和Maven集成。
富有信息的工作区。这条实践强调开发者能够快速方便地了解到项目的最新状态。使用Maven发布的项目报告站点,病配置你需要的项目报告,如测试覆盖率报告,都能帮你把信息推送到开发者面前。
Maven的安装和配置
Windows上安装
设置环境变量:M2_HOME,path变量值的末尾加上%M2_HOME%\bin。
升级Maven:更新M2_HOME环境变量即可。
基于Unix系统上安装
tar -zxvf apache-maven-3.0-bin.tar.gz
虽然直接使用该目录配置环境变量就能使用Maven了,这里推荐做法是:在安装目录旁平行地创建一个符号链接,方便日后升级。
ln -s目录名 链接名
M2_HOME环境变量指向符号链接,并改path环境变量。
export M2_HOME=/home/dev/maven-link
export PATH=$PATH:$M2_HOME/bin
检查Maven安装:
echo $M2_HOME
mvn -v
升级Maven:不必每次升级都必须更新环境变量。假设需要升级到Maven4
rm maven-link
ln -s apache-maven-4 maven-link
Maven使用入门
推荐为每个POM声明name标签,方便信息交流。
定义POM,能让项目对象模型最大程度地与实际代码相独立,我们称之为解耦,很大程度上避免了Java代码和POM代码的相互影响。当项目需要升级版本时,只需修改POM,不需更改Java代码;当POM稳定后,日常的Java代码开发工作基本不涉及POM的修改。
Maven项目结构规范
主代码:src/main/java
测试代码:src/test/java
包名:groupId+artifactId
install:install该任务将项目输出的jar包安装到了本地仓库中,只有安装到本地仓库后,其他Maven项目才能使用。
mvn clean compile
执行了以下任务:
clean:clean、resources: resources、compiler:compile
都是对应 插件:插件目标。
使用Archetype生成项目骨架
简单地运行:
实际上在运行插件maven-archetype-plugin,完整格式为groupId:artifactId:version:goal
之所以可以简化成archetype来代替maven-archetype-plugin插件,因为插件也是由三个坐标准确定位的,groupId、artifactId和version都有一个Maven默认值。
groupId默认是org.apache.maven.plugins。可以添加settings.xml <pluginGroups>下的<pluginGroup>元素,增加默认groupId。
artifactId默认根据插件简称和本地仓库中groupId对应的路径下maven-metadata.xml中匹配<prefix>元素确定插件前缀,进而知道和它同一级的<artifactId>。
version默认根据artifactId对应的路径下maven-metadata.xml文件<release>元素确定。
坐标和依赖
何为Maven坐标
世界上任何一个构件(artifact)的唯一标识,Maven坐标的元素包括:groupId、artifactId、version、packaging、classfier。
坐标详解
groupId:定义当前Maven项目所属的实际项目。Maven项目和实际的一个项目不一定是一对一的关系。比如SpringFramework这个实际项目,其对应的Maven项目会有很多,如spring-core、spring-context等。这些Maven项目相当于实际项目中划分好的模块。表示方式和Java包名表示类似,域名反向。
artifactId:定义实际项目中的Maven项目(实际项目划分好的模块)。推荐使用实际项目名作为artifactId的前缀,这样做的好处是方便寻找实际构件。
version:定义Maven项目当前所处的版本。需要注意的是,Maven定义了一套完整的版本规范,以及快照的概念。
packaging:定义Maven项目的打包方式。通常和生成的构件的扩展名对应,packaging为jar,最终文件名为artifactId-version.jar,使用war打包方式的Maven项目,最终生成一个.war构件。不过这不是绝对的,比如值为maven-plugin的构件扩展名为jar。打包方式会影响到构建的生命周期,比如jar打包和war打包会使用不同的命令。packaging不定义,Maven会使用默认值jar。
classfier:帮助定义构建输出的一些附属构件。附属构件与主构件相对应,例主构件是nexus-core-2.0.jar,该项目可能还会通过使用一些插件生成如nexus-core-2.0-sources.jar、nexus-core-2.0-javadoc.jar这样一些附属构件,其包含了Java源码和文档。这时候,sources和javadoc就是这两个附属构件的classfier。这样,附属构件也就拥有了自己唯一的坐标。
上述5个元素中,groupId、artifactId、version是必须定义的,packaging是可选的,classfier不能直接定义,通过命令行传参设定。
依赖的配置
<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:排除传递性依赖。
依赖范围
JUint依赖的测试范围是test,测试范围用scope元素表示,下面将详解什么是测试范围,以及各种范围的效果和用途。
首先要知道,Maven在编译项目主代码的时候需要用一套classpath,用到spring项目中,项目主代码需要用到spring-core,该文件以依赖的方式引入到classpath中。其次,Maven在编译、测试时会使用另外一套classpath。
依赖范围就是用来控制依赖与三种classpath(编译classpath、测试classpath、运行classpath)的关系,Maven有以下几种依赖范围:
compile:编译依赖范围。没有指定的话,就默认用compile。典型例子是spring-core,三种情况都需要使用该依赖。
test:测试依赖范围。典型例子是JUnit,它只在编译测试代码、运行测试时才需要。
provided:已提供的依赖范围。典型例子是servlet-api,编译和测试项目的时候需要该依赖,但运行项目时,由于容器已经提供,就不需要Maven重复引入。
runtime:运行时依赖范围。典型例子是JDBC驱动实现,项目主代码的编译只需要JDK提供的JDBC接口,只有在执行测试或运行项目时才需要实现上述接口的具体JDBC驱动。
system:系统依赖范围。和provided依赖范围完全一致。但是使用时必须通过systemPath元素显式地指定依赖文件路径。由于依赖文件不是通过Maven解析的,而且往往与本机绑定,可能造成构建的不可移植,因此谨慎使用。systemPath元素可以引用环境变量:
<dependency>
<groupId>java.sql</groupId>
<artifactId>jdbc-stdext</artifactId>
<version>2.0</version>
<scope>system</scope>
<systemPath>${java.home}/lib/rt.jar</systemPath>
</dependency>
import:导入依赖范围。该依赖范围不会对三种classpath产生实际影响,会在POM继承时,对依赖的完全继承起作用。
scope | 编译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之外的类文件 |
传递性依赖
构建一个spring项目,都会导入spring-core构件,在spring-core构件的POM文件包含了一个commons-logging依赖,
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1</version>
<scope></scope>
</dependency>
该依赖没有声明依赖范围,那么默认就是compile,项目p1对spring-core的依赖范围是compile。那么commons-logging就会成为p1的compile范围依赖,准确的说:传递性依赖。
有了此机制,在使用spring的时候就不用考虑它依赖了什么,也不用担心引入多余依赖。Maven会解析各个直接依赖,将那些必要的间接依赖,以传递性依赖形式引入到当前项目中。
依赖范围不仅可以控制三种classpath的关系,还对传递性依赖产生影响。
1 2 | compile | test | provided | runtime |
compile | compile | - | - | runtime |
test | test | - | - | test |
provided | provided | - | provided | provided |
runtime | runtime | - | - | runtime |
-为依赖不能传递
依赖调解
当项目A有这样的依赖关系:A->B->C->X(1.0)、A->D->X(2.0)
两个X依赖的groupId和artifactId相同,但是version不同。
Maven依赖调解(dependency mediation)的第一原则是:路径最近者优先。该例中X(1.0)的路径长度为3,而X(2.0)的路径长度为2,因此X(2.0)会被解析使用。
依赖调解第一原则不能解决所有问题,比如这样的依赖关系:A->B->Y(1.0)、A->C->Y(2.0),Y(1.0)和Y(2.0)的依赖路径长度是一样的,都是2。
Maven依赖调解的第二原则是:第一声明者优先。在依赖路径长度相等的前提下,在POM中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的会被依赖。
可选依赖
项目A依赖于项目B,项目B对于项目X和项目Y的依赖都是可选依赖:A->B、B->X(可选)、B->Y(可选)。
由于X、Y是可选依赖,依赖将不会得以传递。换句话说,X、Y将不会对A有任何影响。
为什么要使用可选依赖这一特性呢?可能项目B实现了两个特性,其中一个特性依赖于X,特性二依赖于Y,并且这个特性是互斥的,用户不可能同时使用两个特性。比如B是一个持久层隔离工具包,它支持多种数据库,包括MySQL PostgreSQL等,在构建B时,需要这两种数据库的驱动,但在使用这个工具包的时候,只会依赖一种数据库。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.10</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>8.4</version>
<optional>true</optional>
</dependency>
当项目A依赖于项目B时,如果其实际基于MySQL数据库,那么在项目A中就需要显式地声明mysql-connector-java这一依赖。
最后,关于可选依赖需要说明一点:在理想情况下,是不该使用可选依赖的。使用可选依赖的原因是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,这个原则在规划Maven项目时也同样适用。在上面的例子中,更好的做法是为MySQL和PostgreSQL分别创建一个Maven项目,基于同样的groupId分配不同的artifactId。
最佳实践
排除依赖
传递性依赖会给项目隐式地引入很多依赖,极大简化了项目依赖的管理。但是有些时候这种特性会带来问题。
例如,当前项目有一个第三方依赖,而这第三方由于某些原因依赖了另外一个类库的SNAPSHOT版本,那么这个SNAPSHOT就会成为当前项目的传递性依赖,SNAPSHOT的不稳定性会直接影响到当前项目。这时候,应该排除掉该SNAPSHOT,并在当前项目中声明该类库的某个正式版本。
你可能想替换某个传递性依赖,比如Sun JTA API,Hibernate依赖于这个jar,但是由于版权因素,该类库不能在中央仓库中,而Apache Geronimo项目有一个对应的实现。这时就可以排除Sun JTA API,再声明Geronimo的JTA API实现,
<dependency>
<groupId>com.gome.qt</groupId>
<artifactId>project-a</artifactId>
<version>1.0</version>
<scope></scope>
<exclusions>
<exclusion>
<groupId>com.gome.qt</groupId>
<artifactId>project-b</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.gome.qt</groupId>
<artifactId>project-b</artifactId>
<version>1.1</version>
</dependency>
注意:声明exclusion的时候只需要groupId和artifactId,而不需要version,因为只需要groupId和artifactId就能唯一定位依赖图中的依赖。
归类依赖
使用常量不仅让代码变得更简洁,更重要的是可以避免重复,同时也降低错误发生率。
同理,对于spring项目A,也应该在一个唯一的地方定义版本,在dependency声明中引用这一版本。
<project>
<properties>
<spring.version>2.5.6</spring.version>
</properties>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
</project>
用<properties>定义Maven属性,定义好后,Maven运行时会将POM中的所有${spring.version}替换成2.5.6。用${}环绕的方式引用Maven属性。
优化依赖
查看当前项目的已解析依赖:
mvn dependency:list
查看当前项目的依赖树(有层次)
mvn dependency:tree
更详细的依赖关系