Maven 介绍

Maven 是功能强大的构建工具能够帮我们自动化构建过程,从清理、编译、测试到生成报告,再到打包和部署。我们只需要输入简单的命令(如 mvn clean install),Maven 就会帮我们处理繁琐的任务;它最大化的消除了构建的重复,抽象了构建生命周期,并且为绝大部分的构建任务提供了已实现的插件。比如说测试,我们只需要遵循 Maven 的约定编写好测试用例,当我们运行构建的时候,这些测试便会自动运行。除此之外,Maven 能帮助我们标准化构建过程。在 Maven 之前,十个项目可能有十种构建方式,但通过 Maven,所有项目的构建命令都是简单一致的。有利于促进项目团队的标准化。

Maven 中的依赖管理

依赖管理是 Maven 的一大特征,对于一个简单的项目,对依赖的管理并不是什么困难的事,但是如果这个项目依赖的库文件达到几十个甚至于上百个的时候就不是一个简单的问题了。在这个时候 Maven 对于依赖管理的作用就显露出来了。下面主要讨论几个方面的内容:传递性依赖,依赖范围,依赖管理,系统依赖,可选依赖

Maven 使用 dependencyManagement 元素来提供了一种管理依赖版本号的方式。通常会在一个组织或者项目的最顶层的父 POM 中看到 dependencyManagement 元素。使用 pom.xml 中的 dependencyManagement 元素能让所有在子项目中引用一个依赖而不用显式的列出版本号。Maven 会沿着父子层次向上走,直到找到一个拥有 dependencyManagement 元素的项目,然后它就会使用在这个 dependencyManagement 元素中指定的版本号。例如在父项目里:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.2</version>
    </dependency>
    ...
  <dependencies>
</dependencyManagement>Copy to clipboardErrorCopied

然后在子项目里就可以添加 mysql-connector 时可以不指定版本号,例如:

<dependencies>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
  </dependency>
</dependencies>Copy to clipboardErrorCopied

同时在 dependenceManagement 种,也可以从外部导入 POM 文件中的依赖项:

<dependencyManagement>
     <dependencies>
        <dependency>
            <!-- Import dependency management from Spring Boot -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>1.3.0.RC1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>Copy to clipboardErrorCopied

Dependency Scope

Maven 有三套 classpath(编译 classpath,运行 classpath,测试 classpath)分别对应构建的三个阶段。依赖范围就是控制依赖与这三套 classpath 的关系。依赖范围有六种。在 POM 4 中,中还引入了,它主要管理依赖的部署。目前 `` 可以使用 5 个值:

  • compile,缺省值,适用于所有阶段,会随着项目一起发布。compile 是默认的范围;如果没有提供一个范围,那该依赖的范围就是编译范围。编译范围依赖在所有的 classpath 中可用,同时它们也会被打包。
  • provided,provided 依赖只有在当 JDK 或者一个容器已提供该依赖之后才使用。例如,如果你开发了一个 Web 应用,你可能在编译 classpath 中需要可用的 ServletAPI 来编译一个 servlet,但是你不会想要在打包好的 WAR 中包含这个 ServletAPI;这个 Servlet API JAR 由你的应用服务器或者 servlet 容器提供。已提供范围的依赖在编译 classpath(不是运行时)可用。它们不是传递性的,也不会被打包。
  • runtime,runtime 依赖在运行和测试系统的时候需要,但在编译的时候不需要。比如,你可能在编译的时候只需要 JDBC API JAR,而只有在运行的时候才需要 JDBC 驱动实现。
  • test,只在测试时使用,用于编译和运行测试代码。不会随项目发布。
  • system,system 范围依赖与 provided 类似,但是你必须显式的提供一个对于本地系统中 JAR 文件的路径。这么做是为了允许基于本地对象编译,而这些对象是系统类库的一部分。这样的构件应该是一直可用的,Maven 也不会在仓库中去寻找它。如果你将一个依赖范围设置成系统范围,你必须同时提供一个 systemPath 元素。注意该范围是不推荐使用的(你应该一直尽量去从公共或定制的 Maven 仓库中引用依赖)。
<project>
  ...
  <dependencies>
    <dependency>
      <groupId>javax.sql</groupId>
      <artifactId>jdbc-stdext</artifactId>
      <version>2.0</version>
      <scope>system</scope>
      <systemPath>${java.home}/lib/rt.jar</systemPath>
    </dependency>
  </dependencies>
  ...
</project>Copy to clipboardErrorCopied

使用 system 依赖范围

在下文中会有对于依赖的 scope 的详细解释,这里只需要知道如果将 scope 设置为了 system 即是自动在本地路径中寻找依赖的 Jar 包即可。

<dependency>
  <groupId>dnsns</groupId>
  <artifactId>dnsns</artifactId>
  <version>1.0</version>
  <scope>system</scope>
  <systemPath>${project.basedir}/src/lib/dnsns.jar</systemPath>
</dependency>
<dependency>
  <groupId>localedata</groupId>
  <artifactId>localedata</artifactId>
  <version>1.0</version>
  <scope>system</scope>
  <systemPath>${project.basedir}/src/lib/localedata.jar</systemPath>
</dependency>
<dependency>
  <groupId>sunjce_provider</groupId>
  <artifactId>sunjce_provider</artifactId>
  <version>1.0</version>
  <scope>system</scope>
  <systemPath>${project.basedir}/src/lib/sunjce_provider.jar</systemPath>
</dependency>
<dependency>
  <groupId>sunpkcs11</groupId>
  <artifactId>sunpkcs11</artifactId>
  <version>1.0</version>
  <scope>system</scope>
  <systemPath>${project.basedir}/src/lib/sunpkcs11.jar</systemPath>
</dependency>Copy to clipboardErrorCopied

maven-install-plugin

maven-install-plugin 默认绑定在 maven 的生命周期 install 阶段。该插件有三个 goals:

  • install:install 即默认的 maven install 执行的命令,用来自动地将本项目的主 artifact 以及它的附件如 source,doc 安装到本地的仓库中。
  • install:install-file 作为 install 的补充,安装你指定的文件到本地仓库。
  • install:help 本插件的帮助信息。
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>**</version>

Dependency Mediation | 依赖调停

传递性依赖是在 Maven 中添加的新特征,这个特征的作用就是你不需要考虑你依赖的库文件所需要依赖的库文件,能够将依赖模块的依赖自动的引入。例如我们依赖于 Spring 的库文件,但是 Spring 本身也有依赖,如果没有传递性依赖那就需要我们了解 Spring 项目依赖,然后也要添加到我们的项目中。

由于没有限制依赖的数量,如果出现循环依赖的时候会出现问题,这个时候有两种方式处理,一种是通过 build-helper-maven-plugin 插件来规避,另一种就是重构两个相互依赖的项目。通过传递性依赖,项目的依赖结构能够很快生成。但是因为这个新的特性会有一些其他的特性被添加进来来限制由于传递性依赖所引入的包。

依赖调节:如果在一个项目里面出现不同的模块,依赖了一个项目的不同版本的时候判断依赖的版本。Maven 的时候仅仅支持最近原则也就是在依赖树中的最靠近项目的版本作为依赖版本。到了 Maven 的时候又提出了一个最先声明原则,也就是在项目中被最早声明的被判断为依赖的版本。

同一个 jar 包,又会有不同的版本,当你所依赖的 jar 有不同的版本的时候,比如你的 A 项目: A -> B -> C -> D 2.0 and A -> E -> D 1.0。这就出现了冲突,你当然也可以再 A 里面配置指定依赖 D2.0。如果你没在 A 的 pom 里面配置指定用哪一个。maven 会替你找一个近的,即 D1.0。

Excluded & Optional Dependencies | 排除依赖于可选依赖

可选依赖使用的情况是对于某个依赖来说系统只有在某个特定的情况下使用到它。例如数据库驱动,有 MySQL 的,Oracle 的。只有在我们使用到 MySQL 的时候才会被使用。

排包

再次回顾下 Maven 中的包依赖规则如下:

  • compile(默认使用):编译依赖范围,编译、测试、运行都生效;
  • test:测试依赖范围,只在测试的 classpath 有效,比如我们常用的 JUnit;
  • provided:只对编译和测试有效,对运行无效,常用于容器提供了的运行环境;
  • runtime:运行时依赖范围,比如 JDBC 驱动,编译和测试时不需要,只需要使用 JDK 提供的 JDBC 接口;
  • system:系统依赖范围,依赖 Maven 仓库以外的依赖。

仲裁规则

  • 第一原则:路径最短优先选中,将 pom 文件默认为一个树形结构,路径节点从根出发。
    • 假设:路径 1 是:A->B->C->D(version1.0),路径 2 是:A->B->D(version2.0)。由于路径 2 的长度小于路径 1,所以结果就是 A 会传递性依赖 D(version2.0),当路径长度相等的时候,使用第二原则
  • 第二原则:先声明者优先原则
    • 当路径长度相同时候,就要使用第二原则。假设:路径 1 不变依然是:A->B->C->D(version1.0),路径 2 是:A->B->E->D(version2.0),由于路径 1 在路径 2 前面声明,所以结果就是 A 会传递性依赖 D(version1.0)。
  • 第三原则:冲突 jar 句柄顺序随机原则
    • 如果系统自己已经在 target 目录中加载了有冲突的 jar 包,并且机器有一定概率可以启动成功,主要是当打包的时候包会打印到 target 目录中,编译机器和目标机器执行了打包上传解压,在这个过程中就会引起互相冲突的 jar 在目标机器上排序,系统启动的时候,加载包的顺序就是随机的。

项目排包

mvn dependency:tree>tree.log 直接执行这个命令,基本上只能先优先看一下总体的依赖状况,做整体评估,然后根据系统日志报错信息反查。

  • 启动时直接报错,java.lang.ClassNotFoundException ,或者方法不存在:这种比较简单,直接查找该类在哪个包中,相互冲突的基本上在两个 jar 包内就可以确定,基本手段:排除低版本,选择和自己 Java 代码一致使用的,排除和业务代码无关的。
  • 类路径长度统计,版本就高不就低。这点需要找到有疑似冲突的,做一个简单的统计,常常新版本的会解决之前的问题,但是也有一个潜在的风险,高版本的可能引入了更多有可能的冲突,因此,高版本的尽量遵循业务无关的类库,如果和业务有关,尽量把两个版本的依赖树都保存以做比对,或者和具体业务负责人确认改动点。
  • 隐含错误加上调试参数:mvn +X dependency:tree>tree.log,这样根据报错信息可以更精确的定位到错误或者冲突。

安装与配置

Maven 的安装也非常方便,可从 Apache 官方下载最新的 Maven 压缩包然后解压,也可以使用 SDK Man 执行安装;如果是手动配置的话我们还需要配置设置下系统的环境变量:

  • M2HOME: 指向 Maven 安装目录
  • Path: 追加 Maven 安装目录下的 bin 目录

在用户目录下,我们可以发现 .m2 文件夹。默认情况下,该文件夹下放置了 Maven 本地仓库 .m2/repository。所有的 Maven 构件(artifact)都被存储到该仓库中,以方便重用。默认情况下,~/.m2 目录下除了 repository 仓库之外就没有其他目录和文件了,不过大多数 Maven 用户需要复制 M2HOME/conf/settings.xml 文件到 ~/.m2/settings.xml。

部分常用的 Maven 命令如下:

# 查看maven版本
mvn -v
# 编译
mvn compile
# 测试
mvn test
# 打包
mvn package
# 删除 target
mvn clean
# 安装jar包到本地仓库中
mvn install
# 创建一个新工程
mvn archetype:generate -DgroupId=com.mycompany.app -DartifactId=my-app -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
Copy to clipboardErrorCopied

网络代理

众所周知的原因,国内有时候并不能够很顺畅的访问 Maven 的中央仓库,往往我们需要访问国内的镜像地址:

<mirror>
  <id>CN</id>
  <name>OSChina Central</name>
  <url>http://maven.oschina.net/content/groups/public/</url>
  <mirrorOf>central</mirrorOf>
</mirror>Copy to clipboardErrorCopied

或者编辑 ~/.m2/settings.xml 文件(如果没有该文件,则复制 $M2HOME/conf/settings.xml),添加代理配置如下:

<settings>
  ...
    <proxies>
        <proxy>
            <id>my-proxy</id>
            <active>true</active>
            <protocol>http</protocol>
            <host>代理服务器主机名</host>
            <port>端口号</port>
            <!--
                <username>***</username>
                <password>***</password>
                <nonProxyHosts>repository.mycom.com|*.google.com</nonProxyHosts>
            -->
        </proxy>
    </proxies>
  ...
</settings>Copy to clipboardErrorCopied

如果不行试试重启机器或者 eclipse 等 ide 还不行试试下面这种方式:windows-->preferences-->maven-->installations add

这样配置后将使用指定目录下的 maven,而非 eclipse 的 maven 内置插件。

生命周期

Maven 对构建(build)的过程进行了抽象和定义,这个过程被称为构建的生命周期(lifecycle)。生命周期(lifecycle)由多个阶段(phase)组成,每个阶段(phase)会挂接一到多个 goal。goal 是 maven 里定义任务的最小单元,相当于 ant 里的 target。

Maven 的生命周期就是对所有的构建过程进行抽象和统一。包含了项目的清理、初始化、编译、测试、打包、集成测试、验证、部署和站点生成等几乎所有的构建步骤。Maven 的生命周期是抽象的,即生命周期不做任何实际的工作,实际任务由插件完成,类似于设计模式中的模板方法。

核心概念

  • lifecycle:生命周期,这是 maven 最高级别的的控制单元,它是一系列的 phase 组成,也就是说,一个生命周期,就是一个大任务的总称,不管它里面分成多少个子任务,反正就是运行一个 lifecycle,就是交待了一个任务,运行完后,就得到了一个结果,中间的过程,是 phase 完成的,自己可以定义自己的 lifecycle,包含自己想要的 phase。
  • phase:可以理解为任务单元,lifecycle 是总任务,phase 就是总任务分出来的一个个子任务,但是这些子任务是被规格化的,它可以同时被多个 lifecycle 所包含,一个 lifecycle 可以包含任意个 phase,phase 的执行是按顺序的,一个 phase 可以绑定很多个 goal,至少为一个,没有 goal 的 phase 是没有意义的。
  • goal: 这是执行任务的最小单元,它可以绑定到任意个 phase 中,一个 phase 有一个或多个 goal,goal 也是按顺序执行的,一个 phase 被执行时,绑定到 phase 里的 goal 会按绑定的时间被顺序执行,不管 phase 己经绑定了多少个 goal,你自己定义的 goal 都可以继续绑到 phase 中。
  • mojo: lifecycle 与 phase 与 goal 都是概念上的东西,mojo 才是做具体事情的,可以简单理解 mojo 为 goal 的实现类,它继承于 AbstractMojo,有一个 execute 方法,goal 等的定义都是通过在 mojo 里定义一些注释的 anotation 来实现的,maven 会在打包时,自动根据这些 anotation 生成一些 XML 文件,放在 plugin 的 jar 包里。

抛开 mojo 不讲,lifecycle 与 phase 与 goal 就是级别的大小问题,引用必须是从高级引用下级(goal 绑定到 phase,也可理间为 phase 引用 goal,只是在具体绑定时,不会 phase 定义引用哪些 goal,但是执行是,却是 phase 调用绑定到它那的 goal),也不能跨级引用,如 lifecycle 可以引用任意的 phase,不同 lifecycle 可以同时引用相同的 phase,lifecycle 不能跨级引用 goal。goal 会绑定到任意的 phase 中,也就是说不同的 phase 可以同时引用相同的 goal,所以 goal 可以在一个 lifecycle 里被重复执行哦,goal 自然也不能说绑定到 lifecycle 中,它们三者的关系可以用公司里的 总领导,组领导,与职员的关系来解释。

lifecycle 的 phase 执行会指向之前所有的 phase,然后执行当前指定的 phase,一个 phase 会引用至少一个 goal。plugin 中的 goal 只是单单执行当前指定的 goal。执行 install:install 只会执行 installplugin 中的 goal:install,但是项目创建后还没有进行之前必要的步骤,比如 complie。这样直接执行 install:install 是肯定会出错的。

三套生命周期

Maven 有三套相互独立的生命周期,分别是 clean、default 和 site。每个生命周期包含一些阶段(phase),阶段是有顺序的,后面的阶段依赖于前面的阶段。

  • clean 生命周期:清理项目,包含三个 phase:
    • pre-clean:执行清理前需要完成的工作
    • clean:清理上一次构建生成的文件
    • post-clean:执行清理后需要完成的工作
  • default 生命周期:构建项目,重要的 phase 如下:
    • validate:验证工程是否正确,所有需要的资源是否可用。
    • compile:编译项目的源代码。
    • test:使用合适的单元测试框架来测试已编译的源代码。这些测试不需要已打包和布署。
    • package:把已编译的代码打包成可发布的格式,比如 jar。
    • integration-test:如有需要,将包处理和发布到一个能够进行集成测试的环境。
    • verify:运行所有检查,验证包是否有效且达到质量标准。
    • install:把包安装到 maven 本地仓库,可以被其他工程作为依赖来使用。
    • deploy:在集成或者发布环境下执行,将最终版本的包拷贝到远程的 repository,使得其他的开发者或者工程可以共享。
  • site 生命周期:建立和发布项目站点,phase 如下:
    • pre-site:生成项目站点之前需要完成的工作
    • site:生成项目站点文档
    • post-site:生成项目站点之后需要完成的工作
    • site-deploy:将项目站点发布到服务器

各个生命周期相互独立,一个生命周期的阶段前后依赖。举例如下:

  • mvn clean:调用 clean 生命周期的 clean 阶段,实际执行 pre-clean 和 clean 阶段
  • mvn test:调用 default 生命周期的 test 阶段,实际执行 test 以及之前所有阶段
  • mvn clean install:调用 clean 生命周期的 clean 阶段和 default 的 install 阶段,实际执行 pre-clean 和 clean,install 以及之前所有阶段

Mojo

以 phase 为目标构建

以 phase 为目标进行构建是最常见的,如我们平时经常执行的 mvn compile,mvn test,mvn package... 等等,compile,test,package 都是 maven 生命周期(lifecycle)里的 phase,通过 mvn 命令,你可以指定一次构建执行到那一个阶段,在执行过程中,所有经历的执行阶段(phase)上绑定的 goal 都将得到执行。例如,对于一个 jar 包应用,当执行 mvn package 命令时,maven 从 validate 阶段一个阶段一个阶段的执行,在执行到 compile 阶段时,compiler 插件的 compile goal 会被执行,因为这个 goal 是绑定在 compile 阶段(phase)上的。这一点可从其对应的 mojo 类上得知:

CompilerMojo

再比如经常使用的打包插件 shade,它的 goal 是绑定到 package 阶段的,这样,使用 mvn package 进行打包时都会执行 shade 的。

ShareMojo

以 goal 为目标构建

虽然以 phase 为目标的构建最常见,但是有时候我们会发现,一些插件的 goal 并不适合绑定到任何阶段(phase)上,或者是,这些 goal 往往是单独执行,不需要同某个阶段(phase)绑定在一起,比如 hibernate 插件的导入\导出 goal 多数情况下是根据需要要手动执行的(当然,也可以绑定到某个阶段上,比如进行单元测试时,可考虑将其绑定到 test 阶段上)。再比如 jetty(6.1.26)插件,它的 goal 都是将打包或未打包的工程部署到 Jetty 里然后启动 Jetty 容器的,多数情况下,人们都是独立运行这些 goal 的,比如:人们希望当键入 mvn jetty:run 后,工程就能完成编译后启动 jetty,而 Jetty 插件也确实是这样做的,它的 run goal 的 mojo 是这样声明的:

Jetty6RunMojo

其中@execute phase="test-compile"指明 jetty:run 这一 goal 会促使 maven 先 build 到 test-compile 阶段,再执行这个 goal.同样,对于 jetty:run-war 这个 goal 则要求先 build 到 package 阶段再执行该 goal。

Jetty6RunWar

而另外一个例子是 exec 插件的 exec:java:

ExecJavaMojo

这个 goal 也声明了 execute 的 phase,但却是 validate,这样,如果代码没有编译,执行这个 goal 就会出错,所以多数情况下,人们总是使用下面的方式执行的:mvn clean compile exec:java

Maven 项目结构

就像 Make 的 Makefile,Ant 的 build.xml 一样,Maven 项目的核心是 pom.xml。首先创建一个名为 hello-world 的文件夹,打开该文件夹,新建一个名为 pom.xml 的文件,输入其内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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.0
http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.wx.mvn</groupId>
  <artifactId>hello-world</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>Maven Hello World Project</name>
</project>Copy to clipboardErrorCopied

文件结构

  • 代码的第一行是 XML 头,指定了该 XML 文档的版本和编码方式。紧接着是 project 元素,project 是所有 pom.xml 的根元素,它还声明了一些 POM 相关的命名空间及 xsd 元素,虽然这些属性不是必须的,但使用这些属性能够让第三方工具(如 IDE 中的 XML 编辑器)帮助我们快速编辑 POM。
  • 根元素下的第一个子元素 modelVersion 指定了当前 POM 模型的版本,对于 Maven 2 及 Maven 3 来说,它只能是 4.0.0。这段代码中最重要的是 groupId,artifactId 和 version 三行。这三个元素定义了一个项目基本的坐标,在 Maven 的世界,任何的 jar、pom 或者 war 都是以基于这些基本的坐标进行区分的。
  • groupId 定义了项目属于哪个组,这个组往往和项目所在的组织或公司存在关联,譬如你在 googlecode 上建立了一个名为 myapp 的项目,那么 groupId 就应该是 com.googlecode.myapp,如果你的公司是 mycom,有一个项目为 myapp,那么 groupId 就应该是 com.mycom.myapp。
  • artifactId 定义了当前 Maven 项目在组中唯一的 ID,我们为这个 Hello World 项目定义 artifactId 为 hello-world,本书其他章节代码会被分配其他的 artifactId。而在前面的 groupId 为 com.googlecode.myapp 的例子中,你可能会为不同的子项目(模块)分配 artifactId,如:myapp-util、myapp-domain、myapp-web 等等。
  • version 指定了 Hello World 项目当前的版本——1.0-SNAPSHOT。SNAPSHOT 意为快照,说明该项目还处于开发中,是不稳定的版本。随着项目的发展,version 会不断更新,如升级为 1.0、1.1-SNAPSHOT、1.1、2.0 等等。
  • 最后一个 name 元素声明了一个对于用户更为友好的项目名称,虽然这不是必须的,但我还是推荐为每个 POM 声明 name,以方便信息交流。没有任何实际的 Java 代码,我们就能够定义一个 Maven 项目的 POM,这体现了 Maven 的一大优点,它能让项目对象模型最大程度地与实际代码相独立,我们可以称之为解耦,或者正交性,这在很大程度上避免了 Java 代码和 POM 代码的相互影响。比如当项目需要升级版本时,只需要修改 POM,而不需要更改 Java 代码;而在 POM 稳定之后,日常的 Java 代码开发工作基本不涉及 POM 的修改。

变量替换

在 pom.xml 定义 properties 标签

<properties>
 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 <spring.version>1.2.6</spring.version>
 <developer.organization><![CDATA[xy公司]]></developer.organization>
</properties>Copy to clipboardErrorCopied

以上内容就改成了

<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-core</artifactId>
 <version>${spring.version}</version>
</dependency>
<dependency>
 <groupId>org.springframework</groupId>
 <artifactId>spring-aop</artifactId>
 <version>${spring.version}</version>
</dependency>Copy to clipboardErrorCopied

也可以使用 maven-properties 插件来支持外部变量

目录结构

项目主代码和测试代码不同,项目的主代码会被打包到最终的构件中(比如 jar),而测试代码只在运行测试时用到,不会被打包。默认情况下,Maven 假设项目主代码位于 src/main/java 目录,我们遵循 Maven 的约定,创建该目录,然后在该目录下创建文件 com/wx/mvn/helloworld/HelloWorld.java,其内容如下:

package com.wx.mvn.helloworld;
public class HelloWorld {
  public String sayHello() {
    return "Hello Maven";
  }
  public static void main(String[] args) {
    System.out.print(new HelloWorld().sayHello());
  }
}Copy to clipboardErrorCopied

关于该 Java 代码有两点需要注意。首先,大部分情况下我们应该把项目主代码放到 src/main/java/ 目录下(遵循 Maven 的约定),而无须额外的配置,Maven 会自动搜寻该目录找到项目主代码。其次,该 Java 类的包名是 com.wx.mvn.helloworld,这与我们之前在 POM 中定义的 groupId 和 artifactId 相吻合。一般来说,项目中 Java 类的包都应该基于项目的 groupId 和 artifactId,这样更加清晰,更加符合逻辑,也方便搜索构件或者 Java 类。代码编写完毕后,我们使用 Maven 进行编译,在项目根目录下运行命令 mvn clean compile 即可。Maven 首先执行了 clean:clean 任务,删除 target/ 目录,默认情况下 Maven 构建的所有输出都在 target/ 目录中;接着执行 resources:resources 任务(未定义项目资源,暂且略过);最后执行 compiler:compile 任务,将项目主代码编译至 target/classes 目录(编译好的类为 com/wx/mvn/helloworld/HelloWorld.Class)。

仓库配置

Maven Repos

下面介绍一些 Maven 仓库工作的原理。典型的一个 Maven 依赖下会有这三个文件:

maven-metadata.xml
maven-metadata.xml.md5
maven-metadata.xml.sha1Copy to clipboardErrorCopied

maven-metadata.xml 里面记录了最后 deploy 的版本和时间。

<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
  <groupId>io.github.hengyunabc</groupId>
  <artifactId>mybatis-ehcache-spring</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <versioning>
    <snapshot>
      <timestamp>20150804.095005</timestamp>
      <buildNumber>1</buildNumber>
    </snapshot>
    <lastUpdated>20150804095005</lastUpdated>
  </versioning>
</metadata>Copy to clipboardErrorCopied

其中 md5, sha1 校验文件是用来保证这个 meta 文件的完整性。Maven 在编绎项目时,会先尝试请求 maven-metadata.xml,如果没有找到,则会直接尝试请求到 jar 文件,在下载 jar 文件时也会尝试下载 jar 的 md5, sha1 文件。Maven 的 repository 并没有优先级的配置,也不能单独为某些依赖配置 repository。所以如果项目配置了多个 repository,在首次编绎时,会依次尝试下载依赖。如果没有找到,尝试下一个,整个流程会很长。所以尽量多个依赖放同一个仓库,不要每个项目都有一个自己的仓库。如果想要使用本地 file 仓库里,在项目的 pom.xml 里配置,如:

<repositories>
  <repository>
    <id>hengyunabc-maven-repo</id>
    <url>file:/home/hengyunabc/code/maven-repo/repository/</url>
  </repository>
</repositories>