一小时教你学会 Maven 项目的构建与管理(1)

/ maven / 没有评论 / 1504浏览

Maven翻译成中文是“专家、内行”。Maven是Apache组织中一个颇为成功的开源项目,Maven主要服务于基于Java平台的项目构建、依赖管理和项目信息管理的优秀工具。

-----------------来自小马哥的故事

xxx

Maven是优秀的构建工具:自动化构建过程、跨平台、标准化构建过程。

Maven为Java开发者提供了一个免费的中央仓库,其中几乎可以找到任何流行的开源类库,通过Maven的衍生工具Nexus,可以进行快速的搜索。Maven项目目录结构有约定的规则,约定优于配置(Convention Over Configuration)。

Ant(Another Neat Tool)另一个整洁的工具,Tomcat构建,过程式,开发者需要显式的指定每一个目标以及完成该目标所需要执行的任务,每一个项目都需要重新编写这一过程。

Maven是声明式的,项目构建过程和过程各阶段所需工作都要插件实现,大部分插件都是现成的,开发者只需要声明项目的基本元素,Maven就可以执行内置的,完整的构建过程。

Maven 基础环境配置与基本命令

Maven的安装与配置分析

Maven环境的安装

安装环境:Jdk1.7、Maven-3.5.0

第一步:JDK安装与配置。Windows和Linux下安装步骤可自行查找。

第二步:Maven的下载。官方下载地址:http://maven.apache.org/download.cgi

当前最新版本3.5.0,Windows上安装下载apache-maven-3.5.0-bin.zip

Linux上安装下载 apache-maven-3.5.0-bin.tar.gz

Windows上安装Maven步骤:

1.解压apache-maven-3.5.0-bin.zip到D:\develop\apache-maven-3.5.0

2.配置环境变量

    M2_HOME=D:\develop\apache-maven-3.5.0

    Path末尾添加;% M2_HOME%\bin

3.测试安装是否正确

在命令行执行mvn –v,可以maven版本信息和基本配置信息表示配置成功。

Linux上安装Maven步骤:

安装目录分析

Maven安装目录的图片

说明

bin:该目录下mvn、mvnDebug是基于Linux平台的shell脚本,mvn.bat、mvnDebug.bat是基于Windows平台的bat脚本,在命令行输入mvn命令时实际上是调用mvn或mvn.bat脚本。mvn和mvnDebug的区别是mvnDebug多了一条MAVENDEBUGOPTS配置,作用是运行Maven时开启debug模式以调试Maven本身。m2.conf文件是classworlds的配置文件,Maven启动时会自动加载。

boot: 该目录只包含一个文件plexus-classworlds-2.5.2.jar,plexus-classworlds是一个类加载器框架,相对于默认的java类加载器,它提供了更加丰富的语法以便配置,Maven使用该框架加载自己的类库。

conf: 该目录包含了Maven的配置文件settings.xml,可以指定2种级别:全局级别:直接修改${maven.conf}/settings.xml文件可以全局定制Maven的行为,对一台机器上的所有用户有效。用户级别:将该文件复制到${user.home}/.m2/目录下,然后修改settings.xml配置,在当前用户范围内定制Maven的行为。

lib: 该目录包含了所有Maven运行时需要的Java类库,Maven本身是分模块的maven-*.jar都是maven自己的包,还有很多第三方依赖包。

LICENSE: Maven使用的软件许可证是Apache LicenseVersion 2.0。

NOTICE: Apache Maven Distribution使用的第三方软件。

README.txt: Maven的简明介绍,包括系统要求、安装说明、Maven URLS等。

Maven的基本命令

Maven项目构建过程中,主要构建命令有几种:

执行后面的命令会自动执行前面的命令,比如执行mvn package时会执行validate、clean、compile、test、package五个阶段。

mvn package的五个阶段的图片

Maven 核心概念理论

Maven概念模型与依赖解析机制

Maven根据项目的pom.xml文件,把它转化成项目对象模型(POM),这时要解析依赖关系,然后去相对应的maven库中查找所依赖的jar包。在clean,compile,test,package等生命周期阶段都有相应的Plug-in来做这些事情,而这些Plug-in会产生一些中间产物。

Maven根据项目的pom.xml文件,把它转化成项目对象模型(POM),这时要解析依赖关系,然后去相对应的maven库中查找所依赖的jar包。在clean,compile,test,package等生命周期阶段都有相应的Plug-in来做这些事情,而这些Plug-in会产生一些中间产物。

Maven核心概念

Maven从仓库解析依赖的机制

当本地仓库没有依赖构件的时候,Maven会自动从远程仓库下载;当依赖版本为快照版本时,Maven会自动找到最新的快照。

1.当依赖范围scope=system时,Maven直接从本地文件系统解析构件;

2.根据依赖坐标计算仓库路径后,尝试直接从本地仓库寻找构件,若发现构件则解析成功;

3.在本地仓库不存在相应构件的情况下,若依赖版本是显式的发布版本构件时,如1.1.0、1.2-alpha-1等,则便利所有的远程仓库,发现后下载到本地仓库并解析使用;

4.如果依赖的版本是RELEASE或者LASTEST,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/maven-metadata.xml,将其与本地仓库的对应元数据合并后,计算出RELEASE或者LASTEST的真实值,然后基于真实值检查本地和远程仓库;

5.如果依赖版本是SNAPSHOT,则基于更新策略读取所有远程仓库的元数据groupId/artifactId/version/maven-metadata.xml,将其与本地仓库的对应元数据合并后,得到最新快照版本的值,然后基于该值检查本地或者从远程仓库下载;

6.如果最后解析到的构件版本是时间戳格式的快照,如1.0-20170712.191220-2,则复制其时间戳格式的文件至非时间戳格式,如SNAPSHOT,并使用该非时间戳格式的构件。

当依赖的版本不明晰的时候,如RELEASE、LASTEST、SNAPSHOT,Maven就需要基于更新远程仓库的更新策略来检查更新。

Maven仓库

构件:在Maven的世界,任何一个依赖、插件或者项目构建的输出,即xxx.jar;任何一个构件都有一组坐标唯一标识。

仓库:得益于坐标机制,任何Maven项目使用任何一个构件的方式都是完全相同的,在此基础上,Maven可以在某个位置统一存储所有Maven项目共享的构件,这个统一的位置就是仓库。


public class DefaultRepositoryLayout
    implements ArtifactRepositoryLayout
{
    private static final char PATH_SEPARATOR = '/';
    private static final char GROUP_SEPARATOR = '.';
    private static final char ARTIFACT_SEPARATOR = '-';
    public String getId()
    {
        return "default";
    }

    public String pathOf( Artifact artifact )
    {
        ArtifactHandler artifactHandler = artifact.getArtifactHandler();

        StringBuilder path = new StringBuilder( 128 );

        path.append( formatAsDirectory( artifact.getGroupId() ) ).append( PATH_SEPARATOR );
        path.append( artifact.getArtifactId() ).append( PATH_SEPARATOR );
        path.append( artifact.getBaseVersion() ).append( PATH_SEPARATOR );
        path.append( artifact.getArtifactId() ).append( ARTIFACT_SEPARATOR ).append( artifact.getVersion() );

        if ( artifact.hasClassifier() )
        {
            path.append( ARTIFACT_SEPARATOR ).append( artifact.getClassifier() );
        }

        if ( artifactHandler.getExtension() != null && artifactHandler.getExtension().length() > 0 )
        {
            path.append( GROUP_SEPARATOR ).append( artifactHandler.getExtension() );
        }

        return path.toString();
    }

    private String formatAsDirectory( String directory )
    {
        return directory.replace( GROUP_SEPARATOR, PATH_SEPARATOR );
    }
}

例如:groupId=com.feiyue、artifactId=demo、version=1.0、artifactId=jdk7、packaging=jar 其对应的路径生成如下:

1.groupId路径:formatAsDirectory()将groupId中的'.'转换成'/',com.feiyue就会转换成com/feiyue,之后再加一个'/',就变成com/feiyue/

2.artifactId路径:在groupId基础的加上artifactId,再加上一个'/,变成com/feiyue/demo/

3.version路径:在前面基础上加上version,再加上一个'/,变成com/feiyue/demo/1.0/

4.依次加上artifactId、一个'-’、version,就变成com/feiyue/demo/1.0/demo-1.0

5.如果有classfier,4)会变成com/feiyue/demo/1.0/demo-1.0-jdk7

6.如果extension存在则依次加上'.’、extension。代码中extension是从artifactHandler而非artifact中获取,artifactHandler是由packaging决定的。故packaging决定了构件的扩展名,因此最终的路径为com/feiyue/demo/1.0/demo-1.0-jdk7.jar

Maven仓库的分类

Maven仓库分为两类:本地仓库和远程仓库。当Maven根据坐标寻找构件时,首先会查看本地仓库,若本地仓库存在此构件则直接使用;若本地仓库不存在此构件,Maven就会去远程仓库查找,查找到下载到本地仓库再使用。若本地仓库和远程仓库都没有需要的构件,Maven就会报错。

中央仓库: Maven核心自带的远程仓库,包含了绝大部分开源构件,默认情况,当本地仓库没有Maven需要构件时,就从中央仓库下载。

私服:一种特殊的远程仓库,为节省带宽和时间,应在局域网内架设一个私有仓库服务器,用其代理所有外部的远程仓库。

本地仓库:用户自定义本地仓库的地址,需编辑${user.home}/.m2/setting.xml文件,设置localRepository节点的值为仓库地址即可,默认情况下${user.home}/.m2/setting.xml是不存在的,需要用户从安装目录复制${M2_HOME}/conf/setting.xml文件在进行编辑。

<settings>
<localRepository>E:/maven/repository</localRepository>
</settings>

中央仓库: Maven默认的远程仓库,安装文件中自带了中央仓库的配置,在${M2_HOME}/lib/maven-model-builder-3.2.5.jar中,解压缩找到org\apache\maven\model\pom-4.0.0.xml,可以看到如下默认远程仓库配置:

<repositories>
  <repository>
   <id>central</id>
   <name>Central Repository</name>
   <url>https://repo.maven.apache.org/maven2</url>
   <layout>default</layout>
   <snapshots>
       <enabled>false</enabled>
   </snapshots>
  </repository>
</repositories>

这个配置文件是所有Maven项目都会继承的超级POM.

私服:特殊的远程仓库,架设在局域网内的仓库服务,代理公网的远程仓库,当Maven需要下载构件时,从私服请求,若私服不存在该构件,则从公网远程仓库下载,缓存到私服之后,再为Maven的下载请求提供服务。另外无法从公网仓库下载的构件也能从本地上传到私服供项目使用。

私服优点:节省外网带宽、提供Maven构件速度、部署第三方构件、提供Maven构件稳定性、降低中央仓库负荷。

Maven坐标

唯一标识Maven构件,坐标元素分为groupId、artifactId、version、packaging、classifier.

groupId:必选,定义当前Maven项目隶属的实际项目,不一定是一对一的关系,通常一个实际项目会被划分成很多模块。groupId一般不应该只定义到公司级别,一个公司可能会有很多实际项目,如果groupId只定义到组织级别,那么artifactId只能定义Maven项目。命名方式和Java包名类似,域名反向一一对应。例如:org.springframework.

artifactId:必选,定义实际项目中的一个Maven模块,推荐使用实际项目名称-模块名称,这样便于找到某个项目的一组构件。例如:spring-core,spring-beans,spring-web等。

version:必选,定义Maven项目当前所处的版本。例如:4.3.9.RELEASE、1.0-SNAPSHOT、RELEASE、LATEST、2.1等。

packaging:可选默认是jar,定义Maven项目的打包方式。打包方式有jar、war、pom等。

classifier:不能直接定义,帮助定义构建输出的一些附属构件。附属构件与主构件对应,例如-javadoc.jar、-sources.jar附属构件包含了java文档和源代码。

依赖管理

依赖管理分为传递性依赖、依赖调解、可选依赖、排除依赖、归类依赖等。

传递性依赖

Maven传递性依赖的图片

maven模块 -> spring-jdbc -> spring-core -> commons-logging

假设: A -> B -> C,即A对B是第一直接依赖,B对C是第二直接依赖,A对C是传递性依赖,第一直接依赖(简称F)和第二直接依赖(简称S)的范围决定了传递性依赖(简称T)的范围。如图所示:

Maven传递性依赖2

例如:A -> B -> C,A依赖B的范围是test,B依赖C的范围是compile,则A传递依赖C的范围是test。

结论:当S=compile时,T与F的范围一致;当S=test时,依赖不会传递;当S=provided时,只有当F=provided时,T=provided;当S=runtime时,T=F,但F=compile例外,此时T=runtime.

依赖调解

依赖调解第一原则:路径最近者优先。

例如:A -> B -> C -> X1 长度为3 A -> D -> X2 长度为2,因此X2会被解析使用 依赖调解第二原则:第一原则优先,依赖路径相等时,POM中依赖声明顺序靠前的优先。

例如:A -> B -> X1 长度为2 A -> C -> X2 长度为2,但是POM文件中B的依赖声明靠前,因此X1会被解析使用。

可选依赖

A依赖于B,B依赖于X和Y,B对于X和Y的依赖都是可选依赖,即optional=true

AB、BX(可选)、BY(可选)。可选依赖不会传递,即X、Y对A没有影响。

可选依赖一般是多种互斥的特性,具体使用时只选其一。

可选依赖的图片

排除依赖

使用exclusions元素声明排除依赖,exclusions包含一个或者多个exclusion子元素,因此可以排除一个或者多个传递性依赖。注意声明exclusion时只需要groupId和artifactId,而不需要version元素,因为只需要groupId和artifactId就可以唯一定位依赖图中的某个依赖。

排除依赖

归类依赖

spring的依赖包版本都是相同的,可以使用properties元素定义Maven属性spring.version=4.x

在定义依赖时可以使用美元符号加大括弧环绕的方式来引用Maven属性,例如${spring.version}。

聚合与继承

聚合:多个项目或者模块聚合到一起,建立一个package方式为pom的项目parent专门负责聚合工作,并使用modules-module指定子模块,目的是快速构建项目。

继承:多个模块聚合时,子模块需要继承父模块以消除重复配置。

聚合与继承的共同点是聚合POM与继承关系中的父POM的packaging都必须是pom。

聚合关系与继承关系的比较如下图所示:

聚合与继承