Maven豆知识

好久没有写豆知识系列了。
记录下使用中的一些问题。

eclipse中maven项目的bug

两个项目同时在开发,在同一个workspace,项目A依赖项目B。项目A会直接使用项目B中的代码(B的最新代码还没有install或deploy),而不是maven repository中的jar包。
换言之,我在项目B中加了一些新的类,eclipse中的项目A也可以使用,正常编译。
但如果在命令行中mvn clean package,就肯定会失败,ClassNotFound。

对于正常的项目而言,改下版本号就可避免。项目A依赖的项目B版本和项目B正在开发的版本不同,就会直接依赖repo中的jar包。
但我碰到的情况是项目A依赖了一个SNAPSHOT版本。而项目B也正在这个SNAPSHOT版本上开发。有点蛋疼。
只能记着多deploy了。

不确定这是bug,也许有配置可以改,但我没找到。

skipTests与maven.test.skip

首先要知道maven生命周期
skipTests只是maven-surefire-plugin插件的一个参数,这个插件是maven自带的,默认绑定到test阶段。设置这个参数后,test阶段不会执行测试。
而maven.test.skip会影响更多插件,包括maven-resources-plugin(testResource阶段),maven-compiler-plugin(testCompile阶段),maven-surefire-plugin(test阶段)。换言之,和测试有关的所有阶段都会起作用。

所以maven.test.skip是更彻底的跳过测试的方法。
直观的感受,就是如果src/test/java下的代码编译有问题,加上maven.test.skip就能防止整个项目构建失败。而只用skipTests则会在testCompile阶段失败。
但maven.test.skip也可能造成一些奇怪的问题,见这里

report插件

maven本质上来说就是个插件的集合。
而插件分为两种,build插件和report插件。build插件会在default生命周期生效,配置在POM中的<build/>元素里;report插件会在site生命周期生效,配置在POM中的<reporting/>元素里。
根据配置的插件不同,mvn site命令会在target目录下生成不同的html报告,帮你了解项目的一些概况。
虽然大多数项目用不到,但挺好玩的。所以研究了下。

常见的report插件(有一些插件既是build插件也是report插件):

插件名 说明
maven-project-info-reports-plugin 这个其实是自带的,有很多内置的报告
maven-pmd-plugin 代码静态检查
maven-jxr-plugin 将代码以html方式呈现
findbugs-maven-plugin 另一个代码静态检查,但findbug是分析class文件,而不是java文件
jdepend-maven-plugin 一些统计信息
taglist-maven-plugin 汇总代码中的TODO,也可自定义其他的tag
maven-surefire-report-plugin 生成单元测试的报告
cobertura-maven-plugin 检查单元测试覆盖率,配置可以很复杂
maven-javadoc-plugin 生成javadoc
maven-checkstyle-plugin 强迫症福音,检查代码风格,比如方法名/变量名/空格/每个方法不超过多少行

每个插件都有自己的配置,可以去看相应的官方文档。

另外对于多模块的项目而言,mvn site只会在各个模块的target目录下生成报告,而不会汇总起来。可以用mvn site:stash命令汇总到父项目的target目录,方便查看。
但这个命令要求POM中做些修改:

1
2
3
4
5
6
7
8
9
<distributionManagement>
<!-- 没有这个配置site:stage会报错 -->
<site>
<id>nexus</id>
<name>site</name>
<!-- 只是随便看看报告,不需要真的部署到某个站点,所以随便写个url -->
<url>http://dummy</url>
</site>
</distributionManagement>

jetty plugin的配置

jetty plugin用来调试很方便,但网上很多文章给的配置都是6.x老版本的。
jetty现在已经由eclipse基金会维护了。新版的配置项和6.x很不一样,见官方文档
例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<!-- jetty Plug-in -->
<plugin>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-maven-plugin</artifactId>
<!-- jetty9需要至少jdk7 -->
<version>9.2.8.v20150217</version>
<configuration>
<!-- <scanIntervalSeconds>10</scanIntervalSeconds> -->
<!-- 我习惯手动reload,按回车即可重新部署 -->
<reload>manual</reload>
<dumpOnStart>false</dumpOnStart>
<webApp>
<contextPath>/</contextPath>
<!-- 配置额外的资源路径,一般用于测试 -->
<resourceBases>
<resourceBase>${project.basedir}/src/main/webapp</resourceBase>
<resourceBase>${project.basedir}/../misc/swagger</resourceBase>
</resourceBases>
</webApp>
<!-- 开启访问日志 -->
<requestLog implementation="org.eclipse.jetty.server.NCSARequestLog">
<logDateFormat>yyyy-MM-dd HH:mm:ss</logDateFormat>
<logTimeZone>GMT+8:00</logTimeZone>
<logServer>true</logServer>
<logCookies>true</logCookies>
</requestLog>
<!-- 设置一些系统属性 -->
<systemProperties>
<force>true</force>
<systemProperty>
<name>dubbo.application.logger</name>
<value>slf4j</value>
</systemProperty>
</systemProperties>
</configuration>
</plugin>

依赖版本冲突

如果在项目中引入同一个依赖的不同版本,会发生什么情况?
maven会自动帮你选择一个版本,这里又分两种情况:

如果是直接声明的依赖,后声明的生效。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--调整这两个依赖的顺序,再执行dependency:tree,可以看到不同-->
<!--注意不但1.6.0的slf4j-jdk14会覆盖前面的,1.6.0引入的间接依赖也会覆盖前面的-->
<dependency>
<!--相当于pom里根本没有这个1.6.1的配置-->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.6.0</version>
</dependency>

如果是间接引入的依赖,规则会更复杂,参考这个
简言之,深度较小的优先;如果深度相同,先声明的优先。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<!--最终1.6.1版本的slf4j-api生效-->
<dependency>
<!--这个依赖会引入slf4j-api-1.6.1-->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<!--这个依赖会引入slf4j-api-1.6.0-->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.6.0</version>
</dependency>

如果上面两种情况混合起来,就更蛋疼了。。。例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.6.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.6.1</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<version>1.6.0</version>
</dependency>

最终生效的依赖是slf4j-jdk14-1.6.1、slf4j-api-1.6.1、slf4j-nop-1.6.0。

enforcer plugin

上面说过依赖版本冲突时maven会自动帮你选择一个版本,但这样可能有隐患。例如某个模块依赖于guava 18.0的一些新功能,而maven却自动选择了guava 15.0。运行的时候很可能会报错,比如ClassNotFound或NoSuchMethod。如果编译正常,但运行时出现这两种异常,很可能是jar包版本冲突。
版本冲突还可能导致其他很多奇怪的问题。

所以最佳实践是不要有版本冲突。检测版本冲突就要用到enforcer plugin,maven的爱之铁拳。。。

简言之,可以配置一些强制性的规则,如果规则不满足,build会失败。不光可以用来检测版本冲突,还可以做很多其他事情,还可以自己编写规则,又一个强迫症福音。。。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version>
<executions>
<execution>
<id>enforce</id>
<configuration>
<!-- 规则不满足时,只输出warning日志,不会导致构建失败 -->
<!-- 看场景,我个人喜欢只输出日志;更严格的场景下可以去掉这个配置。 -->
<fail>false</fail>
<rules>
<!-- 检测依赖版本冲突 -->
<dependencyConvergence />
<!-- 版本号的写法:http://maven.apache.org/enforcer/enforcer-rules/versionRanges.html -->
<!-- 至少maven3 -->
<requireMavenVersion>
<version>3.0</version>
</requireMavenVersion>
<!-- 至少jdk6 -->
<requireJavaVersion>
<version>1.6</version>
</requireJavaVersion>
</rules>
</configuration>
<goals>
<goal>enforce</goal>
</goals>
</execution>
</executions>
</plugin>

enforcer plugin默认会绑定到validate阶段,详细文档见这里

插件的configuration元素

折腾enforcer插件的时候,发现一个奇怪的问题。mvn clean package时,插件可以正常生效;mvn enforcer:enforce却会报错,说找不到相关配置。
google了一下,找到这个

简单的说,配置一个插件的<configuration>元素时,可以直接配置,比如:

1
2
3
4
5
6
7
8
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version>
<configuration>
...
</configuration>
</plugin>

这样的配置对这个插件在所有情况下都生效。

也可以配置在<execution>元素中,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version>
<executions>
<execution>
<id>enforce</id>
<configuration>
...
</configuration>
<goals>
<goal>enforce</goal>
</goals>
</execution>
</executions>
</plugin>

这样的配置只会在maven执行到特定生命周期时才会生效。
对enforcer插件而言,默认绑定在validate阶段,所以configuration元素只有在validate阶段才生效。
所以直接执行mvn enforcer:enforce才会报错。

默认插件

maven默认会将一些插件绑定到特定生命周期,比如clean插件、compile插件、resource插件等。这个是在哪里配置的?
答案是$M2_HOME/lib/maven-core-x.y.z.jar文件中,解压这个文件可以找到一个xml文件,配置了所有默认的插件和绑定的阶段。参考官方文档:12
本来以为是在超级pom中配置的,实际不是。

插件绑定

一个插件可以有多个goal,每个goal可以绑定到特定的生命周期上。
有些goal会有默认的绑定,比如enforcer:enforce默认会绑定到validate阶段。这种插件在配置execution元素时可以不用配置phase。
有些goal没有默认绑定,比如jetty:run,如果要在build过程中自动执行,配置execution元素时就必须明确指定phase。当然也可以不在build过程中执行,直接mvn jetty:run去执行特定插件的特定goal。
这个是由编写插件时的@Mojo注解决定的,见官方文档

另外pluginManagement和dependencyManagement的语义有些不同。pluginManagement中的插件即使不配置在plugins中,也可以直接使用,比如mvn jetty:run。而dependencyManagement中的元素,必须在dependency中再配置一遍才能生效。