Maven 开发随笔

Maven 使用

Maven 调试技巧

  • mvn dependency:tree,打印依赖树
  • mvn clean -X,使用 -X 参数输出详细的日志信息
  • mvn -X,查看当前生效的是哪个 settings.xml 配置文件
  • mvn help:effective-settings,查看正在起作用的是那个 settings.xml 的文件内容

打包跳过测试用例

  • 使用命令 mvn install -DskipTests,不执行测试用例,但会编译测试用例类的代码
  • 使用命令 mvn install -Dmaven.test.skip=true,不但跳过单元测试的运行,也跳过单元测试代码的编译

添加阿里云镜像仓库

添加阿里云的镜像到 Maven 的 setting.xml 配置文件中,这样就不需要每次在项目的 pom.xml 中添加镜像仓库的配置内容,在 <mirrors> 节点里添加对应的子节点即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<mirrors>
<mirror>
<id>alimaven</id>
<name>aliyun maven</name>
<url>https://maven.aliyun.com/repository/public/</url>
<mirrorOf>central</mirrorOf>
</mirror>
<mirror>
<id>repo2</id>
<mirrorOf>central</mirrorOf>
<name>Human Readable Name for this Mirror.</name>
<url>http://repo2.maven.org/maven2/</url>
</mirror>
</mirrors>

Maven 项目配置

打包源码

有时候开发一个公共 Jar 包给别人引用,当别人打开包中的类的时候,默认情况下打开的是 IDE 工具反编译出来的 .class 文件,类中的注释什么的都看不到,此时 IDE 工具会提示可以 Download Sources,但是如果打包的时候没有同时打一个以 -sources.jar 结尾的源码 Jar 包,那么调用方下载源码包的时候就会失败。maven-source 插件就是用来打包源码的,可以部署到私有仓库上的,对于需要查看源码、注释和调试代码,有很大的帮助。

1
2
3
4
5
6
7
8
9
10
11
12
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>

打包跳过测试用例

1
2
3
4
5
6
7
8
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.5</version>
<configuration>
<skipTests>true</skipTests>
</configuration>
</plugin>

使用阿里云镜像仓库

编辑项目的 pom.xml 文件,添加阿里云中央仓库的配置内容。

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
<repositories>
<repository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>

值得一提的是,Maven 默认中央仓库的 idcentral。其中 id 是唯一的,因此使用 <id>central</id> 就可以覆盖默认的中央仓库。

指定编译的 JDK 版本

方式一

1
2
3
4
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

方法二

1
2
3
4
5
6
7
8
9
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>

方法三

该方式并非 Maven 官方配置,要使该方式能够生效首先必须满足以下两个条件:

  • 项目为一个 SpringBoot 工程
  • 项目的 POM 继承了 spring-boot-starter-parent
1
2
3
4
5
6
7
8
9
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>

<properties>
<java.version>1.8</java.version>
</properties>

引入本地的 Jar 包

方法一(推荐)

通过 mvn install 命令手动将 Jar 包安装到 Maven 本地仓库(如下),相关资料可参考这里

1
$ mvn install:install-file -Dfile=taobao-sdk-java-auto.jar -DgroupId=com.dingtalk -DartifactId=dingtalk-api-sdk -Dversion=1.0.0-SNAPSHOT -Dpackaging=jar

最后在 pom.xml 里正常引入对应的依赖即可:

1
2
3
4
5
<dependency>
<groupId>com.dingtalk</groupId>
<artifactId>dingtalk-api-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

若需要从本地仓库卸载特定的本地 Jar 包,可以在 pom.xml 所在目录下执行以下命令,其中 -DmanualInclude 的参数格式为 groupId:artifactId

1
2
3
4
5
# 从Maven本地仓库卸载某个Jar包
$ mvn dependency:purge-local-repository -DmanualInclude="com.dingtalk:dingtalk-api-sdk"

# 阻止Maven对已删除的Jar包进行ReResolve
$ mvn dependency:purge-local-repository -DreResolve=false

方法二(推荐)

通过 Maven 插件将本地 Jar 包安装到 Maven 本地仓库(如下),值得一提的是,需要手动执行 mvn clean && mvn install 命令将本地 Jar 包安装到 Maven 本地仓库,相关资料可参考这里

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
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
<executions>
<execution>
<id>install-external</id>
<phase>clean</phase>
<configuration>
<file>${project.basedir}/lib/taobao-sdk-java-auto.jar</file>
<repositoryLayout>default</repositoryLayout>
<groupId>com.dingtalk</groupId>
<artifactId>dingtalk-api-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<generatePom>true</generatePom>
</configuration>
<goals>
<goal>install-file</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

最后在 pom.xml 里正常引入对应的依赖即可:

1
2
3
4
5
<dependency>
<groupId>com.dingtalk</groupId>
<artifactId>dingtalk-api-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

方法三(不推荐)

假设项目的目录结构如下:

1
2
3
4
5
market
├── common-api
│   └── lib
│   └── taobao-sdk-java-auto.jar
└── common-cache

common-api 模块下的 pom.xml 添加以下依赖来引入本地 Jar 包:

  • groupId:自定义
  • artifactId:自定义
  • version:自定义
  • scope:必须是 system
  • systemPath:Jar 包的路径,${project.basedir} 是指当前 pom.xml 所在的目录
1
2
3
4
5
6
7
<dependency>
<groupId>com.dingtalk</groupId>
<artifactId>dingtalk-api-sdk</artifactId>
<version>1.0.0-SNAPSHOT</version>
<scope>system</scope>
<systemPath>${project.basedir}/lib/taobao-sdk-java-auto.jar</systemPath>
</dependency>

common-api 模块是普通 Maven 项目,那么打包时需要添加以下配置,目的是将本地 Jar 包同时打包在一起:

  • directory:指定 lib 文件夹的位置,一般直接写上 ${project.basedir}/lib 即可
  • targetPath:打包到的文件夹位置,写上 BOOT-INF/lib 即可,或者是 WEB-INF/lib
  • includes:一般都是以 .jar 结尾,直接写 **/*.jar 即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<resources>
<resource>
<directory>${project.basedir}/lib</directory>
<targetPath>/BOOT-INF/lib/</targetPath>
<includes>
<include>**/*.jar</include>
</includes>
</resource>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
</build>

common-api 模块是 SpringBoot 项目,那么打包时需要添加以下配置,目的是将本地 Jar 包同时打包在一起:

1
2
3
4
5
6
7
8
9
10
11
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<includeSystemScope>true</includeSystemScope>
</configuration>
</plugin>
</plugins>
</build>

单元测试拷贝资源文件

@SpringBootTest 注解,只会加载 src/test 路径下的资源文件(如 XML 配置),并不会加载 src/main 路径下的资源文件。若需要在执行单元测试时,加载 src/main 路径下的资源文件,需要让 Maven 拷贝对应的资源文件到 src/test 路径下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
<includes>
<include>**/*.*</include>
</includes>
</resource>
</resources>
<!--单元测试时引用src/main/resources下的资源文件-->
<testResources>
<testResource>
<directory>src/main/resources</directory>
</testResource>
<testResource>
<directory>src/test/resources</directory>
</testResource>
</testResources>
</build>

编译时过滤字体图标文件

如果 Maven 编译项目时,出现下述的错误信息。这一般是 Maven 的 filter 解析字体图标文件时,破坏了文件的二进制文件格式导致的。

1
filtering /xxxx/target/classes/static/fonts/webfont.ttf failed with MalformedInputException: Input length = 1 -> [Help 1]
  • 第一种解决方案(推荐):
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
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>true</filtering>
</resource>
</resources>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<nonFilteredFileExtensions>
<nonFilteredFileExtension>woff</nonFilteredFileExtension>
<nonFilteredFileExtension>woff2</nonFilteredFileExtension>
<nonFilteredFileExtension>eot</nonFilteredFileExtension>
<nonFilteredFileExtension>ttf</nonFilteredFileExtension>
<nonFilteredFileExtension>otf</nonFilteredFileExtension>
<nonFilteredFileExtension>svg</nonFilteredFileExtension>
</nonFilteredFileExtensions>
</configuration>
</plugin>
</plugins>
</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
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<filtering>false</filtering>
<includes>
<include>**/*.woff</include>
<include>**/*.ttf</include>
<include>**/*.woff2</include>
<include>**/*.otf</include>
<include>**/*.eot</include>
<include>**/*.svg</include>
</includes>
</resource>
</resources>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build>

将所有依赖包打包进单个 Jar 包

若 Maven 打包时,需要将项目源码与所有依赖包一起打包在单个 Jar 文件中,可以参考以下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
</plugin>
</plugins>
</build>

将项目的测试代码打包进 Jar 包

Maven 支持打包的插件列表如下:

pluginfunction
maven-jar-pluginmaven 默认打包插件,用来创建 project jar
maven-shade-plugin 用来打可执行包,executable (fat) jar
maven-assembly-plugin 支持定制化打包方式,例如 apache 项目的打包方式

添加 maven-assembly-plugin 插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>src/main/resources/assembly.xml</descriptors>
</configuration>
</execution>
</executions>
</plugin>

还可以在上面的 configuration 节点中指定执行 Jar 包时的 Test 主类:

1
2
3
4
5
6
7
8
<configuration>
<descriptors>src/main/resources/assembly.xml</descriptors>
<archive>
<manifest>
<mainClass>com.sample.TestMain</mainClass>
</manifest>
</archive>
</configuration>

创建 assembly.xml 配置文件:

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
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">

<id>assembly</id>

<formats>
<format>jar</format>
</formats>

<includeBaseDirectory>false</includeBaseDirectory>

<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>test</scope>
</dependencySet>
</dependencySets>

<fileSets>
<fileSet>
<directory>${project.build.directory}/test-classes</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>**/*.class</include>
</includes>
<useDefaultExcludes>true</useDefaultExcludes>
</fileSet>
</fileSets>
</assembly>

特别注意:上面 assembly.xml 里的配置,默认会将所有 Jar 包的源码一起打包(包括依赖的第三方 Jar 包),如果只希望打包项目自身的源码,那么则需要添加 exclude 节点来排除依赖:

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
37
38
39
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">

<id>assembly</id>

<formats>
<format>jar</format>
</formats>

<includeBaseDirectory>false</includeBaseDirectory>

<dependencySets>
<dependencySet>
<outputDirectory>/</outputDirectory>
<useProjectArtifact>true</useProjectArtifact>
<unpack>true</unpack>
<scope>test</scope>
<excludes>
<exclude>org.slf4j:slf4j-api</exclude>
<exclude>com.fasterxml.jackson.core:*</exclude>
<exclude>org.apache.httpcomponents:*</exclude>
<exclude>com.google.guava:guava</exclude>
</excludes>
</dependencySet>
</dependencySets>

<fileSets>
<fileSet>
<directory>${project.build.directory}/test-classes</directory>
<outputDirectory>/</outputDirectory>
<includes>
<include>**/*.class</include>
</includes>
<useDefaultExcludes>true</useDefaultExcludes>
</fileSet>
</fileSets>
</assembly>

运行以下打包命令,会生成 xxxx-1.0.0-SNAPSHOT-assembly.jar 文件,其中文件名里的 assembly 是由 assembly.xml 配置文件里的 id 节点指定:

1
$ mvn package

Maven 私有仓库配置

通过插件上传 Jar 包到私有仓库

编辑 Maven 的 settings.xml 配置文件,通过添加 server 节点来配置私有仓库的账号和密码,这里的 id 可以随便取名字,但必须与下面配置的 id 一致:

1
2
3
4
5
6
7
8
9
10
11
<server>
<id>nexus-releases</id>
<username>admin</username>
<password>admin123</password>
</server>

<server>
<id>nexus-snapshots</id>
<username>admin</username>
<password>admin123</password>
</server>

编辑 Maven 的 settings.xml 配置文件,通过添加 mirror 节点来配置私有仓库的地址,这里的 id 必须和上面 server 节点的 id 一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
<mirror>
<id>nexus-releases</id>
<mirrorOf>*</mirrorOf>
<name>nexus maven releases repo</name>
<url>http://127.0.0.1:8081/nexus/content/repositories/releases/</url>
</mirror>

<mirror>
<id>nexus-snapshots</id>
<mirrorOf>*</mirrorOf>
<name>nexus maven snapshots repo</name>
<url>http://127.0.0.1:8081/nexus/content/repositories/snapshots/</url>
</mirror>

编辑项目的 pom.xml 文件,添加以下内容,这里的 id 必须与 上面 mirror 节点里的 id 一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<distributionManagement>
<repository>
<id>nexus-snapshots</id>
<name>Nexus Snapshots Repository</name>
<url>http://127.0.0.1:8081/nexus/content/repositories/snapshots/</url>
</repository>
<!--
<repository>
<id>nexus-releases</id>
<name>Nexus Releases Repository</name>
<url>http://127.0.0.1:8081/nexus/content/repositories/releases/</url>
</repository>
-->
</distributionManagement>

执行发布命令:

1
$ maven clean deploy

Maven 常见问题解决

上传 Jar 包到私有仓库出现 400 错误码

一般有 3 个导致出现上面问题的原因:

  • 一、pom.xml 中仓库 id 配置不对,检查 pom.xml 中配置的 distributionManagement 中的仓库 id、url 和私服 Nexus 中的是否相同
1
2
3
4
5
6
7
<distributionManagement>
<repository>
<id>nexus-snapshots</id>
<name>Nexus Snapshots Repository</name>
<url>http://127.0.0.1:8081/nexus/content/repositories/snapshots/</url>
</repository>
</distributionManagement>
  • 二、私服 Nexus 已经存在相同版本且代码完全一样的 Jar 包,同时部署策略为不允许覆盖;此时只需要将仓库对应的 Deployment Policy 设置为 Allow Redeploy 即可

maven-nexus-redeploy

  • 三、如果 Repository Policy 为 Release,则部署 Jar 包的版本号中不允许出现 snapshot 关键字;特别注意,Repository Policy 有两个选项,一个发布版本,一个是快照版本,要和部署 Jar 包的版本号完全对应

maven-nexus-repository-policy

上传 Jar 包到私有仓库出现 401 错误码

一般报 401 这个错误码,是因为没有发布权限,而没发布权限的原因,大部分都是因为密码错了导致,或者账号本身就没有发布 Jar 包的权限。如果是密码错误,则可以编辑 Maven 的 settings.xml 配置文件,通过添加 server 节点来配置私有仓库的账号和密码:

1
2
3
4
5
6
7
8
9
10
11
<server>
<id>nexus-releases</id>
<username>admin</username>
<password>admin123</password>
</server>

<server>
<id>nexus-snapshots</id>
<username>admin</username>
<password>admin123</password>
</server>

如果密码正确,账号本身也具有发布 Jar 包的权限,但依然提示 401 错误,那么此时应该检查当前生效的 settings.xml 配置文件是哪个,查询命令如下:

1
2
3
4
5
$ mvn -X
[DEBUG] Reading global settings from /usr/develop/maven-3.6.0/conf/settings.xml
[DEBUG] Reading user settings from /root/.m2/settings.xml
[DEBUG] Reading global toolchains from /usr/develop/maven-3.6.0/conf/toolchains.xml
[DEBUG] Reading user toolchains from /root/.m2/toolchains.xml

如果上面输出的 global settings 指向的配置文件不是所期望的,此时就应该注意了,可以使用以下命令进一步查看正在起作用的那个 settings.xml 的内容:

1
$ mvn help:effective-settings

Nexus 常见问题解决

指定 Nexus 使用的 JDK 版本

Nexus VersionSupported Sun/Oracle JRE version
1.9 and earlier5 or 6
2.0-2.56 or 7
2.6.x7u45+, only 8+ will not work
2.7.x-2.9.x7u45+, 8+ may work but is not thoroughly tested
2.10.x-2.11.17u45+, 8u25+
2.11.2+8u31+ strongly recommended, 7u79+ no further public updates as of April 2015
2.12.0-018u31+ strongly recommended, 11.0.9 not work

由于以前安装的 Nexus 版本为 2.12.0-01,JDK 版本为 8,后来 JDK 升级为 11 后,Nexus 无法启动,因此只能指定 Nexus 默认使用 JDK 8,方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 进入配置文件所在的目录
$ cd nexus/nexus-2.12.0-01/bin/jsw/conf

# 备份配置文件
$ cp wrapper.conf wrapper.conf.default

# 编辑配置文件,指定JDK的安装路径
$ vim wrapper.conf
wrapper.java.command=/usr/java/jdk1.8.0_102/bin/java

# 重启Nexus服务
$ service nexus restart

# 查看Nexus服务的运行状态
$ service nexus status

Maven 实用插件介绍

项目打包与 Git Commit 信息关联插件

Maven 打包发布版本可能会遇到自己的提交不起作用的情况,排查比较困难,可能需要拉下服务器上的 Jar 包,然后反编译查看是否包含自己的提交记录。如果使用的是 Git 作为 SCM,可以使用 git-commit-id-plugin 插件,该插件可以很方便地在项目打包时,将 Git Commit 信息写入 git.properties 文件并存放在 Jar 包中,这样就可以很方便的查看 Jar 包对应的提交记录。

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
<build>
<plugins>
<plugin>
<groupId>io.github.git-commit-id</groupId>
<artifactId>git-commit-id-maven-plugin</artifactId>
<version>4.9.9</version>
<executions>
<execution>
<id>get-the-git-infos</id>
<goals>
<goal>revision</goal>
</goals>
<phase>initialize</phase>
</execution>
</executions>
<configuration>
<failOnNoGitDirectory>false</failOnNoGitDirectory>
<generateGitPropertiesFile>true</generateGitPropertiesFile>
<!-- 日期格式 -->
<dateFormat>yyyy-MM-dd HH:mm:ss</dateFormat>
<includeOnlyProperties>
<includeOnlyProperty>^git.build.(time|version)$</includeOnlyProperty>
<includeOnlyProperty>^git.commit.(id|message|time).*$</includeOnlyProperty>
</includeOnlyProperties>
</configuration>
</plugin>
</plugins>
</build>