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定义了唯一的坐标,统一下载,避免了到各个网站下载的局面。

如下图:

 

druid maven坐标 maven 依赖坐标_druid maven坐标

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

 

druid maven坐标 maven 依赖坐标_maven_02

 

 

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.

 

druid maven坐标 maven 依赖坐标_java_03

 

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.

 

druid maven坐标 maven 依赖坐标_druid maven坐标_04

 

 

1.3.2 account-email的主代码

JAVA主代码:位于src/main/java

资源文件(非JAVA):位于src/main/resources

 

druid maven坐标 maven 依赖坐标_java_05

-------------------------------------------------------------------------------

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:导入依赖范围。

 

 

druid maven坐标 maven 依赖坐标_maven_06


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.

 

 

druid maven坐标 maven 依赖坐标_spring_07

这样,有了传递性依赖机制,在使用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):

第一直接依赖的范围和第二直接依赖的范围决定了传递依赖的范围。

druid maven坐标 maven 依赖坐标_java_08

最左列:表示第一直接依赖范围(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

 

druid maven坐标 maven 依赖坐标_java_09

在A中显示的声明mysql依赖

druid maven坐标 maven 依赖坐标_spring_10


druid maven坐标 maven 依赖坐标_maven_11

注:在理想情况下,是不应该使用可选依赖的,它违反单一职责性原则。

 

 

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版本的依赖。

 

druid maven坐标 maven 依赖坐标_jar_12

 

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>