由pom文件结构拆解Maven功能
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
官方reference和xsd文件都是了解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 availablecompile
- compile the source code of the projecttest
- test the compiled source code using a suitable unit testing framework. These tests should not require the code be packaged or deployedpackage
- 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 metinstall
- install the package into the local repository, for use as a dependency in other projects locallydeploy
- 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:clean
和clean:help
两个goals.
presentation notes:此时看idea右侧插件的goals
每个goal可以灵活绑定到一个或多个phase上:
- 插件开发者开发每个goal时指定默认绑定的phase.
- 项目拥有者通过
<execution>
指定自己想要的绑定.
所以执行构建的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就好了