如果你是有一定的开发经验,我相信你一定被项目 lib
下的 JAR
包折磨过,如果碰上兼容问题,更是逐个下载不同版本 JAR
包进行替换排查,相信是每个程序员都不想再经历一边的噩梦。
Maven
的出现则大大降低开发人员的准备工作,让开发人员更专心与业务,下面即介绍 Maven
基本使用。
Maven 是一个项目管理工具,可以对 Java 项目进行构建、依赖管理。
一、基本介绍
1. 仓库配置
在 Maven
中引入了仓库的概念,开发人员将所编写的 JAR
按照相应格式推送到仓库中,当其他开发者需要引用这个 JAR
包时在工程中引用相应依赖,则会先从中央仓库进行下载到本地仓库,此时项目将读取本地仓库的内容。
对于部分组织或机构通常会在此基础上额外搭建私人仓库,在引用依赖时会先从私人仓库进行读取,如果未找到再从中央仓库下载至私人仓库,最后再下载到本地仓库。
通过这种方式开发者则无需再手动管理繁杂的项目 JAR
包,从而实现更高的效率。
2. 目录配置
在 setting.xml
配置文件中可通过 localRepository
标签配置本地仓库目录,若没有配置则默认目录为:${user.home}/.m2/repository
。
<localRepository>c:\repository</localRepository>
3. 服务连接
在使用私服时,在 setting.xml
中通过 servers
标签定义私服的连接账号信息。
servers
标签组中可定义多个 server
,每一个 server
代表一个用户,且包含下述三个属性。
标签 | 作用 |
---|---|
id | 标识符,后续可根据使用用户。 |
username | 用户名。 |
password | 用户密码。 |
privateKey | 认证信息,与 username/password 二者互斥。 |
如下述分别定义了 user_1
与 user_2
两个用户,分别通过通过用户名密码以及认证文件的形式声明。
<servers>
<server>
<id>user_1</id>
<username>alex</username>
<password>123456</password>
</server>
<server>
<id>user_2</id>
<privateKey>/path/to/private/key</privateKey>
<passphrase>optional; leave empty if not used.</passphrase>
</server>
</servers>
完成上述定义后,当在项目中通过 distributionManagement
配置私服仓库时,即可通过 id
关联读取上述 server
定义的用户。
<distributionManagement>
<repository>
<id>user_1</id>
<url>http://127.0.0.1:8080/repository/maven-releases</url>
</repository>
<snapshotRepository>
<id>user_2</id>
<url>http://127.0.0.1:8080/repository/maven-snapshots</url>
</snapshotRepository>
</distributionManagement>
4. 线上私服
在 setting.xml
文件中通过 profiles
标签组定义配置,如依赖、插件与仓库等。同理 profiles
包含多个 profile
,每个 profile
包含下述属性:
属性 | 作用 |
---|---|
id | 标识符用于关联引用。 |
repositories | 私服地址。 |
pluginRepositories | 插件私服地址 |
下述为一个私服配置示例:
<profiles>
<profile>
<id>nexus</id>
<repositories>
<repository>
<id>maven-repo</id>
<url>http://127.0.0.1:8080/repository/maven-releases</url>
<layout>default</layout>
<releases>
<!-- 启用仓库的发布版本 -->
<enabled>true</enabled>
</releases>
<snapshots>
<!-- 启用仓库的快照版本 -->
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
</profile>
</profiles>
同时通过 activeProfiles
标签使上述的配置对每个项目构建都生效。
<activeProfiles>
<activeProfile>nexus</activeProfile>
</activeProfiles>
5. 镜像配置
在 setting.xml
中通过 mirrors
配置镜像地址,当拉取依赖时优先到该地址查找。
标签 | 作用 |
---|---|
id | 唯一标识符 |
mirrorOf | 原始仓库 ID,可选:中央仓库(central),所有仓库(*),所有外部仓库(external.*)。 |
url | 镜像仓库地址。 |
下述为一个镜像配置示例:
<mirrors>
<mirror>
<id>ibudai</id>
<mirrorOf>*</mirrorOf>
<url>http://127.0.0.1:8080/repository/maven</url>
</mirror>
</mirrors>
二、依赖管理
1. 工程定义
一个最基本的 Maven
项目通常应包含下表中所列信息,当我们引用一个模块时,也是通过 groupId
,artifactId
与 version
三项进行确定。
标 签 | 作 用 |
---|---|
groupId | 项目组织,通常为域名反写,必填。 |
artifactId | 项目的名称,必填。 |
version | 项目的版本信息,必填。 |
name | 项目的简称,非必填。 |
description | 项目的简要描述,非必填。 |
下面是一个基本定义示例:
<?xml version="1.0" encoding="UTF-8"?>
<project ...>
<!-- 固定 4.0.0, 指定了当前 POM 模型的版本 -->
<modelVersion>4.0.0</modelVersion>
<groupId>xyz.ibudai</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>maven demo</name>
<description>This is maven demo.</description>
</project>
2. 依赖引用
通过 dependencies
标签我们即可导入所需要的工程依赖,在上述已经提到了通过 groupId
,artifactId
与 version
三者即可确定依赖信息。
如下述示例即在当前 Maven
工程中导入 mysql jdbc
驱动依赖。
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
<scope>runtime</scope>
</dependency>
</dependencies>
3. 作用域
在导入依赖时可通过 scope
标签定义依赖的作用域,默认省略时值为 compile
。
依赖作用域可选值及其说明参考下表:
作用域 | 效果 |
---|---|
compile | 编译时需要用到该 JAR 包(默认)。 |
runtime | 编译时不需要,但运行时需要用到。 |
provided | 编译时需要用到,但运行时由 JDK 或某个服务器提供。 |
test | 仅在编译 Test 时需要用到该 JAR 包。 |
system | 搭配 systemPath 用于手动指定 JAR 文件路径。 |
默认寻找依赖是通过对应配置的 Maven
仓库中获取,当配置 scope
为 system
时将会绕过 Maven
仓库管理机制,即可以手动指定依赖路径。
如下述示例中即配置手动读取 D:/repo/demo-1.0-SNAPSHOT.jar
依赖文件。
<dependency>
<groupId>xyz.ibudai</groupId>
<artifactId>demo-1</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>system</scope>
<!-- 手动指定路径 -->
<systemPath>D:/repo/demo-1.0-SNAPSHOT.jar</systemPath>
</dependency>
4. 变量配置
当项目中引入了大量依赖,为了更便捷的统一管理,通常会将引入依赖的版本由变量进行统一配置,从而实现更直观的依赖管理。
通过 properties
标签即可自定义变量配置,然后使用 ${}
引用变量,注意模块间的变量是继承的,子模块可选择重写覆盖。
<properties>
<mysql.version>8.0.30</mysql.version>
<junit.version>4.13.2</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!-- 使用 "${}" 引用上述自定义变量 -->
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies>
5. 依赖排除
在引用多个模块时可能会发生版本兼容冲突问题,通过 excludes
标签即可实现依赖排除。
如下我们在工程中引入了 demo-a
依赖,但其又引用 demo-b
依赖,如想要在当前工程中移除 demo-b
依赖,此时即可通过 excludes
标签将 demo-b
排除依赖。
<dependencies>
<dependency>
<groupId>xyz.ibudai</groupId>
<artifactId>demo-a</artifactId>
<version>1.0.0</version>
<excludes>
<exclude>
<groupId>xyz.ibudai</groupId>
<artifactId>demo-b</artifactId>
<version>1.0.0</version>
</exclude>
</excludes>
</dependency>
</dependencies>
除了手动通过 excludes
标签排除依赖,被引模块也可以在导入依赖时通过 optional
标签禁用依赖传递。
上述示例中若在 demo-a
工程中引入 demo-b
依赖时添加 optional
标签,那么其它工程在引入 demo-a
依赖时将不会将 demo-b
作为间接依赖导入。
<dependencies>
<dependency>
<groupId>xyz.ibudai</groupId>
<artifactId>demo-b</artifactId>
<version>1.0.0</version>
<!-- 默认 false, 设置为 true 禁用依赖传递 -->
<optional>true</optional>
</dependency>
</dependencies>
三、模块配置
1. 模块管理
当我们项目包含多个子项目时,通过 modules
标签即可实现模块管理。
<!-- maven-demo pom.xml -->
<modules>
<module>module-1</module>
<module>module-2</module>
</modules>
如下在 maven-demo
中又包含了 module-1
和 module-2
两个工程。
2. 模块继承
通过 parent
即可标记当前模块的父模块,且子模块将会继承父模块中的所有依赖配置。子模块若没有指定的 groupId
和 version
默认继承父模块中的配置。
其中 relativePath
用于指定父模块的 POM
文件目录,省略时默认值为 ../pom.xml
即当前目录的上一级中,若仍未找到则会在本地仓库中寻找。
<!-- module-1 pom.xml -->
<parent>
<groupId>xyz.ibudai</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>module-1</artifactId>
3. 间接依赖
当项目需要引用到其它依赖时,只需指定所依赖的工程的基本信息即可,剩下的一切都交给 Maven
处理。即便是所要依赖的工程依赖了其它工程,我们也只需引入项目所直接的依赖的工程。
如下图示例中 Dependency-A
引用了 Dependency-B
,而 Dependency-B
又依赖于 Dependency-C
,在传统项目中若在 Dependency-A
中引用 Dependency-B
则需要同时手动添加 Dependency-B
与 Dependency-C
所对应的 JAR
包,但在 Maven
中我们只需要引入 Dependency-B
即可, Mavne
会自动将子模块所依赖的包导入。
(1) 依赖顺序
在 maven
工程中遵循先定义先导入的原则,即当存在多个相同间接依赖,优先导入其父依赖定义在前的简洁依赖。
举个例子,如工程中引入 Dependency-A
与 Dependency-B
两个依赖,二者又分别引用了不同版本的 Dependency-C
,但对于 Maven
而言最终编译时同一个依赖即便是不同的版本也只会选择一份。
其计算规则如下:若 Dependency-A
定义在 Dependency-B
之前则最终将导入 Dependency-A
中的 C-1.0
版本。而在右侧图例中虽然 Dependency-A
引入优先级高于 Dependency-B
,但是 C-2.0
的间接依赖层级高于 C-1.0
,因此将导入 C-2.0
版本。
四、统一管理
1. 导入管理
当一共项目包含多个模块,且多个模块引用了相同依赖时显然重复引用是不太合适的,而通过 dependencyManagement
即可很好的解决依赖共用的问题。
将项目依赖统一定义在父模块的 dependencyManagement
标签中,子模块只需继承父模块并在 dependencies
引入所需的依赖,便可自动读取父模块 dependencyManagement
所指定的版本等信息。
dependencyManagement
既不会在当前模块引入依赖,也不会给其子模块引入依赖,同时其是可以被继承的。只有在子模块下同样声明了该依赖,才会引入到模块中,子模块中只需在依赖中引入 groupId
与 artifactId
即可, 也可以指定版本则会进行覆盖。
2. 模块示例
接下来以下图中的模块层级关系进行举例:
(1) maven-demo
在 maven-demo
的 dependencyManagement
定义 mysql
和 junit
两个依赖。
<groupId>xyz.ibudai</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>maven-1</module>
<module>maven-2</module>
</module>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.30</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
</dependencyManagement>
(2) module-1
在 module-1
中继承 maven-demo
工程,并引入 mysql
,此时无需指定版本将会自动读取父模块中 dependencyManagement
中所指定的版本。当然你也可以选择指定版本,则将会进行覆盖,但并不建议这么操作,将提高项目维护难度。
module-1
的 pom
文件内容如下:
<parent>
<groupId>xyz.ibudai</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>maven-1</artifactId>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
(3) module-2
在 module-2
配置同 module-1
,通过 dependencyManagement
实现了项目依赖版本的统一管理。
<parent>
<groupId>xyz.ibudai</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>maven-2</artifactId>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
3. 模块导入
上面介绍了如何通过 dependencyManagement
实现全局的依赖版本管理,但如果工程中的两个子模块都需要配置相同的 dependencyManagement
配置时,既可以选择通过继承方式实现,也可以直接复制粘贴一份。
除此之外,Maven
提供了一种更便捷的方式快速导入。在上述的 maven-demo
创建同级模块 maven-demo1
,如果要实现 maven-demo
中配置的 dependencyManagement
则在其 dependencyManagement
配置中导入 maven-demo
即可,配置示例如下:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>xyz.ibudai</groupId>
<artifactId>maven-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 导入目标模块的 dependencyManagement -->
<!-- 依赖范围为 import -->
<scope>import</scope>
<!-- 类型一般为 pom -->
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>
五、插件管理
经过前面的介绍相信对于 Maven
你已经有了一个初步的了解,但 Maven
除了依赖管理之外提供一系列强大的插件,插件对于 Maven
而言可谓时左膀右臂但却经常被人忽略。
今天就让我介绍一下 Maven 中常用的构建插件。
1. Jar
在使用 Java
开发时通常情况下我们都会将工程打包为 JAR
文件,首先了解一下 JAR
的文件结构。
下图即为通过 Maven
打包后的 JAR
文件,其中 org.example
目录为工程中定义的包名,存在编译后的 .class
文件, META-INF
目录用于存放工程的元数据信息。
如上图中 META-INF
下的 MANIFEST.MF
文件内容如下:
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Built-By: great
Created-By: Apache Maven 3.6.3
Build-Jdk: 1.8.0_202
而通过 maven-jar-plugin
插件我们即可在添加额外信息至打包后的 JAR
文件,插件配置信息如下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>2.3.1</version>
<configuration>
<archive>
<manifest>
<!-- 替换为主类完整限定名 -->
<mainClass>fully.qualified.MainClass</mainClass>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
<!-- 配置额外属性信息 -->
<manifestEntries>
<Plugin-Id>demo-plugin</Plugin-Id>
<Plugin-Version>1.0.0</Plugin-Version>
</manifestEntries>
</archive>
</configuration>
</plugin>
在之前的工程 POM
文件中添加上述构建插件重新进行打包,可以看到 MANIFEST.MF
文件中即添加了我们配置的额外属性。
Manifest-Version: 1.0
Archiver-Version: Plexus Archiver
Created-By: Apache Maven
Built-By: great
Build-Jdk: 1.8.0_202
# Specification entries
Specification-Title: maven-v1
Specification-Version: 1.0-SNAPSHOT
# Implementation entries
Implementation-Title: maven-v1
Implementation-Version: 1.0-SNAPSHOT
Implementation-Vendor-Id: org.example
# Manifest
Main-Class: org.example.MyTest
# ManifestEntries
Plugin-Id: demo-plugin
Plugin-Version: 1.0.0
2. Assembly
在普通 Maven
工程打包时默认仅会编译工程中新建的 java
文件并存储其 .class
文件,对于 POM
文件中引用的第三方依赖并不会一同打包。
如新建一个 Maven
工程并在依赖中导入 Jackson
依赖库并进行打包编译,可以看到下图编译后的 JAR
文件中只有工程中新建的 MyTest.class
文件,项目中所导入的依赖并没有被一起打包。
而通过 assembly
插件即可将 POM
配置中的所有依赖一同打包编译至 JAR
文件中。
其中 execution
标签定义了 assembly
插件的作用阶段,如这里设置了在 Maven package
即打包阶段生效。
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<!-- Set jar file name -->
<finalName>${project.artifactId}-${project.version}-all</finalName>
<!-- disabled default file name suffix of "jar-with-dependencies" -->
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<!-- Add all dependency to jar file -->
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
<attach>false</attach>
<archive>
<manifest>
<!-- replace to your main class name -->
<mainClass>fully.qualified.MainClass</mainClass>
<addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
<addDefaultImplementationEntries>true</addDefaultImplementationEntries>
</manifest>
</archive>
</configuration>
<executions>
<execution>
<!-- Set effect phase -->
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
在工程 POM
配置中添加上述信息并重新编译打包工程,可以看到此时 JAR
文件中除了自定义创建的 MyTest.clss
文件外同时包含了依赖的第三方库。
3. Shade
Shade
插件的功能更为强大,其提供了两个功能:第一个即与 assembly
类似可实现依赖的打包编译,与 assembly
不同的是 Shade
提供了更灵活的执行策略,可指定需要打包编译的依赖集合。
另一个即实现包的重命名功能,我们都知道 Maven
并不允许在一共工程中同时引入单个依赖的不同版本,而通过 Shade
插件即可实现二次包装从而绕开该限制。
下面介绍一个 Shade
插件中各标签的使用。
(1) artifactSet
通过 includes
标签可以指定需要一同打包编译的第三方依赖。
定义的格式为:groupId:artifactId
。
<artifactSet>
<includes>
<include>groupId:artifactId</include>
</includes>
</artifactSet>
(2) relocations
通过 relocations
标签即可实现模块的重命名功能。
其中 pattern
为需要重命名的模块包, shadedPattern
为重命名后的模块名。
<relocations>
<relocation>
<pattern>old.package.name</pattern>
<shadedPattern>new.package.name</shadedPattern>
</relocation>
</relocations>
(3) filters
通过 filters
标签可以实现非必要文件的排除,如常见的协议文件等,可通过文件名或类型实现匹配。
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>filename</exclude>
<exclude>file pattern</exclude>
</excludes>
</filter>
</filters>
(4) 完整配置
Shade
同样可以通过 execution
设置作用阶段,上述介绍标签的完整配置内容如下:
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.0</version>
<executions>
<!-- Working phase -->
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<configuration>
<minimizeJar>true</minimizeJar>
<!-- Defined what dependencies to pull into the uber JAR -->
<artifactSet>
<includes>
<include>com.fasterxml.jackson.core:jackson-core</include>
</includes>
</artifactSet>
<!-- Rename the package -->
<relocations>
<relocation>
<!-- Old name -->
<pattern>com.fasterxml.jackson.core</pattern>
<!-- New name -->
<shadedPattern>com.ibudai.fasterxml.jackson.core</shadedPattern>
</relocation>
</relocations>
<!-- Exclude the file that didn't want -->
<filters>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/license/**</exclude>
<exclude>META-INF/*</exclude>
<exclude>LICENSE</exclude>
<exclude>NOTICE</exclude>
</excludes>
</filter>
</filters>
</configuration>
</plugin>
</plugins>
在之前的工程中添加上述配置并重新打包,可以看到编译后的 Jackson
模块包层级已经变成我们自定义的内容,而 Java
的类加载即通过类的完成限定名(包名+类名)来区分是否为同一个类,因此通过 Shade
插件即可实现 Maven
的单一工程多版本引入。
六、构建配置
在上面介绍了工程的依赖管理与多模块的管理配置,下面介绍一下工程打包构建时涉及的配置。
注意以下所有配置项都是定义在 <build>
标签组内,下述不再重复说明。
1. 版本指定
在 <plugin>
标签内可指定工程打包编译时使用的 JDK
版本,可根据服务器环境手动修改版本。
<plugins>
<plugin>
<!-- 编译时使用 JDK 版本 -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
除了上述方式,下述配置与上述配置等价。
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
2. 文件排除
默认项目打包后 /resources
目录下文件都将统一打包进编译后的 JAR
文件,但为了方便配置修改通常将配置文件排除打包,使用时只需将文件放置于 JAR
同级即可。
如下示例中将 application.yml
文件排除打包,后续若需要修改配置无需重新打包只需重启项目即可。
<resources>
<resource>
<!-- 设置编译去除 yml 配置文件 -->
<directory>src/main/resources</directory>
<excludes>
<exclude>application.yml</exclude>
</excludes>
</resource>
</resources>
3. 主类配置
在打包时可能出现无法识别工程主类的问题,导致编译后的文件无法正常运行,此时则可以在 pom
文件中手动设置工程的主类。
其中 <mainClass>
中配置的为项目主类的完成限定名。
<plugins>
<plugin>
<!-- 设置工程主类路径, 可略去 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>xyz.ibudai.TestWebApplication</mainClass>
<layout>JAR</layout>
</configuration>
</plugin>
</plugins>
七、模块编译
1. 构建命令
在执行编译构建时通常我们会使用 IDEA
插件等方式执行,当然也可手动执行编译命令。
需要注意的一点是通过下述命令执行需要在系统中配置 Maven
的环境变量,常用的编译命令如下:
# 清除已下载打包文件
mvn clean
# 下载工程依赖文件
mvn install
# 打包工程模块为可执行文件
mvn package
# 推送模块至配置的远程仓库
mvn deploy
2. 编译参数
除了上述提到的编译命令,在执行时可通过 -D
添加编译参数,如跳过测试编译等等。
# 跳过单元测试
mvn clean install -DskipTests
# 跳过所有测试检查
mvn clean install -Dmaven.test.skip=true
# 跳过 enforcer 检查
mvn clean install -Denforcer.skip
# 跳过 Checkstyle 检查
mvn clean install -Dcheckstyle.skip
在 Maven
中,参数传递时是否需要设置 =true
取决于 Maven
插件或构建脚本中对这些属性的处理方式,通常在没有时设置属性值为 true
。
# 下述两个命令生效结果是一致的。
mvn clean install -Denforcer.skip
mvn clean install -Denforcer.skip=true