Maven编译、测试、运行会使用不同的classpath

Maven再编译、测试、运行时会使用三套classpath (编译classpath、测试classpath、运行classpath)

 

Maven有一下几种依赖范围

compile

test

provide

runtime

system

import (Maven 2.0.9 及以上)

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仓库解析的,而且往往与本机系统绑定,可能造成构建的不可移植,因此应该谨慎使用。systemPath元素可以引用环境变量如:

<dependency>
       <groupId>javax.sql</groupId>
       <artifactId>jdbc-stdext</artofactId>
       <version>2.0</version>
       <scope>system</scope>
       <systemPath>${java.home}/lib/rt.jar</systemPath>
</depemdency>

 

import是指?

导入依赖范围,该依赖范围不会对三种classpath产生实际的影响,后面会讲

 

上述除了import以外的各种依赖范围与三种classpath的关系

依赖范围

(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 Framework的项目,如果不使用Maven,那么在项目中就需要手动下载相关依赖,由于Spring Framework又会依赖于其他开源类库,因此实际中往往会下载一个很大的如spring-framework-2.5.6-with-dependencies.zip的包,这里包含了所有Spring Framework的jar包,以及所有它依赖的其他jar包。这么做往往就引入了很多不必要的依赖。

另一种做法是只下载spring-framework-2.5.6.zip这样的包,这里不包含其他相关依赖,到实际使用的时候,在根据出错信息,或者相关文档,加入需要的其他依赖,很显然,这也是一件非常麻烦的事情。

 

Maven的传递性依赖机制可以很好地解决这一问题

如果一个项目有一个org.springframework:spring-core:2.5.6的依赖,而实际上spring-core也有它自己的依赖

我们可以直接访问位于中央仓库的该构建的:

POM:http://repo1.maven.org/maven2/org/springframework/spring-core/2.5.6/spring-core-2.5.6.pom,该文件包含了一个commons-logging依赖:

<dependency>
       <groupId>commons-logging</groupId>
       <artifactId>commons-logging</artifactId>
       <version>1.1.1</version>
</dependency>

account-mail有一个compile范围的spring-core依赖,spring-core有一个compile范围的commons-logging依赖,那么commons-logging就会成为account-email的compile范围依赖,commons-logging就是account-email的一个传递性依赖

 

传递性依赖机制的逻辑是?

Maven会解析各个直接依赖的POM,将那些不必要的间接依赖,以传递性依赖的形式引入到当前的项目中

 

依赖范围也影响传递性依赖

依赖范围不仅可以控制依赖于三种classpath的关系,还对传递性依赖产生影响

比如,account-email对于spring-core的依赖范围是compile,spring-core对于commons-loggin的依赖范围是compile,那么account-email对于commons-logging这一传递性依赖的范围也就是compile

 

什么是第一直接依赖,第二直接依赖?

假设A依赖于B,B依赖于C,我们说A对于B是第一直接依赖,B对于C是第二直接依赖,A对于C是传递性依赖。第一直接依赖的范围和第二直接依赖的范围决定了传递性依赖的范围

 

依赖范围影响传递性依赖


Compile

Test

Provided

Runtime

Compile

Compile

——

——

Runtime

Test

Test

——

——

Test

Provided

Provided

——

Provided

Provided

Runtime

Runtime

——

——

Runtime

最左边的一行表示第一直接依赖范围,最上面一行表示第二直接依赖范围,中间的交叉单元格则表示传递性依赖范围

 

仔细观察上表发现规律

1、当第二直接依赖的范围是compile的时候,传递性依赖的范围与第一直接依赖的范围一致

2、当第二直接依赖的范围是provided的时候,只传递第一直接依赖范围也为provided的依赖,且传递的范围同样为provided

3、当第二直接依赖的范围是runtime的时候,传递性依赖的范围与第一直接依赖的范围一致,但compile例外,此时传递性依赖的范围为runtime

 

传递性依赖机制的问题

Maven引入的传递性依赖机制,一方面大大简化和方便了依赖声明,另一方面,大部分情况下我们只需要关心项目的直接依赖是什么,而不用考虑这些直接依赖会引入什么传递性依赖。但是有时候,当传递性依赖造成问题的时候,我们就需要清楚地知道该传递性依赖是从那条依赖路径引入的

 

如何发现传递性依赖的问题?

比如,项目A有这样的依赖关系: A -> B -> C -> X (1.0)、A->D->X(2.0),X是A的传递性依赖,但是两条依赖路径上有两个版本的X,那么哪个X会被Maven解析使用呢?

两个版本都被解析显然是不对的,因为那会造成依赖重复,因此必须选择一个。

 

Maven依赖调解的第一原则

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,那么到底谁会被解析使用呢?

 

Maven2.0.8及之前的版本

在Maven2.0.8及之前的版本,这是不确定的,解决不了。哦NO

 

Maven2.0.9版本开始

从Maven2.0.9开始,为了尽可能避免构建的不确定性,Maven定义了依赖调解的第二原则

 

什么是依赖调解的第二原则?

Maven的依赖调解第二原则是,第一声明者优先,在依赖路径长度相等的前提下,在POM中依赖声明的顺序决定了谁会被解析使用,顺序最靠前的那个依赖优胜,上面的例子中,如果B的依赖声明在C之前,那么Y(1.0)就会被解析使用

 

什么是可选依赖?假设有这样一个依赖关系,项目A依赖于项目B,项目B依赖于项目X和Y,B对于X和Y的依赖都是可选依赖:A->B、B->X(可选)、B->Y(可选)。根据传递性依赖的定义,如果所有这三个依赖的范围都是compile,那么X、Y就是A的compile范围传递性依赖。然而,由于这X、Y是可选依赖,依赖将不会得以传递。换句话说,X、Y将不会对A有任何影响

 

那么,为什么要使用可选依赖这一特性呢?

可能项目B实现了两个特性,其中的特性一依赖于X,特性二依赖于Y,而且这两个特性是互斥的,用户不可能同时使用两个特性。

 

能不能具体说明一下两个特性互斥是什么意思?

比如B是一个持久隔离工具包,它支持多种数据库,包括MySQL、PostgreSQL等,在构建这个工具包的时候,需要这两种数据库的启动程序,但在使用这个工具包的时候,只会依赖一种数据库。

项目B的依赖声明如下

<project>
       <modelVersion>4.0.0</modelVersion>
       <groupId>com.juvenxu.mvnbook</groupId>
       <artifactId>project-b</artifactId>
       <version>1.0.0<version>
       <dependencies>
              <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-701.jdbc3<version>
                     <optional>true</optional>
              </dependency>
       </dependencies>
</project>

上面的XML代码片段中,使用<optional>元素表示mysql-connector-java和postgresql这两个依赖为可选依赖,它们只会对当前项目B产生影响,当其他项目依赖于B的时候,这两个依赖不会被传递。因此,当项目A依赖于项目B的时候,如果其实际使用基于MySQL数据库,那么在项目A中就需要显式地声明mysql-connector-java这一依赖

项目a的XML代码片段

<project>
       <modelVersion>4.0.0</modelVersion>
       <groupId>com.juvenxu.mvnbook</groupId>
       <artifactId>project-a</artifactId>
       <version>1.0.0<version>
       <dependencies>
              <dependency>
                     <groupId> com.juvenxu.mvnbook </groupId>
                     <artifactId> project-b </artifactId>
                     <version>1.0.0<version>
              </dependency>
              <dependency>
                     <groupId>mysql</groupId>
                     <artifactId>mysql-connector-java</artifactId>
                     <version>5.1.10<version>           
              </dependency>
       </dependencies>
</project>

 

最后强调几点

1、在理想的情况下,是不应该使用可选依赖的

2、使用可选依赖的原因是某一个项目实现了多个特性,但是违背了单一职责原则

3、规划Maven项目的时候也应该遵循这个原则

 

更好的做法是?

1、为MySQL和PostgreSQL分别创建一个Maven项目,基于同样的groupId分配不同的artifactId

2、如com.juvenxu.mvnbook:project-b-mysql和com.juvenxu.mvnbook:project-b-postgresql

3、在各自的POM中声明对应的JDBC驱动依赖,而且不使用可选依赖,用户根据需要选择使用project-b-mysql或者project-b-postgresql,由于传递性依赖的作用,就不用再声明JDBC驱动依赖了

 

最佳实践

传递性依赖带来的两个问题

1、当项目有一个第三方依赖,而这个第三方依赖由于某些原因依赖了另外一个类库的SNAPSHOT版本,那么这个SNAPSHOT就会成为当前项目的传递性依赖,而SNAPSHOT的不稳定性会直接影响到当前的项目。这时就需要排除掉该SNAPSHOT,并且在当前项目中声明该类库的某个正式发布的版本

2、你可能想要替换某个传递性依赖,比如Sun JTA API,Hibernate依赖于这个JAR,但是由于版权的因素,该类库不在中央仓库中,而Apache Gernimo项目有一个对应的实现。这时你就可以排除Sun JTA API,再声明Geronimo的JTA API实现

 

关于排除掉某个传递性依赖如何操作?

<project>
       <modelVersion>4.0.0</modelVersion>
       <groupId>com.juvenxu.mvnbook</groupId>
       <artifactId>project-a</artifactId>
       <version>1.0.0<version>
       <dependencies>
              <dependency>
                     <groupId> com.juvenxu.mvnbook </groupId>
                     <artifactId> project-b </artifactId>
                     <version>1.0.0<version>
                     <exclusions>
                            <exclustion>
                                   <groupId> com.juvenxu.mvnbook </groupId>
                                   <artifactId> project-c </artifactId>
                            </exclustion>
                     </exclustions>
              </dependency>
              <dependency>
                     <groupId> com.juvenxu.mvnbook </groupId>
                     <artifactId> project-c </artifactId>
                     <version>1.1.0<version>
              </dependency>
       </dependencies>
</project>

上面的代码,项目A依赖于项目B,但由于一些原因,不想引入传递性依赖C,而是自己显式地声明对于项目C 1.1.0 版本的依赖。

代码中使用exclusions元素声明排除依赖,exclusions可以包含一个或者多个exclusion子元素,因此可以排除一个或者多个传递性依赖

需要注意的是,groupId和artifactId就能唯一定位依赖图中的某个依赖


如果要升级依赖的版本

考虑这样一种场景,有很多关于Spring Framework的依赖

它们分别是:

org.springframework:spring-core:2.5.6

org.springframework:spring-beans:2.5.6

org.springframework:spring-context:2.5.6

org.springframework:spring-context-support:2.5.6

它们是来自同一项目的不同模块,因此,所有这些依赖的版本都是相同的,而且可以预见

如果将来需要升级Spring Framework,这些依赖的版本会一起升级,这是个问题,所有的version都得改一遍,这种情况在Java中似曾相识

 

如果说技术是想通的话,那么看一下为什么java会喜欢使用常量?

考虑

public double c(double r) {
       return 2 * 3.14 * r
}
 public double s(double r) {       return 3.14 * r * r
}


上面两个函数计算圆的周长和面积,稍微有经验的程序员一眼就会看出一个问题,使用字面量(3.14)显然不合适,应该使用定义一个常量并在方法中使用

public final double PI = 3.14;
public double c(double r) {
       return 2 * PI * r
}
 public double s(double r) {       return PI * r * r
}

使用常量不仅让代码变得更加简洁,更重要的是可以避免重复,在需要更改PI的值的时候,只需要修改移除,降低了错误发生的概率。

 

同理应用与Maven

对于account-mail中的Spring Framework来说,也应该在一个唯一的地方定义版本,并且在dependency声明中引用这一版本,这样,在升级Spring Framework的时候就只需要修改一处

<project>
       <modelVersion>4.0.0</modelVersion>
       <groupId>com.juvenxu.mvnbook</groupId>
       <artifactId>account-email</artifactId>
       <name>Account Email</name>
       <version>1.0.0<version>
      
       <projecties>
              <springframework.version>2.5.6</springframework.version>
       </projecties>
 
       <dependencies>
              <dependency>
                     <groupId>org.springframwork</groupId>
                     <artifactId>spring-core</artifactId>
                     <version>${ springframework.version }<version>
              </dependency>
              <dependency>
                     <groupId>org.springframwork</groupId>
                     <artifactId>spring-beans</artifactId>
                     <version>${ springframework.version }<version>
              </dependency>
              <dependency>
                     <groupId>org.springframwork</groupId>
                     <artifactId>spring-context</artifactId>
                     <version>${ springframework.version }<version>
              </dependency>
              <dependency>
                     <groupId>org.springframwork</groupId>
                     <artifactId>spring-context-support</artifactId>
                     <version>${ springframework.version }<version>
              </dependency>
       </dependencies>
</project>

 

解释上面的代码

这里简单用到了Maven属性,首先使用properties元素定义Maven属性,该例中定义了一个springframework.version子元素,其值为2.5.6

有了这个属性定义之后,Maven运行的时候会将POM中的所有的${springframework.version}替换成实际值2.5.6,也就是说,可以使用美元符号和大括号环绕的方式引用Maven属性。然后,将所有Spring Framework依赖的版本值用这一属性应用表示。这和在Java中用常量PI替换3.14是同样的道理,不同的只是语法


什么是Maven的属性?

 

优化依赖

在软件开发过程中,程序员会通过重构等方式不断地优化自己得代码,使其变得更简洁、更灵活。同理,程序员也应该能够对Maven项目的依赖了然于胸,并对其进行优化,如去除多余的依赖,显式地声明某些必要得依赖

 

什么是已解析依赖(Resolved Dependency)

Maven会自动解析所有项目的直接依赖和传递依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有唯一的版本在依赖中存在,在这些工作之后最后得到的那些依赖被称为一解析依赖


查看当前项目的已解析依赖的命令有mvn dependency:list

mvn dependency:tree

mvn dependency:analyze

 

关于mvn dependency:analyze命令需要注意的是?

mvn dependency:analyze工具可以帮助分析当前项目的依赖

1、Used undeclared dependencies 指项目中使用到的,但没有显式声明的依赖,这种依赖是通过直接依赖传递进来的,当升级直接依赖的时候,相关的传递性依赖的版本也可能发生变化,这种变化不易察觉,但是有可能导致当前项目出错。例如由于接口的改变,当前项目中的相关代码无法编译,这种隐藏的、潜在的威胁一旦出现,就往往需要耗费大量的时间来查明真相,因此,显式声明任何项目中直接用到的依赖

2、Unused declared dependencies,意指项目中未使用的,但显式声明的依赖,对于这类依赖不应该简单地直接删除其声明,而应该仔细分析,由于dependency:analyze只会分析编译主代码和测试代码需要用到的依赖,一些执行测试和运行时需要的依赖它发现不了。