超级详细的 Maven 教程(基础+高级)

2022-10-28 15:38:31 浏览数 (1)

0.为什么使用Maven这样的构建工具【why】

① 一个项目就是一个工程

如果项目非常庞大,就不适合使用package来划分模块,最好是每一个模块对应一个工程,利于分工协作。借助于maven就可以将一个项目拆分成多个工程

② 项目中使用jar包,需要“复制”、“粘贴”项目的lib中

同样的jar包重复的出现在不同的项目工程中,你需要做不停的复制粘贴的重复工作。借助于maven,可以将jar包保存在本地meven“仓库”中,不管在哪个项目只要使用引用即可就行。

③ jar包需要的时候每次都要自己准备好或到官网下载

借助于maven我们可以使用统一的规范方式下载jar包.

④ jar包版本不一致的风险

不同的项目在使用jar包的时候,有可能会导致各个项目的jar包版本不一致,导致未执行错误。借助于maven,所有的jar包都放在“仓库”中,所有的项目都使用仓库的一份jar包。

⑤ 一个jar包依赖其他的jar包需要自己手动的加入到项目中

FileUpload组件->IO组件,commons-fileupload-1.3.jar依赖于commons-io-2.0.1.jar

极大的浪费了我们导入包的时间成本,也极大的增加了学习成本。借助于maven,它会自动的将依赖的jar包导入进来。

1. Maven 是什么

Maven 是 Apache 软件基金会组织维护的一款专门为 Java 项目提供构建依赖管理支持的工具。

一个 Maven 工程有约定的目录结构,约定的目录结构对于 Maven 实现自动化构建而言是必不可少的一环,就拿自动编译来说,Maven 必须 能找到 Java 源文件,下一步才能编译,而编译之后也必须有一个准确的位置保持编译得到的字节码文件。 我们在开发中如果需要让第三方工具或框架知道我们自己创建的资源在哪,那么基本上就是两种方式:

  1. 通过配置的形式明确告诉它
  2. 基于第三方工具或框架的约定 Maven 对工程目录结构的要求

1.1 构建

Java 项目开发过程中,构建指的是使用『原材料生产产品』的过程。

构建过程主要包含以下环节:

1.2 依赖

Maven 中最关键的部分,我们使用 Maven 最主要的就是使用它的依赖管理功能。当 A jar 包用到了 B jar 包中的某些类时,A 就对 B 产生了依赖,那么我们就可以说 A 依赖 B。

依赖管理中要解决的具体问题:

  • jar 包的下载:使用 Maven 之后,jar 包会从规范的远程仓库下载到本地
  • jar 包之间的依赖:通过依赖的传递性自动完成
  • jar 包之间的冲突:通过对依赖的配置进行调整,让某些 jar 包不会被导入

2. Maven 开发环境配置

2.1 下载安装

首页:

Maven – Welcome to Apache Maven

下载页面:

Maven – Download Apache Maven

或者你也可以选择之前的版本:

然后里面选择自己对应的版本下载即可:

下载之后解压到非中文、没有空格的目录,如下:

2.2 指定本地仓库

本地仓库默认值:用户家目录/.m2/repository。由于本地仓库的默认位置是在用户的家目录下,而家目录往往是在 C 盘,也就是系统盘。将来 Maven 仓库中 jar 包越来越多,仓库体积越来越大,可能会拖慢 C 盘运行速度,影响系统性能。所以建议将 Maven 的本地仓库放在其他盘符下。配置方式如下:

代码语言:javascript复制
<!-- localRepository
| The path to the local repository maven will use to store artifacts.
|
| Default: ${user.home}/.m2/repository
<localRepository>/path/to/local/repo</localRepository>
-->
<localRepository>D:softwaremaven-repository</localRepository>

本地仓库这个目录,我们手动创建一个空的目录即可。

记住:一定要把 localRepository 标签从注释中拿出来

注意:本地仓库本身也需要使用一个非中文、没有空格的目录。

2.3 配置镜像仓库

Maven 下载 jar 包默认访问境外的中央仓库,而国外网站速度很慢。改成镜像仓库,访问国内网站,可以让 Maven 下载 jar 包的时候速度更快。配置的方式是:

将原有的例子配置注释掉

代码语言:javascript复制
<!-- <mirror>
  <id>maven-default-http-blocker</id>
  <mirrorOf>external:http:*</mirrorOf>
  <name>Pseudo repository to mirror external repositories initially using HTTP.</name>
  <url>http://0.0.0.0/</url>
  <blocked>true</blocked>
</mirror> -->

加入自己的配置

代码语言:javascript复制
<mirror>
    <id>nexus-aliyun</id>
    <mirrorOf>central</mirrorOf>
    <name>Nexus aliyun</name>
    <url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>

2.4 配置基础 JDK 版本

如果按照默认配置运行,Java 工程使用的默认 JDK 版本是 1.5,而我们熟悉和常用的是 JDK 1.8 版本。修改配置的方式是:将 profile 标签整个复制到 settings.xml 文件的 profiles 标签内。

代码语言:javascript复制
<profile>
    <id>jdk-1.8</id>
    <activation>
        <activeByDefault>true</activeByDefault>
        <jdk>1.8</jdk>
    </activation>
    <properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
       <maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
    </properties>
</profile>

2.5 配置环境变量

Maven 是一个用 Java 语言开发的程序,它必须基于 JDK 来运行,需要通过 JAVA_HOME 来找到 JDK 的安装位置。

可以使用下面的命令验证:

代码语言:javascript复制
C:UsersAdministrator>echo %JAVA_HOME%
D:softwareJava
C:UsersAdministrator>java -version
java version "1.8.0_141"
Java(TM) SE Runtime Environment (build 1.8.0_141-b15)
Java HotSpot(TM) 64-Bit Server VM (build 25.141-b15, mixed mode)

然后新建环境变量:

配置环境变量的规律: XXX_HOME 通常指向的是 bin 目录的上一级 PATH 指向的是 bin 目录

在配置 PATH

通过 mvn -v 验证:

代码语言:javascript复制
C:UsersAdministrator>mvn -v
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T00:41:47 08:00)
Maven home: D:softwareapache-maven-3.3.9bin..
Java version: 1.8.0_333, vendor: Oracle Corporation
Java home: D:softwarejdk1.8jre
Default locale: zh_CN, platform encoding: GBK
OS name: "windows 11", version: "10.0", arch: "amd64", family: "dos"

3. Maven 的使用

3.1 核心概念:坐标

数学中的坐标使用 x、y、z 三个『向量』作为空间的坐标系,可以在『空间』中唯一的定位到一个『』。

Maven中的坐标使用三个『向量』在『Maven的仓库』中唯一的定位到一个『jar』包。

  • groupId:公司或组织的 id,即公司或组织域名的倒序,通常也会加上项目名称

例如:groupId:com.tofacebook.maven

  • artifactId:一个项目或者是项目中的一个模块的 id,即模块的名称,将来作为 Maven 工程的工程名就是:module的名称

例如:artifactId:auth

  • version:版本号

例如:version:1.0.0

提示:坐标和仓库中 jar 包的存储路径之间的对应关系,如下

代码语言:javascript复制
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>

上面坐标对应的 jar 包在 Maven 本地仓库中的位置:

代码语言:javascript复制
Maven本地仓库根目录javaxservletservlet-api2.5servlet-api-2.5.jar

3.2 pom.xml

POM:Project Object Model,项目对象模型。和 POM 类似的是:DOM(Document Object Model),文档对象模型。它们都是模型化思想的具体体现。

POM 表示将工程抽象为一个模型,再用程序中的对象来描述这个模型。这样我们就可以用程序来管理项目了。我们在开发过程中,最基本的做法就是将现实生活中的事物抽象为模型,然后封装模型相关的数据作为一个对象,这样就可以在程序中计算与现实事物相关的数据。

POM 理念集中体现在 Maven 工程根目录下 pom.xml 这个配置文件中。所以这个 pom.xml 配置文件就是 Maven 工程的核心配置文件。其实学习 Maven 就是学这个文件怎么配置,各个配置有什么用。

代码语言:javascript复制
<!-- 当前Maven工程的坐标 -->
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<!-- 当前Maven工程的打包方式,可选值有下面三种: -->
<!-- jar:表示这个工程是一个Java工程  -->
<!-- war:表示这个工程是一个Web工程 -->
<!-- pom:表示这个工程是“管理其他工程”的工程 -->
<packaging>jar</packaging>
<properties>
    <!-- 工程构建过程中读取源码时使用的字符集 -->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- 当前工程所依赖的jar包 -->
<dependencies>
    <!-- 使用dependency配置一个具体的依赖 -->
    <dependency>
        <!-- 在dependency标签内使用具体的坐标依赖我们需要的一个jar包 -->
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <!-- scope标签配置依赖的范围 -->
        <scope>test</scope>
    </dependency>
</dependencies>

3.3 依赖

上面说到我们使用 Maven 最主要的就是使用它的依赖管理功能,引入依赖存在一个范围,maven的依赖范围包括: compileprovideruntimetestsystem

  • compile:表示编译范围,指 A 在编译时依赖 B,该范围为默认依赖范围。编译范围的依赖会用在编译,测试,运行,由于运行时需要,所以编译范围的依赖会被打包。
  • provided:provied 依赖只有当 jdk 或者一个容器已提供该依赖之后才使用。provide 依赖在编译和测试时需要,在运行时不需要。例如:servlet api被Tomcat容器提供了。
  • runtime:runtime 依赖在运行和测试系统时需要,但在编译时不需要。例如:jdbc 的驱动包。由于运行时需要,所以 runtime 范围的依赖会被打包。
  • test:test 范围依赖在编译和运行时都不需要,只在测试编译和测试运行时需要。例如:Junit。由于运行时不需要,所以 test 范围依赖不会被打包。
  • system:system 范围依赖与 provide 类似,但是必须显示的提供一个对于本地系统中 jar 文件的路径。一般不推荐使用。

依赖范围

编译

测试

运行时

是否会被打入jar包

compile

provided

×

×

runtime

×

test

×

×

×

system

×

而在实际开发中,我们常用的就是 compiletestprovided

3.4 依赖的传递

A 依赖 B,B 依赖 C,那么在 A 没有配置对 C 的依赖的情况下,A 里面能不能直接使用 C?

再以上的前提下,C 是否能够传递到 A,取决于 B 依赖 C 时使用的依赖范围。

  • B 依赖 C 时使用 compile 范围:可以传递
  • B 依赖 C 时使用 test 或 provided 范围:不能传递,所以需要这样的 jar 包时,就必须在需要的地方明确配置依赖才可以。

3.5 依赖的排除

当 A 依赖 B,B 依赖 C 而且 C 可以传递到 A 的时候,A 不想要 C,需要在 A 里面把 C 排除掉。而往往这种情况都是为了避免 jar 包之间的冲突。

所以配置依赖的排除其实就是阻止某些 jar 包的传递。因为这样的 jar 包传递过来会和其他 jar 包冲突。

一般通过使用excludes标签配置依赖的排除:

代码语言:javascript复制
<dependency>
    <groupId>net.javatv.maven</groupId>
    <artifactId>auth</artifactId>
    <version>1.0.0</version>
    <scope>compile</scope>
    
    <!-- 使用excludes标签配置依赖的排除    -->
    <exclusions>
        <!-- 在exclude标签中配置一个具体的排除 -->
        <exclusion>
            <!-- 指定要排除的依赖的坐标(不需要写version) -->
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
        </exclusion>
    </exclusions>
</dependency>

3.6 继承

3.6.1 概念

Maven工程之间,A 工程继承 B 工程

  • B 工程:父工程
  • A 工程:子工程

本质上是 A 工程的 pom.xml 中的配置继承了 B 工程中 pom.xml 的配置。

3.6.2 作用

在父工程中统一管理项目中的依赖信息,具体来说是管理依赖信息的版本。

它的背景是:

  • 对一个比较大型的项目进行了模块拆分。
  • 一个 project 下面,创建了很多个 module。
  • 每一个 module 都需要配置自己的依赖信息。

它背后的需求是:

  • 在每一个 module 中各自维护各自的依赖信息很容易发生出入,不易统一管理。
  • 使用同一个框架内的不同 jar 包,它们应该是同一个版本,所以整个项目中使用的框架版本需要统一。
  • 使用框架时所需要的 jar 包组合(或者说依赖信息组合)需要经过长期摸索和反复调试,最终确定一个可用组合。这个耗费很大精力总结出来的方案不应该在新的项目中重新摸索。

通过在父工程中为整个项目维护依赖信息的组合既保证了整个项目使用规范、准确的 jar 包;又能够将以往的经验沉淀下来,节约时间和精力。

3.6.3Maven在IDEA中的应用
1. 在idea中设置maven

idea中内置maven,但一般不用,因为用内置修改maven的设置不方便

因此使用自己安装的maven,需要覆盖默认配置,也就是指定maven的安装位置等信息

其中:Setting选项是用来设置本项目的Maven的

Other Setting是设置以后建立的项目的Maven的

目录如下:

进入Setting项,可以看到有三个项目管理工具

其中,框选的两个项目工具最常用,Gradle是最新开发的项目管理工具,功能更强但应用较少。

打开Maven,框选的三个分别是Maven工具的存放路径、配置文件的存放路径、本地仓库(target文件)的存放路径

2. maven-Runner选项的配置

1. 变得更快

接下来进入Runner选项,配置vm项,可以让maven创建的更快

原本的maven默认下载一个模板文件,有7M,下载很慢,为了不让他下载,就需要在vmOption中进行配置,禁用相关的下载。

代码语言:javascript复制
-DarchetypeCatalog=internal

2. 自动刷新功能

若想让Maven自动刷新,即一旦更新pom.xm文件,Maven项目就自动刷新,只需勾选红框内的选项即可

3. 对新创建的项目的Maven配置

操作与对本项目进行Maven配置一致


4. Maven创建Java项目

需要选择的Maven模板:

使用普通Java项目模板创建即可


5.Maven创建Web项目

需要选择的Maven模板:

3.6.4 创建父子工程

① 一般在模块化开发中一般都会创建一个父工程,如下

父工程创建好之后,要修改它的打包方式:

代码语言:javascript复制
<!-- 当前工程作为父工程,它要去管理子工程,所以打包方式必须是 pom -->
<packaging>pom</packaging>

只有打包方式为 pom 的 Maven 工程能够管理其他 Maven 工程。打包方式为 pom 的 Maven 工程中不写业务代码,它是专门管理其他 Maven 工程的工程,所以可以将生成的 src 目录删除。

② 创建模块工程

然后可以再父工程的 pom 文件中看到:

子工程的 pom 如下:

③ 在父工程中配置依赖的统一管理

使用dependencyManagement标签配置对依赖的管理,如下:

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>net.javatv.maven</groupId>
    <artifactId>maven-demo-parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <modules>
        <module>demo-module</module>
    </modules>

    <!-- 引入spirng5 的依赖 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>5.3.19</version>
            </dependency>

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>5.3.19</version>
            </dependency>

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>5.3.19</version>
            </dependency>

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>5.3.19</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

而实际上被管理的依赖并没有真正被引入到工程

④ 子工程中引用那些被父工程管理的依赖

关键点:省略版本号

子工程引用父工程中的依赖信息时,可以把版本号去掉。把版本号去掉就表示子工程中这个依赖的版本由父工程决定,具体来说是由父工程的dependencyManagement来决定。

子工程 pom 如下:

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <!-- 使用parent标签指定当前工程的父工程 -->
    <parent>
        <artifactId>maven-demo-parent</artifactId>
        <groupId>net.javatv.maven</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <!-- 子工程的坐标 -->
    <!-- 如果子工程坐标中的groupId和version与父工程一致,那么可以省略 -->
    <artifactId>demo-module</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
        </dependency>
    </dependencies>
    
</project>

此时,被管理的依赖才被引入到工程

⑤ 修改父工程依赖信息的版本

这个修改可以是降级,也可以是升级,但一般来说都是升级。

⑥ 父工程中声明自定义属性

对同一个框架的一组 jar 包最好使用相同的版本,为了方便升级框架,可以将 jar 包的版本信息统一提取出来,统一声明版本号 :

代码语言:javascript复制
<!-- 通过自定义属性,统一指定Spring的版本 -->
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <!-- 自定义标签,维护Spring版本数据 -->
    <spring.version>5.3.19</spring.version>
</properties>

在需要的地方使用${}的形式来引用自定义的属性名,真正实现一处修改,处处生效

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>net.javatv.maven</groupId>
    <artifactId>maven-demo-parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>

    <modules>
        <module>demo-module</module>
    </modules>


    <!-- 通过自定义属性,统一指定Spring的版本 -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- 自定义标签,维护Spring版本数据 -->
        <spring.version>5.3.19</spring.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-beans</artifactId>
                <version>${spring.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-core</artifactId>
                <version>${spring.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-context</artifactId>
                <version>${spring.version}</version>
            </dependency>

            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-aop</artifactId>
                <version>${spring.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

</project>

编写一套符合要求、开发各种功能都能正常工作的依赖组合并不容易。如果公司里已经有人总结了成熟的组合方案,那么再开发新项目时,如果不使用原有的积累,而是重新摸索,会浪费大量的时间。为了提高效率,我们可以使用工程继承的机制,让成熟的依赖组合方案能够保留下来。如下:

如上图所示,公司级的父工程中管理的就是成熟的依赖组合方案,各个新项目、子系统各取所需即可。

3.7 聚合

聚合,指分散的聚集到一起,即部分组成整体。

3.7.1 Maven 中的聚合

使用一个总工程将各个模块工程汇集起来,作为一个整体对应完整的项目,实际就是 module 标签。

  • 项目:整体
  • 模块:部分
3.7.2 继承和聚合的对应关系

从继承关系角度来看:

  • 父工程
  • 子工程

从聚合关系角度来看:

  • 总工程
  • 模块工程
3.7.3 聚合的配置

在总工程中配置 modules 即可:

代码语言:javascript复制
<modules>
    <module>demo-module</module>
</modules>
3.7.4 依赖循环问题

如果 A 工程依赖 B 工程,B 工程依赖 C 工程,C 工程又反过来依赖 A 工程,那么在执行构建操作时会报下面的错误:

代码语言:javascript复制
DANGER
[ERROR] [ERROR] The projects in the reactor contain a cyclic reference:

这个错误的含义是:循环引用。

3.8 全局变量

全局变量的定义格式:${变量名}

对于:自定义全局变量,一般用来自定义版本号。做法是:先使用全局变量定义,再使用${变量名}


3.9 指定资源插件

一般来说,如果不指定任何资源插件,src/main/javasrc/test/java两个目录中的所有*.java文件会被编译,并将结果分别存放到target/classestarge/test-classes目录中

但是这两个目录中的其他文件都会被忽略掉,因此,如果我们需要使用其他文件,在运行时就会报错。 解决办法是指定资源插件,将以下内容放到<buid>标签中,将其他文件也进行编译

代码语言:javascript复制
<build>
    <!--把src/main/java目录中的.xml文件包含到输出结果中,输出到classes目录中-->
    <resources>
        <resource>
            <directory>src/main/java</directory> <!--所在的目录-->
            <includes> <!--包括目录下的.properties.xml文件都会扫描到-->
                <include>**/*.properties</include>
                <include>**/*.xml</include>
            </includes>
            <!--表示不用过滤器,因为.property已经起到过滤的作用了-->
            <filtering>false</filtering>
        </resource>
    </resources>
</build>

4. build 标签

在实际使用 Maven 的过程中,我们会发现 build 标签有时候有,有时候没,这是怎么回事呢?其实通过有效 POM 我们能够看到,build 标签的相关配置其实一直都在,只是在我们需要定制构建过程的时候才会通过配置 build 标签覆盖默认值或补充配置。这一点我们可以通过打印有效 POM 来看到。

打印有效 pom mvn help:effective-pom

当默认配置无法满足需求的定制构建的时候,就需要使用 build 标签。

4.1 build 标签的组成

build 标签的子标签大致包含三个主体部分:

4.1.1 定义约定的目录结构
代码语言:javascript复制
<sourceDirectory>D:productmaven-demo-parentdemo-modulesrcmainjava</sourceDirectory>
<scriptSourceDirectory>D:productmaven-demo-parentdemo-modulesrcmainscripts</scriptSourceDirectory>
<testSourceDirectory>D:productmaven-demo-parentdemo-modulesrctestjava</testSourceDirectory>
<outputDirectory>D:productmaven-demo-parentdemo-moduletargetclasses</outputDirectory>
<testOutputDirectory>D:productmaven-demo-parentdemo-moduletargettest-classes</testOutputDirectory>
<resources>
    <resource>
        <directory>D:productmaven-demo-parentdemo-modulesrcmainresources</directory>
    </resource>
</resources>
<testResources>
    <testResource>
        <directory>D:productmaven-demo-parentdemo-modulesrctestresources</directory>
    </testResource>
</testResources>
<directory>D:productmaven-demo-parentdemo-moduletarget</directory>
<finalName>demo-module-1.0-SNAPSHOT</finalName>

各个目录的作用如下:

目录名

作用

sourceDirectory

主体源程序存放目录

scriptSourceDirectory

脚本源程序存放目录

testSourceDirectory

测试源程序存放目录

outputDirectory

主体源程序编译结果输出目录

testOutputDirectory

测试源程序编译结果输出目录

resources

主体资源文件存放目录

testResources

测试资源文件存放目录

directory

构建结果输出目录

4.1.2 备用插件管理

pluginManagement 标签存放着几个极少用到的插件:

  • maven-antrun-plugin
  • maven-assembly-plugin
  • maven-dependency-plugin
  • maven-release-plugin

通过 pluginManagement 标签管理起来的插件就像 dependencyManagement 一样,子工程使用时可以省略版本号,起到在父工程中统一管理版本的效果。

4.1.3 生命周期插件

plugins 标签存放的是默认生命周期中实际会用到的插件,这些插件想必大家都不陌生,所以抛开插件本身不谈,plugin 标签的结构如下:

代码语言:javascript复制
<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.1</version>
    <executions>
        <execution>
            <id>default-compile</id>
            <phase>compile</phase>
            <goals>
                <goal>compile</goal>
            </goals>
        </execution>
        <execution>
            <id>default-testCompile</id>
            <phase>test-compile</phase>
            <goals>
                <goal>testCompile</goal>
            </goals>
        </execution>
    </executions>
</plugin>

① 坐标部分

artifactId 和 version 标签定义了插件的坐标,作为 Maven 的自带插件这里省略了 groupId。

② 执行部分

executions 标签内可以配置多个 execution 标签,execution 标签内:

  • id:指定唯一标识
  • phase:关联的生命周期阶段
  • goals/goal:关联指定生命周期的目标

goals 标签中可以配置多个 goal 标签,表示一个生命周期环节可以对应当前插件的多个目标。

4.2 典型应用:指定 JDK 版本

前面我们在 settings.xml 中配置了 JDK 版本,那么将来把 Maven 工程部署都服务器上,脱离了 settings.xml 配置,如何保证程序正常运行呢?思路就是我们直接把 JDK 版本信息告诉负责编译操作的 maven-compiler-plugin 插件,让它在构建过程中,按照我们指定的信息工作。如下:

代码语言:javascript复制
<!-- build 标签:意思是告诉 Maven,你的构建行为,我要开始定制了! -->
<build>
    <!-- plugins 标签:Maven 你给我听好了,你给我构建的时候要用到这些插件! -->
    <plugins>
        <!-- plugin 标签:这是我要指定的一个具体的插件 -->
        <plugin>
            <!-- 插件的坐标。此处引用的 maven-compiler-plugin 插件不是第三方的,是一个 Maven 自带的插件。 -->
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.1</version>
            
            <!-- configuration 标签:配置 maven-compiler-plugin 插件 -->
            <configuration>
                <!-- 具体配置信息会因为插件不同、需求不同而有所差异 -->
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
    </plugins>
</build>
  • settings.xml 中配置:仅在本地生效,如果脱离当前 settings.xml 能够覆盖的范围,则无法生效。
  • 在当前 Maven 工程 pom.xml 中配置:无论在哪个环境执行编译等构建操作都有效。

4.3 典型应用:SpringBoot 定制化打包

很显然 spring-boot-maven-plugin 并不是 Maven 自带的插件,而是 SpringBoot 提供的,用来改变 Maven 默认的构建行为。具体来说是改变打包的行为。默认情况下 Maven 调用 maven-jar-plugin 插件的 jar 目标,生成普通的 jar 包。

普通 jar 包没法使用 java -jar xxx.jar 这样的命令来启动、运行,但是 SpringBoot 的设计理念就是每一个『微服务』导出为一个 jar 包,这个 jar 包可以使用 java -jar xxx.jar 这样的命令直接启动运行。

这样一来,打包的方式肯定要进行调整。所以 SpringBoot 提供了 spring-boot-maven-plugin 这个插件来定制打包行为。

代码语言:javascript复制
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.5.5</version>
        </plugin>
    </plugins>
</build>

5. 依赖配置补充

管理依赖最基本的办法是继承父工程,但是和 Java 类一样,Maven 也是单继承的。如果不同体系的依赖信息封装在不同 POM 中了,没办法继承多个父工程怎么办?这时就可以使用 import 依赖范围。

5.1 import

典型案例当然是在项目中引入 SpringBoot、SpringCloud 依赖:

代码语言:javascript复制
<dependencyManagement>
    <dependencies>
        <!-- SpringCloud 微服务 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
        
        <!-- SpringCloud Alibaba 微服务 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>${spring-cloud-alibaba.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

import 依赖范围使用要求:

  • 打包类型必须是 pom
  • 必须放在 dependencyManagement 中

官网说明如下: This scope is only supported on a dependency of type pom in the <dependencyManagement> section. It indicates the dependency is to be replaced with the effective list of dependencies in the specified POM’s <dependencyManagement> section. Since they are replaced, dependencies with a scope of import do not actually participate in limiting the transitivity of a dependency.

5.2 system

以 Windows 系统环境下开发为例,假设现在 D:productmaven-demo-parentdemo-moduletargetdemo-module-1.0-SNAPSHOT.jar 想要引入到我们的项目中,此时我们就可以将依赖配置为 system 范围:

代码语言:javascript复制
<dependency>
    <groupId>net.javatv.maven</groupId>
    <artifactId>demo-module</artifactId>
    <version>1.0-SNAPSHOT</version>
    <systemPath>D:productmaven-demo-parentdemo-moduletargetdemo-module-1.0-SNAPSHOT.jar</systemPath>
    <scope>system</scope>
</dependency>

但是很明显:这样引入依赖完全不具有可移植性,所以不要使用

5.3 runtime

专门用于编译时不需要,但是运行时需要的 jar 包。比如:编译时我们根据接口调用方法,但是实际运行时需要的是接口的实现类。典型案例是:

代码语言:javascript复制
<!--热部署 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

6. profile

6.1 profile 概述

这里我们可以对接 profile 这个单词中『侧面』这个含义:项目的每一个运行环境,相当于是项目整体的一个侧面。

通常情况下,我们项目至少有三种运行环境:

  • 开发环境:供不同开发工程师开发的各个模块之间互相调用、访问;内部使用
  • 测试环境:供测试工程师对项目的各个模块进行功能测试;内部使用
  • 生产环境:供最终用户访问——所以这是正式的运行环境,对外提供服务

而我们这里的『环境』仍然只是一个笼统的说法,实际工作中一整套运行环境会包含很多种不同服务器:

  • MySQL
  • Redis
  • ElasticSearch
  • RabbitMQ
  • FastDFS
  • Nginx
  • Tomcat
  • ……

就拿其中的 MySQL 来说,不同环境下的访问参数肯定完全不同,可是代码只有一套。如果在 jdbc.properties 里面来回改,那就太麻烦了,而且很容易遗漏或写错,增加调试的难度和工作量。所以最好的办法就是把适用于各种不同环境的配置信息分别准备好,部署哪个环境就激活哪个配置。

在 Maven 中,使用 profile 机制来管理不同环境下的配置信息。但是解决同类问题的类似机制在其他框架中也有,而且从模块划分的角度来说,持久化层的信息放在构建工具中配置也违反了『高内聚,低耦合』的原则。

实际上,即使我们在 pom.xml 中不配置 profile 标签,也已经用到 profile了。为什么呢?因为根标签 project 下所有标签相当于都是在设定默认的 profile。这样一来我们也就很容易理解下面这句话:project 标签下除了 modelVersion 和坐标标签之外,其它标签都可以配置到 profile 中。

6.2 profile 配置

6.2.1 外部视角:配置文件

从外部视角来看,profile 可以在下面两种配置文件中配置:

  • settings.xml:全局生效。其中我们最熟悉的就是配置 JDK 1.8。
  • pom.xml:当前 POM 生效
6.2.2 内部实现:具体标签

从内部视角来看,配置 profile 有如下语法要求:

① profiles/profile 标签

  • 由于 profile 天然代表众多可选配置中的一个所以由复数形式的 profiles 标签统一管理。
  • 由于 profile 标签覆盖了 pom.xml 中的默认配置,所以 profiles 标签通常是 pom.xml 中的最后一个标签。

② id 标签

每个 profile 都必须有一个 id 标签,指定该 profile 的唯一标识。这个 id 标签的值会在命令行调用 profile 时被用到。这个命令格式是:

代码语言:javascript复制
-D<profile id>

③ 其它允许出现的标签

一个 profile 可以覆盖项目的最终名称、项目依赖、插件配置等各个方面以影响构建行为。

  • build
    • defaultGoal
    • finalName
    • resources
    • testResources
    • plugins
  • reporting
  • modules
  • dependencies
  • dependencyManagement
  • repositories
  • pluginRepositories
  • properties

6.3 激活 profile

① 默认配置默认被激活

前面提到了,POM 中没有在 profile 标签里的就是默认的 profile,当然默认被激活。

② 基于环境信息激活

环境信息包含:JDK 版本、操作系统参数、文件、属性等各个方面。一个 profile 一旦被激活,那么它定义的所有配置都会覆盖原来 POM 中对应层次的元素。可参考下面的标签结构:

代码语言:javascript复制
<profile>
    <id>dev</id>
    <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>mavenVersion</name>
            <value>2.0.5</value>
        </property>
        <file>
            <exists>file2.properties</exists>
            <missing>file1.properties</missing>
        </file>
    </activation>
</profile>

这里有个问题是:多个激活条件之间是什么关系呢?

  • Maven 3.2.2 之前:遇到第一个满足的条件即可激活——的关系。
  • Maven 3.2.2 开始:各条件均需满足——的关系。

下面我们来看一个具体例子。假设有如下 profile 配置,在 JDK 版本为 1.6 时被激活:

代码语言:javascript复制
<profiles>
    <profile>
        <id>JDK1.6</id>
        <activation>
            <!-- 指定激活条件为:JDK 1.6 -->
            <jdk>1.6</jdk>
        </activation>
        ……
    </profile>
</profiles>

这里需要指出的是:Maven 会自动检测当前环境安装的 JDK 版本,只要 JDK 版本是以 1.6 开头都算符合条件。下面几个例子都符合:

  • 1.6.0_03
  • 1.6.0_02
  • ……

6.4 Maven profile 多环境管理

在开发过程中,我们的软件会面对不同的运行环境,比如开发环境、测试环境、生产环境,而我们的软件在不同的环境中,有的配置可能会不一样,比如数据源配置、日志文件配置、以及一些软件运行过程中的基本配置,那每次我们将软件部署到不同的环境时,都需要修改相应的配置文件,这样来回修改,很容易出错,而且浪费劳动力。

因此我们可以利用 Maven 的 profile 来进行定义多个 profile,然后每个profile对应不同的激活条件和配置信息,从而达到不同环境使用不同配置信息的效果。

代码语言:javascript复制
<build>
    <!-- profile对资源的操作 -->
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <!-- 先排除所有环境相关的配置文件 -->
            <excludes>
                <exclude>application*.yml</exclude>
            </excludes>
        </resource>
        <resource>
            <directory>src/main/resources</directory>
            <!-- 是否替换 @xx@ 表示的maven properties属性值 -->
            <!--通过开启 filtering,maven 会将文件中的 @xx@ 替换 profile 中定义的 xx 变量/属性-->
            <filtering>true</filtering>
            <includes>
                <include>application.yml</include>
                <include>application-${profileActive}.yml</include>
            </includes>
        </resource>
    </resources>
</build>


<!--多环境文件配置-->
<profiles>
    <!--开发环境-->
    <profile>
        <id>dev</id>
        <activation>
            <!--默认激活-->
            <activeByDefault>true</activeByDefault>
        </activation>
        <properties>
            <profileActive>dev</profileActive>
        </properties>
    </profile>
    <!--测试环境-->
    <profile>
        <id>test</id>
        <properties>
            <profileActive>test</profileActive>
        </properties>
    </profile>
    <!--正式环境-->
    <profile>
        <id>prod</id>
        <properties>
            <profileActive>prod</profileActive>
        </properties>
    </profile>
</profiles>

在 idea 中可以看到,因此,当你需要打包哪一个环境的就勾选即可:

同时,SpringBoot 天然支持多环境配置,一般来说,application.yml存放公共的配置,application-dev.ymlapplication-test.ymlapplication.prod.yml分别存放三个环境的配置。如下:

application.yml 中配置spring.profiles.active=prod(或者dev、test)指定使用的配置文件,如下:

注:profileActive,就是上面我们自定义的标签。

然后当我们勾选哪一个环境,打包的配置文件就是那一个环境:

同时我们再在 resource 标签下看到 includes 和 excludes 标签。它们的作用是:

  • includes:指定执行 resource 阶段时要包含到目标位置的资源
  • excludes:指定执行 resource 阶段时要排除的资源

7. 搭建 Maven 私服:Nexus

很多公司都是搭建自己的 Maven 私有仓库,主要用于项目的公共模块的迭代更新等。

7.1 Nexus 下载安装

下载地址:https://download.sonatype.com/nexus/3/latest-unix.tar.gz

百度网盘:https://pan.baidu.com/s/12IjpSSUSZa6wHZoQ8wHsxg (提取码:5bu6)

百度链接: https://pan.baidu.com/s/1urwk4XfIl3lYUKuuDj6O2Q (提取码: tfb1 )

然后将下载的文件上传到 Linux 系统,解压后即可使用,不需要安装。但是需要注意:必须提前安装 JDK。(我这里放在 /root/nexus 下)

代码语言:javascript复制
tar -zxvf nexus-3.25.1-04-unix.tar.gz

解压后如下:

通过以下命令启动:

代码语言:javascript复制
# 启动
/root/nexus/nexus-3.25.1-04/bin/nexus start
# 查看状态
/root/nexus/nexus-3.25.1-04/bin/nexus status

如果显示nexus is stopped.则说明启动失败,通过命令查看端口占用情况:

代码语言:javascript复制
netstart -luntp|grep java

可以看到 8081 端口被占用,而 nexus 的默认端口为 8081,我们可以修改其默认端口号,其配置文件在 etc目录下的nexus-default.properties,如下:

打开后修改为自己需要设置的端口,注意开启对外防火墙:

提示

bin目录下 nexus.vmoptions 文件,可调整内存参数,防止占用内存太大。 etc目录下 nexus-default.properties 文件可配置默认端口和host及访问根目录。

然后访问 http://[Linux 服务器地址]:8081/进入首页:

7.2 初始设置

点击右上角的登录:

这里参考提示:

  • 用户名:admin
  • 密码:查看 /opt/sonatype-work/nexus3/admin.password 文件

然后输入密码进行下一步:

匿名登录,启用还是禁用?由于启用匿名登录后,后续操作比较简单,这里我们演示禁用匿名登录的操作方式:

除了默认账号 admin,admin 具有全部权限,还有 anonymous,anonymous 作为匿名用户,只具有查看权限,但可以查看仓库并下载依赖。

完成:

7.3 Nexus Repository

nexus 默认创建了几个仓库,如下:

其中仓库 Type 类型为:

仓库类型

说明

proxy

某个远程仓库的代理

group

存放:通过 Nexus 获取的第三方 jar 包

hosted

存放:本团队其他开发人员部署到 Nexus 的 jar 包

仓库名称:

仓库名称

说明

maven-central

Nexus 对 Maven 中央仓库的代理

maven-public

Nexus 默认创建,供开发人员下载使用的组仓库

maven-releasse

Nexus 默认创建,供开发人员部署自己 jar 包的宿主仓库,要求 releasse 版本

maven-snapshots

Nexus 默认创建,供开发人员部署自己 jar 包的宿主仓库,要求 snapshots 版本

其中 maven-public 相当于仓库总和,默认把其他 3 个仓库加进来一起对外提供服务了,另外,如果有自己建的仓库,也要加进该仓库才有用。

初始状态下,这几个仓库都没有内容:

7.4 创建 Nexus Repository

除了自带的仓库,有时候我们需要单独创建自己的仓库,按照默认创建的仓库类型来创建我们自己的仓库。

7.4.1 创建 Nexus 宿主仓库

点击左边导航栏中的 Repositories,如下图:

然后创建仓库,如下:

同理创建 releases 仓库,然后查看列表:

宿主仓库配置如下:

配置

说明

Repository ID

仓库 ID。

Repository Name

仓库名称。

Repository Type

仓库的类型,如 hosted、proxy 等等。

Provider

用来确定仓库的格式,一般默认选择 Maven2。

Repository Policy

仓库的策略。

Default Local Storage Location

仓库默认存储目录,例如 D:nexus-2.14.20-02-bundlesonatype-worknexusindexerbianchengbang_Snapshot_hosted_ctx。

Override Local Storage Location

自定义仓库存储目录。

Deployment Policy

仓库的部署策略。

Allow File Browsing

用来控制是否允许浏览仓库内容,一般选择 true。

Include in Search

用来控制该仓库是否创建索引并提供搜索功能。

Publish URL

用来控制是否通过 URL 提供服务。

Not Found Cache TTL

缓存某构件不存在信息的时间,默认取值为 1440,表示若某一个构件在仓库中没有找到,在 1440 分钟内再次接收到该构件的请求,则直接返回不存在信息,不会再次查找。

7.4.2 创建 Nexus 代理仓库

然后建一个代理仓库,用来下载和缓存中央仓库的构件,这里选择 proxy:

然后创建:

代理仓库配置中,仓库 ID、仓库名称、Provider、Policy 以及 Default Local Storage Location 等配置的含义与宿主仓库相同,不再赘述。需要注意的是,代理仓库的 Repository Type 的取值是 proxy。

代理仓库配置如下表:

配置

说明

Remote Storage Location

远程仓库或中央仓库的地址,它是 Nexus 代理仓库最重要得配置,必须输入有效值,通常取值为 https://repo1.maven.org/maven2/。

Download Remote Indexes

是否下载远程仓库的索引。

Auto Blocking Enabled

是否启用自动阻止,即当 Nexus 无法连接中央仓库或远程仓库时,是否一直等待。取值为 true 表示不再等待,直接通知客户端无法连接,并返回。

File Content Validation

是否启用文件内容校验。

Checksum Policy

配置校验和出错时的策略,用户可以选择忽略、警告、记录警告信息或拒绝下载等多种策略。

Artifact Max Age

构件缓存的最长时间,对于发布版本仓库来说,默认值为 -1,表示构件缓存后,就一直保存着,不再重新下载。对于快照版本仓库来说,默认值为 1440 分钟,表示每隔一天重新缓存一次代理的构件。

Metadata Max Age

仓库元数据缓存的最长时间。

Item Max Age

项目缓存的最长时间。

7.4.3 创建 Nexus 仓库组

下面我们将创建一个仓库组,并将刚刚创建的 3 个仓库都聚合起来,这里选择 group,如下:

查看 Nexus 仓库列表,可以看到创建的仓库组已经创建完成,如下图:

7.5 通过 Nexus 下载 jar 包

由于初始状态下都没有内容,所以我们需要进行配置,我们先在本地的 Maven 的配置文件中新建一个空的本地仓库作为测试。

然后,把我们原来配置仓库地址的 mirror 标签改成下面这样:

代码语言:javascript复制
<mirror>
  <id>maven-public</id>
  <mirrorOf>central</mirrorOf>
  <name>Maven public</name>
  <url>http://106.15.15.213:8090/repository/maven-public/</url>  
</mirror>

这里的 url 标签是这么来的:

把上图中看到的地址复制出来即可。如果我们在前面允许了匿名访问,到这里就够了。但如果我们禁用了匿名访问,那么接下来我们还要继续配置 settings.xml:

代码语言:javascript复制
<server>
  <id>maven-public</id>
  <username>admin</username>
  <password>@123456</password>
</server>

注意:server 标签内的 id 标签值必须和 mirror 标签中的 id 值一样。

然后找一个用到框架的 Maven 工程,编译 compile,下载过程日志:

下载后,Nexus 服务器上就有了 jar 包:

7.6 将 jar 包部署到 Nexus

这一步的作用是将通用的模块打成 jar 包,发布到 Nexus 私服,让其他的项目来引用,以更简洁高效的方式来实现复用和管理。

需要配置 server:

代码语言:javascript复制
<server>
  <id>maven-public</id>
  <username>admin</username>
  <password>@123456</password>
</server>
<server>
  <id>maven-releases</id>
  <username>admin</username>
  <password>@123456</password>
</server>
<server>
  <id>maven-snapshots</id>
  <username>admin</username>
  <password>@123456</password>
</server>

然后在我们需要上传的 maven 项目中的pom.xml添加如下配置:

代码语言:javascript复制
<!-- 这里的 id 要和上面的 server 的 id 保持一致,name 随意写-->
<distributionManagement>
    <repository>
        <id>maven-releases</id>
        <name>Releases Repository</name>
        <url>http://106.15.15.213:8090/repository/maven-releases/</url>
    </repository>
    <snapshotRepository>
        <id>maven-snapshots</id>
        <name>Snapshot Repository</name>
        <url>http://106.15.15.213:8090/repository/maven-snapshots/</url>
    </snapshotRepository>
</distributionManagement>
7.6.1 上传到 maven-snapshots

执行命令 mvn deploy 将当前 SNAPSHOT(快照版)上传到私服 maven-snapshots。

7.6.2 上传到 maven-releases

修改项目的版本,如下:

执行命令 mvn deploy

查看:

package 命令完成了项目编译、单元测试、打包功能。 install 命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库。 deploy 命令完成了项目编译、单元测试、打包功能,同时把打好的可执行jar包(war包或其它形式的包)布署到本地maven仓库和远程maven私服仓库。

8. jar 包冲突问题

先给结论:编订依赖列表的程序员。初次设定一组依赖,因为尚未经过验证,所以确实有可能存在各种问题,需要做有针对性的调整。那么谁来做这件事呢?我们最不希望看到的就是:团队中每个程序员都需要自己去找依赖,即使是做同一个项目,每个模块也各加各的依赖,没有统一管理。那前人踩过的坑,后人还要再踩一遍。而且大家用的依赖有很多细节都不一样,版本更是五花八门,这就让事情变得更加复杂。

所以虽然初期需要根据项目开发和实际运行情况对依赖配置不断调整,最终确定一个各方面都 OK 的版本。但是一旦确定下来,放在父工程中做依赖管理,各个子模块各取所需,这样基本上就能很好的避免问题的扩散。

即使开发中遇到了新问题,也可以回到源头检查、调整 dependencyManagement 配置的列表——而不是每个模块都要改。

8.1 表现形式

由于实际开发时我们往往都会整合使用很多大型框架,所以一个项目中哪怕只是一个模块也会涉及到大量 jar 包。数以百计的 jar 包要彼此协调、精密配合才能保证程序正常运行。而规模如此庞大的 jar 包组合在一起难免会有磕磕碰碰。最关键的是由于 jar 包冲突所导致的问题非常诡异,这里我们只能罗列较为典型的问题,而没法保证穷举。

但是我们仍然能够指出一点:一般来说,由于我们自己编写代码、配置文件写错所导致的问题通常能够在异常信息中看到我们自己类的全类名或配置文件的所在路径。如果整个错误信息中完全没有我们负责的部分,全部是框架、第三方工具包里面的类报错,这往往就是 jar 包的问题所引起的。

而具体的表现形式中,主要体现为找不到类或找不到方法。

8.1.1 抛异常:找不到类

此时抛出的常见的异常类型:

  • java.lang.ClassNotFoundException:编译过程中找不到类
  • java.lang.NoClassDefFoundError:运行过程中找不到类
  • java.lang.LinkageError:不同类加载器分别加载的多个类有相同的全限定名

我们来举个例子:

代码语言:javascript复制
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.x.x</version>
</dependency>
12345

httpclient 这个 jar 包中有一个类:org.apache.http.conn.ssl.NoopHostnameVerifier。这个类在较低版本中没有,但在较高版本存在。比如:

jar 包版本

是否存在

4.3.6

4.4

那当我们确实需要用到 NoopHostnameVerifier 这个类,我们看到 Maven 通过依赖传递机制引入了这个 jar 包,所以没有明确地显式声明对这个 jar 包的依赖。可是 Maven 传递过来的 jar 包是 4.3.6 版本,里面没有包含我们需要的类,就会抛出异常。

而『冲突』体现在:4.3.6 和 4.4 这两个版本的 jar 包都被框架所依赖的 jar 包给传递进来了,但是假设 Maven 根据『版本仲裁』规则实际采纳的是 4.3.6。

版本仲裁 Maven 的版本仲裁机制只是在没有人为干预的情况下,自主决定 jar 包版本的一个办法。而实际上我们要使用具体的哪一个版本,还要取决于项目中的实际情况。所以在项目正常运行的情况下,jar 包版本可以由 Maven 仲裁,不必我们操心;而发生冲突时 Maven 仲裁决定的版本无法满足要求,此时就应该由程序员明确指定 jar 包版本。

版本仲裁遵循以下规则:

  • 最短路径优先

在下图的例子中,对模块 pro25-module-a 来说,Maven 会采纳 1.2.12 版本。

  • 路径相同时先声明者优先

此时 Maven 采纳哪个版本,取决于在 pro29-module-x 中,对 pro30-module-y 和 pro31-module-z 两个模块的依赖哪一个先声明。

8.1.2 抛异常:找不到方法

程序找不到符合预期的方法。这种情况多见于通过反射调用方法,所以经常会导致:java.lang.NoSuchMethodError。

8.1.3 没报错但结果不对

发生这种情况比较典型的原因是:两个 jar 包中的类分别实现了同一个接口,这本来是很正常的。但是问题在于:由于没有注意命名规范,两个不同实现类恰巧是同一个名字。

具体例子是实际工作中遇到过:项目中部分模块使用 log4j 打印日志;其它模块使用 logback,编译运行都不会冲突,但是会引起日志服务降级,让你的 log 配置文件失效。比如:你指定了 error 级别输出,但是冲突就会导致 info、debug 都在输出。

8.2 本质

以上表现形式归根到底是两种基本情况导致的:

  • 同一 jar 包的不同版本
  • 不同 jar 包中包含同名类

这里我们拿 netty 来举个例子,netty 是一个类似 Tomcat 的 Servlet 容器。通常我们不会直接依赖它,所以基本上都是框架传递进来的。那么当我们用到的框架很多时,就会有不同的框架用不同的坐标导入 netty。可以参照下表对比一下两组坐标:

| 截止到3.2.10.Final版本以前的坐标形式: | 从3.3.0.Final版本开始以后的坐标形式: | | :--------------------------------------------- | :------------------------------------- | | org.jboss.netty netty 3.2.10.Final | io.netty netty 3.9.2.Final |

但是偏偏这两个『不同的包』里面又有很多『全限定名相同』的类。例如:

代码语言:javascript复制
org.jboss.netty.channel.socket.ServerSocketChannelConfig.class
org.jboss.netty.channel.socket.nio.NioSocketChannelConfig.class
org.jboss.netty.util.internal.jzlib.Deflate.class
org.jboss.netty.handler.codec.serialization.ObjectDecoder.class
org.jboss.netty.util.internal.ConcurrentHashMap$HashIterator.class
org.jboss.netty.util.internal.jzlib.Tree.class
org.jboss.netty.util.internal.ConcurrentIdentityWeakKeyHashMap$Segment.class
org.jboss.netty.handler.logging.LoggingHandler.class
org.jboss.netty.channel.ChannelHandlerLifeCycleException.class
org.jboss.netty.util.internal.ConcurrentIdentityHashMap$ValueIterator.class
org.jboss.netty.util.internal.ConcurrentIdentityWeakKeyHashMap$Values.class
org.jboss.netty.util.internal.UnterminatableExecutor.class
org.jboss.netty.handler.codec.compression.ZlibDecoder.class
org.jboss.netty.handler.codec.rtsp.RtspHeaders$Values.class
org.jboss.netty.handler.codec.replay.ReplayError.class
org.jboss.netty.buffer.HeapChannelBufferFactory.class
……
123456789101112131415161718

8.3 解决办法

很多情况下常用框架之间的整合容易出现的冲突问题都有人总结过了,拿抛出的异常搜索一下基本上就可以直接找到对应的 jar 包。我们接下来要说的是通用方法。

不管具体使用的是什么工具,基本思路无非是这么两步:

  • 第一步:把彼此冲突的 jar 包找到
  • 第二步:在冲突的 jar 包中选定一个。具体做法无非是通过 exclusions 排除依赖,或是明确声明依赖。
8.3.1 IDEA 的 Maven Helper 插件

这个插件是 IDEA 中安装的插件,不是 Maven 插件。它能够给我们罗列出来同一个 jar 包的不同版本,以及它们的来源。但是对不同 jar 包中同名的类没有办法。

然后基于 pom.xml 的依赖冲突分析,如下:

查看冲突分析结果:

8.3.2 Maven 的 enforcer 插件

使用 Maven 的 enforcer 插件既可以检测同一个 jar 包的不同版本,又可以检测不同 jar 包中同名的类。

这里我们引入两个对 netty 的依赖,展示不同 jar 包中有同名类的情况作为例子。

代码语言:javascript复制
<dependencies>
    <dependency>
        <groupId>org.jboss.netty</groupId>
        <artifactId>netty</artifactId>
        <version>3.2.10.Final</version>
    </dependency>

    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty</artifactId>
        <version>3.9.2.Final</version>
    </dependency>
</dependencies>

然后配置 enforcer 插件:

代码语言:javascript复制
<build>
    <pluginManagement>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-enforcer-plugin</artifactId>
                <version>1.4.1</version>
                <executions>
                    <execution>
                        <id>enforce-dependencies</id>
                        <phase>validate</phase>
                        <goals>
                            <goal>display-info</goal>
                            <goal>enforce</goal>
                        </goals>
                    </execution>
                </executions>
                <dependencies>
                    <dependency>
                        <groupId>org.codehaus.mojo</groupId>
                        <artifactId>extra-enforcer-rules</artifactId>
                        <version>1.0-beta-4</version>
                    </dependency>
                </dependencies>
                <configuration>
                    <rules>
                        <banDuplicateClasses>
                            <findAllDuplicates>true</findAllDuplicates>
                        </banDuplicateClasses>
                    </rules>
                </configuration>
            </plugin>
        </plugins>
    </pluginManagement>
</build>

执行如下 Maven 命令:

代码语言:javascript复制
mvn clean package enforcer:enforce

部分运行结果:

文章知识点与官方知识档案匹配,可进一步学习相关知识

9.拓展知识:

1.pom.xml配置详解信息:

代码语言:javascript复制
<?xml version="1.0" encoding="utf-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0http://maven.apache.org/maven-v4_0_0.xsd">
    <!--父项目的坐标。如果项目中没有规定某个元素的值,
  那么父项目中的对应值即为项目的默认值。 
  坐标包括group ID,artifact ID和 version。-->
    <parent>
        <!--被继承的父项目的构件标识符-->
        <artifactId/>
        <!--被继承的父项目的全球唯一标识符-->
        <groupId/>
        <!--被继承的父项目的版本-->
        <version/>
    </parent>
    <!--声明项目描述符遵循哪一个POM模型版本。模型本身的版本很少改变,虽然如此,
  但它仍然是必不可少的,这是为了当Maven引入了新的特性或者其他模型变更的时候,
  确保稳定性。-->
    <modelVersion>4.0.0</modelVersion>
    <!--项目的全球唯一标识符,通常使用全限定的包名区分该项目和其他项目。
  并且构建时生成的路径也是由此生成, 如com.mycompany.app生成的相对路径为:
  /com/mycompany/app -->
    <groupId>cn.missbe.web</groupId>
    <!-- 构件的标识符,它和group ID一起唯一标识一个构件。换句话说,
  你不能有两个不同的项目拥有同样的artifact ID和groupID;在某个 
  特定的group ID下,artifact ID也必须是唯一的。构件是项目产生的或使用的一个东西,
  Maven为项目产生的构件包括:JARs,源码,二进制发布和WARs等。-->
    <artifactId>search-resources</artifactId>
    <!--项目产生的构件类型,例如jar、war、ear、pom。插件可以创建
  他们自己的构件类型,所以前面列的不是全部构件类型-->
    <packaging>war</packaging>
    <!--项目当前版本,格式为:主版本.次版本.增量版本-限定版本号-->
    <version>1.0-SNAPSHOT</version>
    <!--项目的名称, Maven产生的文档用-->
    <name>search-resources</name>
    <!--项目主页的URL, Maven产生的文档用-->
    <url>http://www.missbe.cn</url>
    <!-- 项目的详细描述, Maven 产生的文档用。  当这个元素能够用HTML格式描述时
  (例如,CDATA中的文本会被解析器忽略,就可以包含HTML标 签), 
  不鼓励使用纯文本描述。如果你需要修改产生的web站点的索引页面,
  你应该修改你自己的索引页文件,而不是调整这里的文档。-->
    <description>A maven project to study maven.</description>
    <!--描述了这个项目构建环境中的前提条件。-->
    <prerequisites>
        <!--构建该项目或使用该插件所需要的Maven的最低版本-->
        <maven/>
    </prerequisites>
    <!--构建项目需要的信息-->
    <build>
        <!--该元素设置了项目源码目录,当构建项目的时候,
    构建系统会编译目录里的源码。该路径是相对于pom.xml的相对路径。-->
        <sourceDirectory/>
        <!--该元素设置了项目脚本源码目录,该目录和源码目录不同:
    绝大多数情况下,该目录下的内容 会被拷贝到输出目录(因为脚本是被解释的,而不是被编译的)。-->
        <scriptSourceDirectory/>
        <!--该元素设置了项目单元测试使用的源码目录,当测试项目的时候,
    构建系统会编译目录里的源码。该路径是相对于pom.xml的相对路径。-->
        <testSourceDirectory/>
        <!--被编译过的应用程序class文件存放的目录。-->
        <outputDirectory/>
        <!--被编译过的测试class文件存放的目录。-->
        <testOutputDirectory/>
        <!--使用来自该项目的一系列构建扩展-->
        <extensions>
            <!--描述使用到的构建扩展。-->
            <extension>
                <!--构建扩展的groupId-->
                <groupId/>
                <!--构建扩展的artifactId-->
                <artifactId/>
                <!--构建扩展的版本-->
                <version/>
            </extension>
        </extensions>
        <!--这个元素描述了项目相关的所有资源路径列表,例如和项目相关的属性文件,
    这些资源被包含在最终的打包文件里。-->
        <resources>
            <!--这个元素描述了项目相关或测试相关的所有资源路径-->
            <resource>
                <!-- 描述了资源的目标路径。该路径相对target/classes目录(例如${project.build.outputDirectory})。举个例 子,如果你想资源在特定的包里(org.apache.maven.messages),你就必须该元素设置为org/apache/maven /messages。
        然而,如果你只是想把资源放到源码目录结构里,就不需要该配置。-->
                <targetPath/>
                <!--是否使用参数值代替参数名。参数值取自properties元素或者文件里配置的属性,
        文件在filters元素里列出。-->
                <filtering/>
                <!--描述存放资源的目录,该路径相对POM路径-->
                <directory/>
                <!--包含的模式列表,例如**/*.xml.-->
                <includes/>
                <!--排除的模式列表,例如**/*.xml-->
                <excludes/>
            </resource>
        </resources>
        <!--这个元素描述了单元测试相关的所有资源路径,例如和单元测试相关的属性文件。-->
        <testResources>
            <!--这个元素描述了测试相关的所有资源路径,参见build/resources/resource元素的说明-->
            <testResource>
                <targetPath/>
                <filtering/>
                <directory/>
                <includes/>
                <excludes/>
            </testResource>
        </testResources>
        <!--构建产生的所有文件存放的目录-->
        <directory/>
        <!--产生的构件的文件名,默认值是${artifactId}-${version}。-->
        <finalName/>
        <!--当filtering开关打开时,使用到的过滤器属性文件列表-->
        <filters/>
        <!--子项目可以引用的默认插件信息。该插件配置项直到被引用时才会被解析或绑定到生命周期。
    给定插件的任何本地配置都会覆盖这里的配置-->
        <pluginManagement>
            <!--使用的插件列表 。-->
            <plugins>
                <!--plugin元素包含描述插件所需要的信息。-->
                <plugin>
                    <!--插件在仓库里的group ID-->
                    <groupId/>
                    <!--插件在仓库里的artifact ID-->
                    <artifactId/>
                    <!--被使用的插件的版本(或版本范围)-->
                    <version/>
                    <!--是否从该插件下载Maven扩展(例如打包和类型处理器),由于性能原因,
          只有在真需要下载时,该元素才被设置成enabled。-->
                    <extensions/>
                    <!--在构建生命周期中执行一组目标的配置。每个目标可能有不同的配置。-->
                    <executions>
                        <!--execution元素包含了插件执行需要的信息-->
                        <execution>
                            <!--执行目标的标识符,用于标识构建过程中的目标,或者匹配继承过程中需要合并的执行目标-->
                            <id/>
                            <!--绑定了目标的构建生命周期阶段,如果省略,目标会被绑定到源数据里配置的默认阶段-->
                            <phase/>
                            <!--配置的执行目标-->
                            <goals/>
                            <!--配置是否被传播到子POM-->
                            <inherited/>
                            <!--作为DOM对象的配置-->
                            <configuration/>
                        </execution>
                    </executions>
                    <!--项目引入插件所需要的额外依赖-->
                    <dependencies>
                        <!--参见dependencies/dependency元素-->
                        <dependency>......</dependency>
                    </dependencies>
                    <!--任何配置是否被传播到子项目-->
                    <inherited/>
                    <!--作为DOM对象的配置-->
                    <configuration/>
                </plugin>
            </plugins>
        </pluginManagement>
        <!--使用的插件列表-->
        <plugins>
            <!--参见build/pluginManagement/plugins/plugin元素-->
            <plugin>
                <groupId/>
                <artifactId/>
                <version/>
                <extensions/>
                <executions>
                    <execution>
                        <id/>
                        <phase/>
                        <goals/>
                        <inherited/>
                        <configuration/>
                    </execution>
                </executions>
                <dependencies>
                    <!--参见dependencies/dependency元素-->
                    <dependency>......</dependency>
                </dependencies>
                <goals/>
                <inherited/>
                <configuration/>
            </plugin>
        </plugins>
    </build>
    <!--模块(有时称作子项目) 被构建成项目的一部分。
  列出的每个模块元素是指向该模块的目录的相对路径-->
    <modules/>
    <!--发现依赖和扩展的远程仓库列表。-->
    <repositories>
        <!--包含需要连接到远程仓库的信息-->
        <repository>
            <!--如何处理远程仓库里发布版本的下载-->
            <releases>
                <!--true或者false表示该仓库是否为下载某种类型构件(发布版,快照版)开启。 -->
                <enabled/>
                <!--该元素指定更新发生的频率。Maven会比较本地POM和远程POM的时间戳。这里的选项是:always(一直),daily(默认,每日),interval:X(这里X是以分钟为单位的时间间隔),或者never(从不)。-->
                <updatePolicy/>
                <!--当Maven验证构件校验文件失败时该怎么做:ignore(忽略),fail(失败),或者warn(警告)。-->
                <checksumPolicy/>
            </releases>
            <!-- 如何处理远程仓库里快照版本的下载。有了releases和snapshots这两组配置,
      POM就可以在每个单独的仓库中,为每种类型的构件采取不同的 策略。
      例如,可能有人会决定只为开发目的开启对快照版本下载的支持。
      参见repositories/repository/releases元素 -->
            <snapshots>
                <enabled/>
                <updatePolicy/>
                <checksumPolicy/>
            </snapshots>
            <!--远程仓库唯一标识符。可以用来匹配在settings.xml文件里配置的远程仓库-->
            <id>banseon-repository-proxy</id>
            <!--远程仓库名称-->
            <name>banseon-repository-proxy</name>
            <!--远程仓库URL,按protocol://hostname/path形式-->
            <url>http://192.168.1.169:9999/repository/</url>
            <!-- 用于定位和排序构件的仓库布局类型-可以是default(默认)或者legacy(遗留)。Maven 2为其仓库提供了一个默认的布局;然 而,Maven 1.x有一种不同的布局。我们可以使用该元素指定布局是default(默认)还是legacy(遗留)。-->
            <layout>default</layout>
        </repository>
    </repositories>
    <!--发现插件的远程仓库列表,这些插件用于构建和报表-->
    <pluginRepositories>
        <!--包含需要连接到远程插件仓库的信息.参见repositories/repository元素-->
        <pluginRepository>......</pluginRepository>
    </pluginRepositories>
    <!--该元素描述了项目相关的所有依赖。 这些依赖组成了项目构建过程中的一个个环节。
  它们自动从项目定义的仓库中下载。要获取更多信息,请看项目依赖机制。-->
    <dependencies>
        <dependency>
            <!--依赖的group ID-->
            <groupId>org.apache.maven</groupId>
            <!--依赖的artifact ID-->
            <artifactId>maven-artifact</artifactId>
            <!--依赖的版本号。 在Maven 2里, 也可以配置成版本号的范围。-->
            <version>3.8.1</version>
            <!-- 依赖类型,默认类型是jar。它通常表示依赖的文件的扩展名,但也有例外
      。一个类型可以被映射成另外一个扩展名或分类器。类型经常和使用的打包方式对应,
       尽管这也有例外。一些类型的例子:jar,war,ejb-client和test-jar。
      如果设置extensions为 true,就可以在 plugin里定义新的类型。所以前面的类型的例子不完整。-->
            <type>jar</type>
            <!-- 依赖的分类器。分类器可以区分属于同一个POM,但不同构建方式的构件。
      分类器名被附加到文件名的版本号后面。例如,如果你想要构建两个单独的构件成 JAR,
      一个使用Java 1.4编译器,另一个使用Java 6编译器,你就可以使用分类器来生成两个单独的JAR构件。-->
            <classifier/>
            <!--依赖范围。在项目发布过程中,帮助决定哪些构件被包括进来。欲知详情请参考依赖机制。    
                      - compile :默认范围,用于编译      
                      - provided:类似于编译,但支持你期待jdk或者容器提供,类似于classpath      
                      - runtime: 在执行时需要使用      
                      - test:    用于test任务时使用      
                      - system: 需要外在提供相应的元素。通过systemPath来取得      
                      - systemPath: 仅用于范围为system。提供相应的路径      
                      - optional:   当项目自身被依赖时,标注依赖是否传递。用于连续依赖时使用-->
            <scope>test</scope>
            <!--仅供system范围使用。注意,不鼓励使用这个元素,
      并且在新的版本中该元素可能被覆盖掉。该元素为依赖规定了文件系统上的路径。
      需要绝对路径而不是相对路径。推荐使用属性匹配绝对路径,例如${java.home}。-->
            <systemPath/>
            <!--当计算传递依赖时, 从依赖构件列表里,列出被排除的依赖构件集。
      即告诉maven你只依赖指定的项目,不依赖项目的依赖。此元素主要用于解决版本冲突问题-->
            <exclusions>
                <exclusion>
                    <artifactId>spring-core</artifactId>
                    <groupId>org.springframework</groupId>
                </exclusion>
            </exclusions>
            <!--可选依赖,如果你在项目B中把C依赖声明为可选,你就需要在依赖于B的项目(例如项目A)中显式的引用对C的依赖。可选依赖阻断依赖的传递性。-->
            <optional>true</optional>
        </dependency>
    </dependencies>
    <!-- 继承自该项目的所有子项目的默认依赖信息。这部分的依赖信息不会被立即解析,
  而是当子项目声明一个依赖(必须描述group ID和 artifact ID信息),
  如果group ID和artifact ID以外的一些信息没有描述,
  则通过group ID和artifact ID 匹配到这里的依赖,并使用这里的依赖信息。-->
    <dependencyManagement>
        <dependencies>
            <!--参见dependencies/dependency元素-->
            <dependency>......</dependency>
        </dependencies>
    </dependencyManagement>
    <!--项目分发信息,在执行mvn deploy后表示要发布的位置。
  有了这些信息就可以把网站部署到远程服务器或者把构件部署到远程仓库。-->
    <distributionManagement>
        <!--部署项目产生的构件到远程仓库需要的信息-->
        <repository>
            <!--是分配给快照一个唯一的版本号(由时间戳和构建流水号)?
      还是每次都使用相同的版本号?参见repositories/repository元素-->
            <uniqueVersion/>
            <id>banseon-maven2</id>
            <name>banseon maven2</name>
            <url>file://${basedir}/target/deploy</url>
            <layout/>
        </repository>
        <!--构件的快照部署到哪里?如果没有配置该元素,默认部署到repository元素配置的仓库,
    参见distributionManagement/repository元素-->
        <snapshotRepository>
            <uniqueVersion/>
            <id>banseon-maven2</id>
            <name>Banseon-maven2 Snapshot Repository</name>
            <url>scp://svn.baidu.com/banseon:/usr/local/maven-snapshot</url>
            <layout/>
        </snapshotRepository>
        <!--部署项目的网站需要的信息-->
        <site>
            <!--部署位置的唯一标识符,用来匹配站点和settings.xml文件里的配置-->
            <id>banseon-site</id>
            <!--部署位置的名称-->
            <name>business api website</name>
            <!--部署位置的URL,按protocol://hostname/path形式-->
            <url>scp://svn.baidu.com/banseon:/var/www/localhost/banseon-web</url>
        </site>
        <!--项目下载页面的URL。如果没有该元素,用户应该参考主页。
    使用该元素的原因是:帮助定位那些不在仓库里的构件(由于license限制)。-->
        <downloadUrl/>
        <!-- 给出该构件在远程仓库的状态。不得在本地项目中设置该元素,
    因为这是工具自动更新的。有效的值有:none(默认),
    converted(仓库管理员从 Maven 1 POM转换过来),partner(直接从伙伴Maven 2仓库同步过来),deployed(从Maven 2实例部 署),verified(被核实时正确的和最终的)。-->
        <status/>
    </distributionManagement>
    <!--以值替代名称,Properties可以在整个POM中使用,也可以作为触发条件(见settings.xml配置文件里activation元素的说明)。格式是<name>value</name>。-->
    <properties/>
</project>

0 人点赞