Maven是Java开发常用的工具, 但是很多同学对里面的概念不是很清楚, 所以讲了一下.

本文基本内容基本是来自于maven官方文档, 基本相当于翻译一遍. 官方文档清晰好懂. RTFM, away from baidu.

Maven是什么

Maven是Java的项目管理工具:

  • Builds
  • Documentation
  • Reporting
  • Dependencies
  • SCMs
  • Releases
  • Distribution

可以看出有很多功能, 个人的理解就是从javac HelloWorld.java到开发复杂结构的项目, 中间就差一个Maven.

Maven里的一切都是围绕project发生的, 使用pom.xml描述关于project的各种信息.

由pom结构拆解maven功能

Project Object Model

官方referencexsd文件都是了解pom很好的途径. 可以将pom整体分为四个部分:

presentation notes:此时看官方文档, intro-pom.xml, 和xsd.

  • 基本信息. 各种ID和描述.
  • 项目组织结构/依赖. parent和modules, dependencies和dependencyManagement等等.
  • 构建配置. build配置, 生命周期和插件.
  • 外部环境. scm, distributionManagement等, 还有比较重要的repositories.

1. 基本信息

  <!-- 基本信息 -->
  <groupId>...</groupId>
  <artifactId>...</artifactId>
  <version>...</version>
  <packaging>...</packaging>
  <classifier>...</classifier>
  <!-- 描述信息 -->
  <name>...</name>
  <description>...</description>
  <url>...</url>
  <inceptionYear>...</inceptionYear>
  <licenses>...</licenses>
  <organization>...</organization>
  <developers>...</developers>
  <contributors>...</contributors>

groupId, artifactId, version等等, 作为项目的唯一ID, 完整的是 groupId:artifactId:packaging:classifier:version.

这里两个需要特别说明的是:

  • packaging, 项目的打包类型, jar/war/ear/pom/maven-plugin.
  • classifier, 额外的标识符, 比如jdk14, jdk15这种不同环境的区分, 或者javadoc, sources这种特殊的包(commons-io-2.6-javadoc.jar), 发布的时候可以指定发布的classifier, 下载依赖的时候可以指定所需的classifier.

name, developers, license, organization等等纯描述性信息.

2. 项目组织结构/依赖

Maven中能够处理的项目间关系包括三种:

  • 依赖(dependencies)
  • 继承(inheritance)
  • 组合(aggregation, multi-module)

依赖(dependencies)

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.0</version>
      <type>jar</type>
      <scope>test</scope>
      <optional>true</optional>
      <exclusions>...<exclusions>
    </dependency>
  </dependencies>

在pom的dependencies中配置的dependency, 即表示这个项目要依赖的项目, maven帮助我们管理这些依赖.

关于version的一个误解是maven只能指定固定的版本号, 实际:

1.0: "Soft" requirement on 1.0 (just a recommendation, if it matches all other ranges for the dependency)
[1.0]: "Hard" requirement on 1.0
(,1.0]: x <= 1.0
[1.2,1.3]: 1.2 <= x <= 1.3
[1.0,2.0): 1.0 <= x < 2.0
[1.5,): x >= 1.5
(,1.0],[1.2,): x <= 1.0 or x >= 1.2; multiple sets are comma-separated
(,1.1),(1.1,): this excludes 1.1 (for example if it is known not to work in combination with this library)

scope表示, 这个依赖在各种构建步骤时是否要放到classpath里

网友制图:

scope compile classpath test classpath runtime classpath 说明
compile 默认scope
provided   运行期由容器提供,如servlet-api包
runtime   编译期间不需要直接引用
test     只在测试期依赖,如junit包
system   编译和测试时由本机环境提供(需要systemPath)

maven的一个很好的功能是能自动处理依赖的传递

A项目依赖B, B依赖C, 那么构建A的时候也会引入C, 最终整个项目依赖关系, 会变成一个巨大的树形结构(图).

presentation notes:此时看idea图.

maven提供了5种方法帮助我们控制传递依赖, 解决传递依赖冲突. 按照顺序分别是:

  • 不同scope的依赖, 处理其传递依赖不同. 见下面的表.
  • exclusion 手动配置排除传递依赖. A exclude掉 B 的依赖 C. (会排除整个树下所有的C);
  • optional dependency. 可选依赖这个命名不合适, 其实是B的owner可以将C标记为optional, 然后A依赖B的时候就不会引入C, 相当于default excluded.
  • dependencyManagement. 依赖管理xml下面的依赖不是真的依赖, 而是当出现传递依赖或没指定版本号的依赖是, 使用依赖管理里面指定的. (依赖管理有个scope为import的特殊用法)
  • 经过以上还是传递依赖冲突时, 以离主项目最近的为准, 最近(closest)就是说树的深度最小. 相同时按顺序. A->B->C1.0 A->D->E->C2.0 会选C1.0.
B的scope \ C的scope compile provided runtime test
compile compile(*) - runtime -
provided provided - provided -
runtime runtime - runtime -
test test - test -

第一行翻译过来就是

  • 我的compile依赖的compile依赖还是我的compile依赖
  • 我的compile依赖的provided依赖被忽略
  • 我的compile依赖的runtime依赖还是我的runtime依赖
  • 我的compile依赖的test依赖被忽略

继承(inheritance, parent)

  <parent>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>my-parent</artifactId>
    <version>2.0</version>
    <relativePath>../my-parent</relativePath>
  </parent>

继承就和java中对象继承一样, 项目会继承parent项目pom中的绝大多数内容(除了artifactId这种), 冲突时以自己的为准. 这也是parent的作用, 抽出公共的配置放在parent里.

parent项目的packageType必须为pom, 也就是不能是jar这种. (类似于只能是接口不能是包含代码的具体类).

所有的项目的默认会有根的parent叫Super POM, (类似于java中的Object类). 这个就是神奇发生的地方.

组合(aggregation,multi-module)

  <groupId>org.codehaus.mojo</groupId>
  <artifactId>my-parent</artifactId>
  <version>2.0</version>
  <!-- 也是必须为pom类型 -->
  <packaging>pom</packaging>
  <modules>
    <module>my-project</module>
    <module>another-project</module>
    <module>third-project/pom-example.xml</module>
  </modules>

build带有modules的project, maven会收集所有子模块, 按一定顺序build每一个子模块.

组合和继承没有什么必然关系. parent项目可以包含或不包含modules.

配置变量properties

maven中可以引入五种配置变量

  • ${env.PATH}, 取shell环境变量
  • ${project.version}, 取pom.xml里的项
  • ${settings.offline}, 取settings.xml里的项
  • ${java.home}, 取java.lang.System.getProperties()能取到的项
  • ${someVar}, 在pom的properties里自定义的项

这些配置变量, 可以在pom文件中用${}来取用, 也可以用在后面会提到的filter中.

3. 构建

上面说了一堆项目的各种信息和依赖, 接下来介绍的就是真正执行各种功能. 我们常用的命令mvn clean package是什么意思呢? mvn -h可以看出, 命令执行时, 后面给出的是 [<goal(s)>] [<phase(s)>].

presentation notes:此时看idea右侧都是什么呢?

lifecycle & phase

maven将项目的构建组织成lifecycle, 每个lifecycle有多个阶段phase组成. 默认三个lifecycle: default, clean, site, 其中default由如下阶段组成:

  • validate - validate the project is correct and all necessary information is available
  • compile - compile the source code of the project
  • test - test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployed
  • package - take the compiled code and package it in its distributable format, such as a JAR.
  • verify - run any checks on results of integration tests to ensure quality criteria are met
  • install - install the package into the local repository, for use as a dependency in other projects locally
  • deploy - done in the build environment, copies the final package to the remote repository for sharing with other developers and projects.

这些阶段是有序的, 比如我们执行package, 那么package前面的几个阶段都会先执行.

(除了这些可以看到和执行的phase还有一些隐含的phase, 完整的phase参看)

plugin & goal

 <plugin>
   <groupId>org.codehaus.modello</groupId>
   <artifactId>modello-maven-plugin</artifactId>
   <version>1.8.1</version>
   <executions>
     <execution>
       <configuration>
         <models>
           <model>src/main/mdo/maven.mdo</model>
         </models>
         <version>4.0.0</version>
       </configuration>
       <goals>
         <goal>java</goal>
       </goals>
     </execution>
   </executions>
 </plugin>

每个阶段具体做什么取决于插件, 每个插件有多个goals, 插件的功能通过goals暴露. 比如clean插件有clean:cleanclean:help两个goals.

presentation notes:此时看idea右侧插件的goals

每个goal可以灵活绑定到一个或多个phase上:

所以执行构建的mvn [<goal(s)>] [<phase(s)>]:

  • 直接执行某个插件的某个goal.
  • 指定某个phase, 按顺序从上到下执行每个phase, 就是执行每个phase上绑定的所有goals.

presentation notes:此时看mvn执行输出中的 — xx

所有maven构建步骤都由各个插件完成, 即使pom.xml中没有配置任何插件, maven也根据打包类型不同, 默认绑定了一些插件的goals到不同的phase.

其实这个goals灵活绑定到phase上的设计还是不错的, 不过没啥太大用处的lifecycle设计可能被idea嫌弃了, 所以idea右侧lifecycle是一个简化形式.

resource & filter

  <build>
	<filters>
	  <filter>src/main/profiles/${package.env}/config.properties</filter>
      <filter>src/main/profiles/${package.env}/jdbc.properties</filter>
    </filters>
    ...
    <resources>
      <resource>
        <targetPath>META-INF/plexus</targetPath>
        <filtering>false</filtering>
        <directory>${basedir}/src/main/plexus</directory>
        <includes>
          <include>configuration.xml</include>
        </includes>
        <excludes>
          <exclude>**/*.properties</exclude>
        </excludes>
      </resource>
    </resources>
    <testResources>
      ...
    </testResources>
    ...
  </build>

build中另外比较重要的配置就是资源. 一些配置文件, 静态文件等不需要编译的东西我们一般放在src/main/resources目录(为什么用这个目录呢)下, 编译的时候需要拷贝到该去的地方.

还有一个filter功能, 就是将资源文件中的${someVar}占位符替换为实际的变量值. 这里可用的someVar就是上面提到过的所有五种properties, 外加使用filters配置的.properties文件.

同一个目录的资源也可以分开处理, 可以指定<inclues><excludes>(冲突时exclude wins.), 可以指定某些资源是否filter.

${}的变量占位符写法是默认的, 可以通过<resource.delimiter>@</resource.delimiter>这项properties配置改掉, springboot就这么坑人@someVar@

其他构建配置

  <defaultGoal>install</defaultGoal>
  <directory>${basedir}/target</directory>
  <finalName>${artifactId}-${version}</finalName>
  <sourceDirectory>${basedir}/src/main/java</sourceDirectory>
  <scriptSourceDirectory>${basedir}/src/main/scripts</scriptSourceDirectory>
  <testSourceDirectory>${basedir}/src/test/java</testSourceDirectory>
  <outputDirectory>${basedir}/target/classes</outputDirectory>
  <testOutputDirectory>${basedir}/target/test-classes</testOutputDirectory>

reporting

site阶段, 将项目的基本信息, 依赖信息, 文档, 测试报告等, 生成一个site. 像测试报告等各种功能可以使用插件增强, 不详细介绍.

4. 外部环境

repository

  <repositories>
   <repository>
      <releases>
        <enabled>false</enabled>
        <updatePolicy>always</updatePolicy>
        <checksumPolicy>warn</checksumPolicy>
      </releases>
      <snapshots>
        <enabled>true</enabled>
        <updatePolicy>never</updatePolicy>
        <checksumPolicy>fail</checksumPolicy>
      </snapshots>
      <id>codehausSnapshots</id>
      <name>Codehaus Snapshots</name>
      <url>http://snapshots.maven.codehaus.org/maven2</url>
      <layout>default</layout>
    </repository>
  </repositories>
  • 仓库: 就是下载依赖的地方, Super POM中配置了中央仓库, 全世界的人都从这里下载jar包.

  • 镜像仓库: 为了速度快之类的原因, 可以在settings.xml里配置镜像仓库 <mirror>, 比如一个阿里云的源的central仓库的镜像. (一个仓库至多指定一个镜像)

  • 私有仓库: 搭建自己的私有仓库, 里面放一些私有的包, 在项目中添加和使用. 也可以在settings.xml里的profile里配置全局的私有仓库. 多个仓库按照在pom中的顺序进行使用.

  • 本地仓库: 其实maven的策略是在本地user/.m2目录下存放本地缓存仓库, 本地有的时候就不用从远程下载了. 使用updatePolicy和checksumPolicy策略校验本地缓存是否有效.

    presentation notes:此时看.m2, 可以看到里面都是按groupId, artifactId组织的依赖

  • releases or snapshots: maven将项目版本分为这两类. 有的仓库比如中央仓库里是不放snapshot版本东西的.
  • 插件仓库同理 <pluginRepositories>

profile

  <profiles>
    <profile>
      <id>test</id>
      <activation>...</activation>
      <build>...</build>
      <modules>...</modules>
      <repositories>...</repositories>
      <pluginRepositories>...</pluginRepositories>
      <dependencies>...</dependencies>
      <reports>...</reports>
      <reporting>...</reporting>
      <dependencyManagement>...</dependencyManagement>
      <distributionManagement>...</distributionManagement>
      <properties>...</properties>
    </profile>
  </profiles>

不同环境不同配置, 比如dev, test, online, jdk8等等, 当这项profile激活时, 用profile里的东西覆盖项目的配置.

我们profile的常用用法, 设个变量, 拷贝resource或添加filter. 其实<profile>下面可以放的配置有很多, 都可以控制不同profile进行变化.

presentation notes:此时看idea代码, 解释上述两种用法.

profile的激活, 除了运行mvn -Ptest这种方式, 还可以在<activation>控制. 多个profile可以同时生效.

<activation>
  <activeByDefault>false</activeByDefault>
  <jdk>1.5</jdk>
  <os>
    <name>Windows XP</name>
    <family>Windows</family>
    <arch>x86</arch>
    <version>5.1.2600</version>
  </os>
  <property>
    <name>sparrow-type</name>
    <value>African</value>
  </property>
  <file>
    <exists>${basedir}/file2.properties</exists>
    <missing>${basedir}/file1.properties</missing>
  </file>
</activation>

scm issue ci dist

  <issueManagement>...</issueManagement>
  <ciManagement>...</ciManagement>
  <mailingLists>...</mailingLists>
  <scm>...</scm>
  <prerequisites>...</prerequisites>
  <distributionManagement>...</distributionManagement>

看起来大部分没啥用的, 其实是可以被一些插件拿来做功能的, 比如<distributionManagement>配置可以在deploy阶段将打好的jar包发布到maven仓库中.

其他todo

settings.xml

对比Gradle

flexibility, performance, user experience, and dependency management

  • 配置简单, 可能有些人比较烦xml吧, gradle使用dsl. 而且虽然都是约定大于配置, gradle内置的更方便一些.
  • 各种提示信息, report, 界面, 比较友好. build scan更是不错, 不过必须发到他的网站上. 不过Idea支持非常蠢.
  • 通过加cache之类的操作, 速度快很多.
  • Android,Groovy官方使用, C++/Scala/Kotlin/Java/Js都可以用.

大部分概念都很相像:

  • 包, 命名规范, 沿用Maven的
  • 仓库沿用Maven的, 另外可以用JCenter库.
  • 功能也靠各种插件实现.
    • Maven是plugin+goal, Gradle是plugin+task
    • Maven通过将goal绑定phase来实现动态控制执行步骤. Gradle通过task直接的depend关系形成DAG动态控制执行步骤.

Archetype

开发插件

extensions

jar结构

源码阅读

UPDATE0: 测试报告


    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>2.19</version>
        </plugin>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <version>0.8.1</version>
            <executions>
                <execution>
                    <id>default-prepare-agent</id>
                    <goals>
                        <goal>prepare-agent</goal>
                    </goals>
                </execution>
                <execution>
                    <id>default-report</id>
                    <goals>
                        <goal>report</goal>
                    </goals>
                </execution>
                <execution>
                    <id>default-check</id>
                    <goals>
                        <goal>check</goal>
                    </goals>
                    <configuration>
                        <rules>
                            <rule implementation="org.jacoco.maven.RuleConfiguration">
                                <element>BUNDLE</element>
                                <limits>
                                    <limit implementation="org.jacoco.report.check.Limit">
                                        <counter>COMPLEXITY</counter>
                                        <value>COVEREDRATIO</value>
                                        <minimum>0.06</minimum>
                                    </limit>
                                </limits>
                            </rule>
                        </rules>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>findbugs-maven-plugin</artifactId>
            <version>3.0.2</version>
        </plugin>
    </plugins>
</build>
<!--所有<reporting>里面的配置不是必须的, 当想要mvn site生成项目site时可以加上-->
<reporting>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-report-plugin</artifactId>
            <version>2.20.1</version>
        </plugin>
        <plugin>
            <groupId>org.jacoco</groupId>
            <artifactId>jacoco-maven-plugin</artifactId>
            <reportSets>
                <reportSet>
                    <reports>
                        <report>report</report>
                    </reports>
                </reportSet>
            </reportSets>
        </plugin>
        <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>findbugs-maven-plugin</artifactId>
            <version>3.0.2</version>
        </plugin>
    </plugins>
</reporting>

UPDATE1: Assembly

使用Assembly插件打成自定义格式的包, 参考官方文档 1 2

UPDATE2: transitive repository

原本以为mvn只会使用setting和项目pom里明确指定的repo下载依赖, 然而有个项目报错从一个amazon的repo下东西超时.

使用mvn dependency:list-repositories 命令看到, 列出的repo远超我配置的. 猜测是mvn还会使用我的依赖的pom里配置的repo, 我称之为鬼畜的传递repo.

参考这两个SO问题:

  • https://stackoverflow.com/q/1754495/5142886
  • https://stackoverflow.com/q/14502440/5142886

使用里面的方法干掉repo就好了