Maven的一大功能是管理项目依赖。 为了能自动化地解析任何一个Java构件,Maven就必须将它们唯一标识,这就是依赖管理的底层基础----坐标。
1.1 Maven坐标
artifact为什么需要坐标?
我们在开发时,会到各个网站去下载依赖,但每个项目的网站风格迥异,大量的时间会花费在搜索,浏览网页等工作上。 由于没有统一的规范,统一的法则,这些工作无法自动化。
为了自动化处理,Maven定义了一组规则:
世界上任何一个构件都可以使用Maven坐标唯一标识,Maven坐标的元素包括:groupId, artifactId, version, packaging, classifier.
现在,只要我们提供正确的坐标元素,Maven就能找到对应的构件。
Maven内置了一个中央仓库的址:
https://repo.maven.apache.org/maven2
在我们开发自己项目的时候,也需要为其定义适当的坐标,这是Maven强制要求的。在这个基础上,其它Maven项目才能引用该项目生成的构件。
Maven中央仓库为每一个artifct定义了唯一的坐标,统一下载,避免了到各个网站下载的局面。
如下图:
1.2 坐标详解
Maven坐标为各种构件引入的秩序,任何一个构件都必须明确定义自己的坐标,而一组Maven坐标是通过一些元素定义的,它们是:groupId, artifactId, version, packaging, classifier.
主构件:
-----------------------------------------------------------------
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
-----------------------------------------------------
------------
附属构件:
-----------------------------------------------------------------
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
<classifier>javadoc</classifier>
-----------------------------------------------------------------
-----------------------------------------------------------------
<groupId>org.sonatype.nexus</groupId>
<artifactId>nexus-indexer</artifactId>
<version>2.0.0</version>
<packaging>jar</packaging>
<classifier>sources</classifier>
-----------------------------------------------------------------
Groupid: 定义当前Maven项目隶属的实际项目。表示方式与java包名类似,通常与域名反向一一对应。如:org.sonatype.nexus
artifactId: 该元素定义实际项目中的一个Maven项目(模块),推荐的做法是使用实际项目名称作为artifactId的前缀。
如:上例中的artifactId是nexus-indexer,使用了实际项目名nexus作为前缀,这样方便寻找实际构件,便于了解这个模块属性哪个groupId的。同时当多个groupId,但有相同的模块core,如果没有groupId前缀,则不知道这个core是属于哪个groupId,会很乱。
Version:该元素定义Maven项目当前所处的版本,如上例中nexus-indexer的版本是2.0.0。
packaging:该元素定义Maven项目的打包方式。如果没有定义该项,默认为jar
classifier:该元素用来帮助定义构建输出的一些附属构件。
附属构件与主构件对应,如:
Nexus-indexer-2.0.0-javedoc.jar 为主构件提供JAVA文档
Nexus-indexer-2.0.0-sources.jar 为主构件提供源代码
它是与主构件nexus-indexer-2.0.0.jar相关联的
使用添加一个classifier,表示附属构件,groupId + artifact + version +classifier唯一性构成自己的唯一坐标,同时也知道它是groupId + artifact + version为标识的主构件的附件属构件。
项目构件打包输出文件名构成:
artifactId-version [ -classifier].packagin
根据上例,它产生的文件名:
主构件:nexus-indexer-2.0.0.jar
附属构件:nexus-indexer-2.0.0-javadoc.jar
附属构件:nexus-indexer-2.0.0-sources.jar
1.3 account-email范例
1.3.1 account-email 的pom.xml
Let look at the coordinate:
groupId: com.juvenxu.mvnbook.account; it mean that it is an account project.
artifactId: account-email; using account as the prefix to different withother project. The email model belong to account project.
Version:1.0.0-SNAPSHOT;it mean that this model has been developing.
pom.xml
-------------------------------------------------------------------------------------------------------------
<projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-email</artifactId>
<name>Account Email</name>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>2.5.6</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.icegreen</groupId>
<artifactId>greenmail</artifactId>
<version>1.3.1b</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
-------------------------------------------------------------------------------------------------------------
POM.XML定义了该项目坐标:
---------------------------------------------------------------------------------
<groupId>com.juvenxu.mvnbook.account</groupId>
<artifactId>account-email</artifactId>
<name>AccountEmail</name>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
---------------------------------------------------------------------------------
groupId:com.juvenxu.mvnbook.account;
artifactId: account-email;
Version: 1.0.0-SNAPSHOT
由于该模块属于账户注册服务项目的一部分。所以,其groupId对应了account项目。
而当前模块的artifactId,以account作为前缀,以方便区分其它项目的构建。
1.0.0-SNAPSHOT,表示该版本处于开发中,还不稳定。
Dependencies元素:
POM使用dependencies定义项目依赖。
通过引用其它包的坐标,来指定依赖。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>2.5.6</version>
</dependency>
引用依赖后,MAVEN会自动根据依赖的坐标从中央仓库查找指定的artifact下载
The jar has been download automatically.
1.3.2 account-email的主代码
JAVA主代码:位于src/main/java
资源文件(非JAVA):位于src/main/resources
-------------------------------------------------------------------------------
packagecom.juvenxu.mvnbook.account.email;
public interfaceAccountEmailService {
voidsendMail(String to,String subject,String htmlText) throwsAccountEmailException;
}
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
packagecom.juvenxu.mvnbook.account.email;
importjavax.mail.MessagingException;
importjavax.mail.internet.MimeMessage;
importorg.springframework.mail.javamail.JavaMailSender;
importorg.springframework.mail.javamail.MimeMessageHelper;
public classAccountEmailServiceImpl
implements AccountEmailService
{
private JavaMailSender javaMailSender;
private String systemEmail;
public void sendMail( String to, Stringsubject, String htmlText )
throws AccountEmailException
{
try
{
MimeMessage msg =javaMailSender.createMimeMessage();
MimeMessageHelper msgHelper = newMimeMessageHelper( msg );
msgHelper.setFrom( systemEmail );
msgHelper.setTo( to );
msgHelper.setSubject( subject );
msgHelper.setText( htmlText, true);
javaMailSender.send( msg );
}
catch ( MessagingException e )
{
throw new AccountEmailException("Faild to send mail.", e );
}
}
public JavaMailSender getJavaMailSender()
{
return javaMailSender;
}
public void setJavaMailSender(JavaMailSender javaMailSender )
{
this.javaMailSender = javaMailSender;
}
public String getSystemEmail()
{
return systemEmail;
}
public void setSystemEmail( StringsystemEmail )
{
this.systemEmail = systemEmail;
}
}
-------------------------------------------------------------------------------
-------------------------------------------------------------------------------
packagecom.juvenxu.mvnbook.account.email;
public classAccountEmailException
extends Exception
{
private static final long serialVersionUID= -4817386460334501672L;
public AccountEmailException( Stringmessage )
{
super( message );
}
public AccountEmailException( Stringmessage, Throwable throwable )
{
super( message, throwable );
}
}
-------------------------------------------------------------------------------
5.3.3 account-email的测试代码
测试代码:位于src/test/java
资源文件:位于src/test/resources
-------------------------------------------------------------------------------
packagecom.juvenxu.mvnbook.account.email;
import staticjunit.framework.Assert.assertEquals;
importjavax.mail.Message;
importorg.junit.After;
importorg.junit.Before;
importorg.junit.Test;
importorg.springframework.context.ApplicationContext;
importorg.springframework.context.support.ClassPathXmlApplicationContext;
importcom.icegreen.greenmail.util.GreenMail;
importcom.icegreen.greenmail.util.GreenMailUtil;
importcom.icegreen.greenmail.util.ServerSetup;
public classAccountEmailServiceTest
{
private GreenMail greenMail;
@Before
public void startMailServer()
throws Exception
{
greenMail = new GreenMail(ServerSetup.SMTP );
greenMail.setUser("test@juvenxu.com", "123456" );
greenMail.start();
}
@Test
public void testSendMail()
throws Exception
{
ApplicationContext ctx = newClassPathXmlApplicationContext( "account-email.xml" );
AccountEmailService accountEmailService= (AccountEmailService) ctx.getBean( "accountEmailService" );
String subject = "TestSubject";
String htmlText ="<h3>Test</h3>";
accountEmailService.sendMail("test2@juvenxu.com", subject, htmlText );
greenMail.waitForIncomingEmail( 2000, 1);
Message[] msgs =greenMail.getReceivedMessages();
assertEquals( 1, msgs.length );
assertEquals( subject,msgs[0].getSubject() );
assertEquals( htmlText,GreenMailUtil.getBody( msgs[0] ).trim() );
//output the result
System.out.println(msgs.length);
System.out.println(msgs[0].getSubject());
System.out.println(GreenMailUtil.getBody( msgs[0] ).trim());
}
@After
public void stopMailServer()
throws Exception
{
greenMail.stop();
}
}
------------------------------------------------------
-------------------------
1.4 依赖的配置
一个依赖声明可以包含如下的一些元素:
<project>
…
<dependencies>
<dependency>
<groupId>…</groupId>
<artifactId>…</artifactId>
<version>..</version>
<type>..</type>
<scope>..</scope>
<optional>..</optional>
<exclusions>
<exclusion>
…
</exclusion>
</dependency>
…
</dependencies>
</project>
元素说明:
groupId,artifactId和version:依赖的基本坐标,Maven需要根据坐标才能找到需要的依赖。才能下载需要artifact.
type:依赖的类型,它与坐标定义packaging元素对应。在没有声明时,默认值为jar.
scope:依赖的范围,不配置,默认就是compile
optional:标记依赖是否可选。
exclusions:用来排除传递性依赖。
注:大部分依赖声明只包含基本坐标即可。
5.5 依赖范围
开发中的问题:在开发过程中,我们会有编译,运行,测试这些阶段,在每一个阶段需要的JAR包可能会不同,如测试时,我们需要有Junit包,但运行时,却是不需要的。
怎么解决这个问题?
Maven的方案:
Maven在编译,测试,运行时,会使用不同的一套classpath。当每一个阶段可以分离使用不同的 jar包。不同的classpath由“依赖范围”scope元素 来区分。当不同的scope值时,这个依赖就会在不同阶段上有效。
Maven的依赖范围:
compile:编译依赖范围。没有指定,则默认该依赖范围。compile表示,该依赖在编译,测试,运行三种classpath都有效。
test:测试依赖范围。它只对测试classpath有效。编译主代码或运行项目时无法使用此类依赖。只有编译测试代码和运行测试代码时有效。如:Junit.
provided:已提供依赖范围。对编译和测试classpath有效,但在运行时无效。如:servlet-api,编译和测试项目时需要该依赖,但在运行项目时,由于容器已提供,故不需要Maven重复引入。
runtime:运行时依赖范围。对 测试和运行classpath有效。编译主代码时无效。如:JDBC驱动,
system:系统依赖范围。此类依赖不是通过Maven仓库解析,而是与本机系统绑定,可能构成构建的不可移值,慎用。
import:导入依赖范围。
1.6 传递性依赖
开发中的问题:
如果有一个项目,需要基于SpringFramework,我们需要手工去下载相关的依赖。但Spring Framework又会依赖其它开源类库。
问题是我们也不知道它还依赖哪些库?
两个方法:
A:下载Spring Framework包及它的所有的依赖包,这种做到往往会引入很多不必要的依赖,造成项目臃肿。
B:只下载Spring Framewok包,如:spring-framework-2.5.6.zip,不下载它的依赖包,然后在实际上,根据出错的信息,或查相关文档,加入需要的其它依赖。这种方法会占用我们很长的时间,显示很麻烦。
Maven的方案:传递性依赖。
原理是,Maven通过访问当前依赖的pom,查pom.xml里又有哪些依赖,然后再找对应的pom.xml,直到找到所有的依赖为止,然后把这些依赖都下载下来
这样解决依赖的问题,就显得非常的简单。
如:account-email项目引用了spring-core-2.5.6依赖,maven会找到它的pom文件http://repo1maven.org/maven2/org/springframework/spring-core/2.5.6/spring-core-2.5.6.pom。
发现它包含commons-logging的依赖,则就会自动下载commons-logging这个artifact.
这样,有了传递性依赖机制,在使用SpringFramework时,不需要去考虑它依赖了什么,也不用担心引入多余的依赖。Maven会解析各个直接依赖的POM,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。
1.6.2 传递性依赖和依赖范围的关系
依赖范围不仅控制依赖与三种classpath的关系,还会对传递性依赖产生影响。
设:
A依赖B,B依赖C:A-->B-->C
则:
第一直接依赖:A-->B (A依赖B)
第二直接依赖:B-->C(B依赖C)
传递性依赖:A-->C(A依赖C)
如何决定传递性依赖的范围(SCOPE):
第一直接依赖的范围和第二直接依赖的范围决定了传递依赖的范围。
最左列:表示第一直接依赖范围(SCOPE)
最上行:表示第二直接依赖范围(SCOPE)
中间交叉单元格:表示传递性依递范围(SCOPE)
如:A-->B-->C
第一直接依赖范围:A项目pom.xml中定义了一个依赖B,它的scope是test
第二直接依赖范围:B项目pom.xml中定义了一个依赖C,它的SCOPE是compile
传递依赖的范围:按照表5-2,则传递依赖的范围是test,也就是说相当于在A项目的pom.xml中定义了一个依赖C,它的SCOPE为test。
1.7 依赖调解
依赖的问题1:如有以下的依赖关系
A-->B-->C-->X(V1.0),A-->D-->X(V2.0).
X是A的传递性依赖
问题是:两个版本都被解析是不对的,这样会造成依赖重复,哪个版本的X会被maven解析使用呢?
Maven的方案:路径最近者优先。
这样,选用会是X(V2.0),因为A-->D-->X(V2.0).路径比较短。
1.8 可选依赖
依赖问题2:
如有以下的依赖关系,它们的SCOPE都是compile
A-->B,B-->X,B-->Y.
但是,X与Y是互斥的,即使用了X就不能使用Y,反之亦然。
如:B是一个持久层隔离工具包,它支持多种数据库,MYSQL,PostgreSQL等。建造这个工具包时,需要这两种数据库的驱动程序,但在使用这个工具包时,只会依赖一种数据库。
即:A的传递依赖,要么是X,要么是Y,这个要怎么处理呢??
Maven的方案:可选依赖,optional元素
如上例,在项目B中添加X与Y的依赖时,添加一个optional元素,值设为true,让其生效。
这样的结果是,X与Y只会对当前项目B有效,其它依赖于B的项目A,则不会把X与Y传递给A。 当A依赖于B,需要使用X或Y其中一种依赖时,需要在A中显式的声明X或Y这个依赖才可以。
在B中将mysql与postgresql都添加optional元素,并设为true
在A中显示的声明mysql依赖
注:在理想情况下,是不应该使用可选依赖的,它违反单一职责性原则。
1.9 最佳实践
总结前人的经验,归纳了一些在实际应用中Maven依赖常见的技巧,方便用来避免和处理很多常见的问题。
1.9.1 排除依赖
传递性依赖会给项目隐式地引入很多依赖,这样极大的简化了项目依赖的管理,但这种特性也会带来一些问题。
问题1:项目有一个第三方依赖A,但A由于某些原因依赖了一个SNAPSHOT版本的包B,根据传递性依赖的规由,B会作为当前项目的传递性依赖,从而影响当前项目的不稳定性。
如何处理?
Maven的方案:使用排除依赖
在引入依赖时,声明exclusion元素。在显式的声明需要版本。
如:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>project-a</artifactId>
<version>1.0.0</version>
<dependencies>
<dependency>
<groupId>com.juvenxu.mvnbook</groupd>
<artifactId>project-b</artifactId>
<version>1.0.0</version>
<exclusions>
<exclusion>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-c</artifactId>
</exclusion>
</exclusions>
</dependecy>
<dependency>
<groupId>com.juvenxu.mvnbook</groupId>
<artifactId>project-c</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
</project>
上面A依赖于B,但不引用传递性依赖C,使用exclusions元素声明排除依赖,exclusions可以包含一个或多个exclusion元素。
然后又显式地声明对于项目C1.1.0版本的依赖。
1.9.2 归类依赖
很多时间,我们引用的依赖,它们都是来自同一项目的不同模块。因此,所有这些依赖的版本都是相同的。如: spring-beans:2.5.6 , spring-support:2.5.6
问题1:对于这种依赖引用,升级时也是全部一起升级,有没有什么简便的方法来避免手工输入错误及提高版本更新的效率?
Maven方案:引用属性变量的概念。通过在<properties>定义变量,然后在POM.XML的其它地方可以使用该变量值。
--------------------------------------------------------------------
<properties>
<springframework.version>2.5.6</springframework.version>
<junit.version>4.7</junit.version>
</properties>
<dependencyManagement>
<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>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
--------------------------------------------------------------------
1.9.3 优化依赖
问题1:随着项目不断的重构,优化。有一些曾经引的依赖可能不再使用。而有一些则是必须的依赖。如何有效的管理日益庞大的依赖?
Maven方案:使用依赖管理插件
已解析依赖:指Maven自动解析所有项目的直接依赖和传递性依赖,并且根据规则正确判断每个依赖的范围,对于一些依赖冲突,自动调节,以确保任何一个构件只有唯一的版本在依赖中存在。经过一系列处理后得到的最终依赖为“已解依赖”(Resolved Dependency)
依赖管理相关指令:
mvn dependency:list:查看当前项目的已解析依赖。
mvn dependency:tree:查看当前项目的依赖树。
mvn dependency:analyze:分析当前项目的依赖树。
Usedundeclared dependencies:
是指那些在项目中直接使用到的,但没有在POM中配置的依赖。例如该例中可能项目中的一些类有关于spring-context的Javaimport声明,但spring-context这个依赖实际是通过传递性依赖进入classpath的,这就意味者潜在的风险。一般来说我们对直接依赖的版本变化会比较清楚,因为那是我们自己直接配置的,但对于传递性依赖的版本变化,就会比较模糊,当这种变化造成构建失败的时候,就很难找到原因。因此我们应当增加这些Used undeclared dependencies 。
显式声明:当前项目中直接用到的依赖。
Unused declareddependencies:意指项目中未使用的,但显式声明的依赖。
表示那些我们配置了,但并未直接使用的依赖。需要注意的时,对于这些依赖,我们不该直接简单地删除。由于dependency:analyze只分析编译主代码和测试代码使用的依赖,一些执行测试和运行时的依赖它发现不了,因此还需要人工分析。通常情况,Unuseddeclared dependencies 还是能帮助我们发现一些无用的依赖配置。
最后,还一些重要的POM内容通常被大多数项目所忽略,这些内容不会影响项目的构建,但能方便信息的沟通,它们包括项目URL,开发者信息,SCM信息,持续集成服务器信息等等,这些信息对于开源项目来说尤其重要。对于那些想了解项目的人来说,这些信息能他们帮助找到想要的信息,基于这些信息生成的Maven站点也更有价值。相关的POM配置很简单,如:
<project>
<description>...</description>
<url>...</url>
<licenses>...</licenses>
<organization>...</organization>
<developers>...</developers>
<issueManagement>...</issueManagement>
<ciManagement>...</ciManagement>
<mailingLists>...</mailingLists>
<scm>...</scm>
</project>