这又是一个系列,一个要把Maven讲透的系列,但愿可以对你们有帮助!
在前面的总结中,老是说到依赖这个东西,并且还有看到dependencies
这个词在pom.xml文件中的使用,因此不少读者就很火烧眉毛的想知道这个依赖究竟是什么东西?做为Maven中一个很是重要的概念,那到底该如何使用和配置,以及使用过程当中有哪些注意事项,而这篇文章就是对Maven中的依赖进行详细的总结,一扫对依赖概念的不解。java
在Maven中,是在pom.xml文件中完成依赖的配置,咱们先来看看依赖配置的语法。mysql
<project> ... <dependencies> <dependency> <groupId>...</groupId> <artifactId>...</artifactId> <version>...</version> <type>...</type> <scope>...</scope> <optional>...</optional> <exclusions> <exclusion> ... </exclusion> </exclusions> </dependency> ... </dependencies> ... </project>
乍一看,这个配置仍是蛮复杂的,其实咱们经常使用的没有这么多,并且这些用起来也是很是简单的。根元素project下的dependencies能够包含一个或者多个dependency元素,以声明一个或者多个项目依赖。下面就详细说一下这些配置的含义。spring
groupId
、artifactId
和version
:依赖的基本坐标,对于任何一个依赖来讲,基本坐标是最重要的,Maven根据坐标才能找到须要的依赖;type
:依赖的类型,对应于项目坐标定义的packaging,大部分状况下,该元素没必要声明,其默认值为jar;scope
:依赖的范围,这个内容就比较多一点,下面会专门进行总结;optional
:标记依赖是否可选,下面会专门进行总结;exclusions
:用来排除传递性依赖,下面会专门进行总结。不少时候,大部分依赖声明只包含groupId
、artifactId
和version
这三个指定基本坐标的元素;而在一些特殊状况下,其它元素相当重要,也就是上面提到的scope
、optional
和exclusions
。下面就对这三个要素进行详细的总结。sql
不知道你们还记不记得在《Maven基础教程之使用入门》中的这段junit依赖代码:数据库
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies>
这里就指定了scope这个要素的值,那这里指定这个要素有什么含义呢?apache
咱们须要知道,Maven在编译项目主代码的时候须要使用一套classpath。举例来讲:segmentfault
因此依赖范围就是用来控制依赖与这三种classpath(编译classpath、测试classpath、运行classpath)的关系。在Maven中,咱们能够针对scope要素设置如下依赖范围:api
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仓库解析的,并且每每与本机系统绑定,可能形成构建的不可移植,所以谨慎使用。system
的使用举例:微信
<dependency> <groupId>com.jellythink.BookStore</groupId> <artifactId>BookStore-SSO</artifactId> <version>1.0</version> <scope>system</scope> <systemPath>${basedir}/lib/BookStore-SSO-1.0.jar</systemPath> </dependency>
对于system
系统依赖范围,在进行以上配置之后,编写代码时已经能够引入Jar包中的class了,可是在打包时,因为scope=system
,默认并不会将依赖包打进WAR包中,全部须要经过插件进行打包。例如:框架
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <version>2.10</version> <executions> <execution> <id>copy-dependencies</id> <phase>compile</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/lib</outputDirectory> <includeScope>system</includeScope> </configuration> </execution> </executions> </plugin>
会了更好的理解和记忆依赖范围与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仓库以外的类库文件 |
说到依赖传递,这里的关系就比较复杂,在没有使用Maven以前,你们是否有这样的开发体验;好比引入了包A,因为咱们不知道包A的依赖,只能在编译的时候,根据出错信息,再加入须要的其它依赖,很显然,这样的开发体验是及其糟糕的。而如今有了Maven,Maven中的传递性依赖机制能够很好的解决这一问题。这里就来详细的总结Maven中的依赖传递。
A依赖B,B又依赖C。因为基于Maven建立的项目,有了传递性依赖机制,在使用A的时候就不用去考虑A依赖了什么,也不用担忧引入多余的依赖。Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。
对于上面的图,最左面的一列表示第一直接依赖范围,最上面一行表示第二直接依赖范围。好比A对B的依赖scope是compile,B对C的依赖scope是runtime,那么A对C的依赖scope就是runtime。以下图标注所示:
在实际使用过程当中,对于这个依赖传递范围的关注仍是比较少的,在之后的使用过程当中,若是遇到问题,咱们应该能想到这里总结的依赖传递范围相关的知识点。
依赖调解
先来讲一个实际开发过程当中常常会遇到的两个问题。
问题一:好比项目A有这样的两个依赖关系:
从上图能够看到,项目A有两条依赖关系,X是A的传递性依赖,可是你会发现两条依赖路径上有两个版本的X,那么哪一个X会被Maven解析使用呢?两个版本都被解析显然是不对的,由于那会形成依赖重复,所以必须选择一个。这种问题,该如何解决呢?
问题二:好比项目A有这样的两个依赖关系:
对于这种状况,和问题一的区别就是X做为A的传递性依赖,两个版本的依赖路径长度是同样的,都是2,这状况下,那么到底谁又会被解析呢?
这里就涉及到Maven中的依赖调解了,当出现上述的两个问题时,Maven就会运用内置的两个调解原则,肯定到底哪一个依赖会被最终解析使用。这两个内置的调解原则以下:
可选依赖
在Maven的世界里,存在着这样的一种依赖关系,以下图所示:
从上图能够看到,M和N对于B都是可选依赖,依赖将不会进行依赖范围传递,也就是说,若是A须要使用M或N时,还须要在pom.xml中显示的进行声明。
既然可选依赖没有依赖范围传递,致使咱们须要人工不得不去进行一些显示的依赖声明,那为何还要使用可选依赖这一特性呢?存在即合理!咱们想象这样的一种场景,项目B实现了两个特性,其中的一个特性依赖于M,另外一个特性依赖于N,并且这两个特性是互斥的,用户不可能同时使用这两个特性。好比B是一个持久层隔离工具包,它支持多种数据库,包括MySQL、PostgreSQL等,在构建这个工具包的时候,须要这两种数据库的驱动程序,但在使用这个工具包的时候,只会依赖一种数据库。这种状况下,项目B的依赖声明就会是这样的:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-B</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> <optional>true</optional> </dependency> <dependency> <groupId>postgresql</groupId> <artifactId>postgresql</artifactId> <version>9.1-901-1.jdbc4</version> <optional>true</optional> </dependency> </dependencies> </project>
上述pom.xml代码片断中,使用<optional>
元素表示mysql和postgresql这两个依赖为可选依赖,这样一来,它们只会对当前的项目B产生影响,当其它项目依赖于B的时候,这两个依赖不会被传递。所以,当项目A依赖于项目B的时候,若是其实际使用基于MySQL数据库,那么在项目A中就须要显式地声明mysql依赖,好比这样:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-A</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-B</artifactId> <version>1.0.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency> </dependencies> </project>
看完上面关于可选依赖的总结,你们都会觉的可选依赖确实很是麻烦,原本使用Maven是为了提高开发效率的,如今到好,搞的更复杂了;因此,在理想的状况下,是不该该使用可选依赖的。经过上面的例子能够看到,使用可选依赖的缘由是某一个项目实现了多个特性,在面向对象设计中,有个单一职责性原则,意指一个类应该只有一项职责,而不是糅合太多的功能。这个原则在规划Maven项目的时候也一样适用。这样,对于上面的例子,更好的作法是为MySQL和PostgreSQL分别建立一个Maven项目,基于一样的groupId分配不一样的artifactId,在各自的POM中声明对应的JDBC驱动依赖,并且不使用可选依赖,这样用户则根据须要选择使用对应的依赖便可。
Maven的依赖涉及的知识点比较多,结合上面整理的内容,再经过结合前人的经验,这里分享一些实战经验,方便你们更好的理解和使用Maven。
排除依赖
经过上面的总结,你们能够感觉到传递性依赖带来的便利,可是有些时候这种特性也会带来问题。你们想象一下如今的这个应用场景。当前项目有一个第三方依赖,而这个第三方依赖因为某些缘由依赖了另一个类库的SNAPSHOT版本,那么这个SNAPSHOT就会成为当前项目的传递性依赖,而SNAPSHOT的不稳定性会直接影响到当前项目。这时就须要排除该SNAPSHOT版本,而且在当前项目中声明该类库的某个正式发布的版本。这种状况就须要使用到Maven中的排除依赖了,经过exclusion
关键字来排除对应的依赖。代码片断以下:
<project> <modelVersion></modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-A</artifactId> <version>1.0.0</version> <dependencies> <dependency> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-B</artifactId> <version>1.0.0</version> <exclusions> <exclusion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-C</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-C</artifactId> <version>1.1.0</version> </dependency> </dependencies> </project>
上面代码的依赖逻辑以下图所示:
项目A依赖于项目B,可是因为某些缘由,不想引入传递性依赖C,而是本身显式地声明对于项目C 1.1.0版本的依赖。代码中使用exclusions
元素声明排除依赖,exclusions
能够包含一个或者多个exclusion
子元素,所以能够排除一个或者多个传递性依赖。须要注意的是,声明exclusion
的时候只须要groupId
和artifactId
,而不须要version
元素,这是由于只须要groupId
和artifactId
就能惟必定位依赖图中的某个依赖。换句话说,Maven解析后的依赖中,不可能出现groupId
和artifactId
相同,而version
不一样的两个依赖。
归类依赖
如今不少应用都是基于Spring框架开发,可是使用Spring框架时,就须要引入多个Spring框架的依赖,好比这样子的:
<project> <modelVersion>4.0.0</modelVersion> <groupId>com.jellythink.BookStore</groupId> <artifactId>project-A</artifactId> <version>1.0.0</version> <dependencies> <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>
能够看到,这些依赖都是Spring框架的不一样模块,在实际项目中,全部这些依赖的版本都是相同的,并且能够预见,若是未来须要升级Spring框架,这些依赖的版本会一块儿升级。按照上面的代码片断,咱们须要全部Spring框架模块的version
字段,这样一个一个的修改增长了错误发生的几率。此时,咱们可使用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> <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> </project>
这里使用properties
元素定义Maven属性,在pom.xml中可使用美圆符号和大括弧环绕的方式引用Maven属性。
优化依赖
在软件开发的过程当中,咱们经过工具来简化咱们的工做,提高咱们的工做效率,而Maven就是这样的一个工具。对于工具,有的时候,咱们不能仅仅停留在使用的层面,而是可以掌控工具。
经过前面的总结,咱们知道Maven会自动解析全部项目的直接依赖和传递性依赖,而且根据规则正确判断每一个依赖的范围,对于一些依赖冲突,也能进行调节,以确保任何一个构件只有惟一的版本在依赖中存在。在这些工做以后,最后获得的那些依赖被称为“已解析依赖”。咱们能够经过如下命令查看当前项目的已解析依赖以及依赖范围:
mvn dependency:list
咱们还能够经过如下命令查看项目的依赖树,经过这棵依赖树就能很清楚地看到某个依赖是经过哪条传递路径引入的:
mvn dependency:tree
使用dependency:list
和dependency:tree
能够帮助咱们详细了解项目中全部依赖的具体信息,在此基础上,还有dependency:analyze
工具能够帮助分析当前项目的依赖。
mvn dependency:analyze
dependency:analyze
命令的输出结果中有两个重要的部分。首先是Used undeclared dependencies,指的是项目中使用到的,可是没有显式声明的依赖。这种依赖是经过直接依赖传递进来的,当升级直接依赖的时候,相关传递性依赖的版本也可能发生变化,这种变化不易察觉,可是有可能致使当前项目出错;所以,显式声明任何项目中直接用到的依赖。
另外一个重要的部分是Unused declared dependencies,指项目中未使用的,可是显式声明的依赖。须要注意的是,对于这样一类依赖,咱们不能简单的直接删除其声明,而是应该仔细分析。因为dependency:analyze
命令只会分析编译主代码和测试代码须要用到的依赖,一些执行测试盒运行时须要的依赖它就没法发现。固然了,有时候确实能经过该信息找到一些没用的依赖,可是必定要当心测试。
如今回头一看,Maven中依赖的知识点还真很多,虽然Maven中关于依赖的知识点不少,可是实际用到的,须要咱们重点关注的内容是不多的,本文中有些概念咱们只须要知道和理解就OK了。这篇文章很是的长,内容多,知识点也多,但愿你们仍是有点耐心,好好的阅读一下,争取一次就把Maven的依赖概念理解到位,一次学会,终生受益。但愿你能喜欢这篇文章。
果冻想,玩代码,玩技术!
2019年4月7日,于内蒙古呼和浩特。