Maven介绍

Maven 是服务于Java项目的自动化构建工具。

Maven 是 Apache 软件基金会组织维护的一款自动化构建工具,专注服务于 Java 平台的项目构建和依赖管理。Maven 这个单词的本意是:专家,内行。

Maven 是目前最流行的自动化构建工具,对于生产环境下多框架、多模块整合开发有重要作用,Maven 是一款在大型项目开发过程中不可或缺的重要工具。

为什么使用Maven

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

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

借助于maven我们可以使用统一的规范方式下载jar包,这样就不用用到的时候每次都要自己提前准备好或到官网下载

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

一个jar包依赖其他的jar包需要自己手动的加入到项目中,极大的浪费了我们导入包的时间成本,也极大的增加了学习成本。借助于maven,它会自动的将依赖的jar包导入进来。

每个项目或模块开发过程中都会有 bug,因此写完了代码,我们还要写一些单元测试,然后一个个的运行来检验代码质量,Maven提供了专门的测试插件来实施测试。

Maven构建过程

  • 清理clean:删除以前的编译结果

  • 编译compile:将java源程序编译成class字节码文件

  • 测试test:自动测试,自动调用junit程序

  • 报告report:测试程序执行的结果

  • 打包package:将一个包含诸多文件的工程封装为一个压缩文件用于安装或部署。Java 工程对应 jar 包,Web 工程对应war包。

  • 安装install:将打包得到的文件复制到“仓库”中的指定位置

  • 部署deploy:将打包得到的文件部署到对应服务器上,使其可以运行

Maven的核心功能

项目构建

对项目进行编译、测试、打包、部署等构建

依赖管理

对jar包的统一管理,Maven提供中央仓库,私服,本地仓库解决jar包的依赖和相关依赖的下载。

如下图所示:包括蓝、黄两个部分分别对应着依赖关系项目构建两大核心功能。

Maven的核心概念

Pom文件介绍

POM(Project Object Model)项目对象模型,它是Maven的核心组件。它是Maven中的基本工作单元。它是一个xml文件,以pom.xml驻留在项目的根目录中。POM不仅包含有关项目的信息及Maven用于构建项目的各种配置的详细信息,还包含目标和插件。

<?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">
    <!-- POM使用的对象模型版本 -->
    <modelVersion>4.0.0</modelVersion>

    <!-- 本项目的坐标(gav) -->
    <!-- 公司或者组织域名倒序+项目名 -->
    <groupId>com.oppo</groupId>
    <!-- 模块名 -->
    <artifactId>mavrn_01</artifactId>
    <!-- 版本号 -->
    <version>1.0-SNAPSHOT</version>

    <!--打包方式: jar(默认)、pom、war-->
    <packaging>war</packaging>

    <!-- 用于项目的显示名称,这经常在 Maven 生成的文档中使用 -->
    <name>mavrn_01</name>
    <!-- 找到项目站点的位置,这经常在 Maven 生成的文档中使用 -->
    <url>http://www.example.com</url>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- 集中定义junit版本号 -->
        <junit.version>5.1.32</junit.version>
    </properties>

    <!--
    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    -->

    <!-- 列出依赖项,POM 的基石 -->
    <dependencies>

        <!-- junit依赖
        junit: https://mvnrepository.com/artifact/junit/junit
        -->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>${junit.version}</version>
            <!-- 依赖范围: compile、provided、runtime、 test、system -->
            <scope>compile</scope>
        </dependency>
        <!-- junit依赖 -->

        <!-- log4j 依赖
        log4j-api: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api
        log4j-core: https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core
        -->
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-api</artifactId>
            <version>2.24.3</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.24.3</version>
        </dependency>
        <!-- log4j 依赖 -->

        <!-- JSP JSTL标签库依赖
        jakarta.servlet.jsp.jstl: https://mvnrepository.com/artifact/org.glassfish.web/jakarta.servlet.jsp.jstl
        jakarta.servlet.jsp.jstl-api: https://mvnrepository.com/artifact/jakarta.servlet.jsp.jstl/jakarta.servlet.jsp.jstl-api
        -->
        <dependency>
            <groupId>org.glassfish.web</groupId>
            <artifactId>jakarta.servlet.jsp.jstl</artifactId>
            <version>2.0.0</version>
        </dependency>
        <dependency>
            <groupId>jakarta.servlet.jsp.jstl</groupId>
            <artifactId>jakarta.servlet.jsp.jstl-api</artifactId>
            <version>2.0.0</version>
        </dependency>
        <!-- JSP JSTL标签库依赖 -->

        <!-- mybatis依赖
        https://mvnrepository.com/artifact/org.mybatis/mybatis
        -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.19</version>
        </dependency>
        <!-- mybatis依赖 -->

        <!-- MySQL依赖
        https://mvnrepository.com/artifact/mysql/mysql-connector-java
        -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <!-- MySQL依赖
        https://mvnrepository.com/artifact/com.mysql/mysql-connector-j
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.3.0</version>
        </dependency>
        -->

    </dependencies>

    <!-- 声明项目的目录结构和管理插件之类的事情 -->
    <build>
        <!-- 项目的名字 -->
        <finalName>WebMavenDemo</finalName>

        <!-- 描述项目中资源的位置 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <!-- 包括哪些文件参与打包 -->
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <!-- 排除哪些文件不参与打包 -->
                <excludes>
                  <exclude>**/*.txt</exclude>
                  <exclude>**/*.doc</exclude>
                </excludes>
            </resource>
        </resources>

        <!-- 设置构建时候的插件 -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <!-- 源代码编译版本 -->
                    <source>8</source>
                    <!-- 目标平台编译版本 -->
                    <target>8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>

        <!-- 资源插件(资源的插件) -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-resources-plugin</artifactId>
            <version>2.1</version>
            <executions>
                <execution>
                    <phase>compile</phase>
                </execution>
            </executions>
            <configuration>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>

        <!-- war插件(将项目打成war包) -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-war-plugin</artifactId>
            <version>2.1</version>
            <configuration>
                <!-- war包名字 -->
                <warName>WebMavenDemo1</warName>
            </configuration>
        </plugin>

    </build>

</project>

目录结构约定

maven项目有预先约定好的目录结构,必须要遵循的规范,所有的Maven项目都依照这个规范。主要的目的是将项目的源码文件,测试代码,资源文件完全分开,便于项目管理和扩展。

Maven坐标(GAV)

也称为GAV定位,使用三个标签来唯一定位jar资源。项目的唯一的名称,创建项目时定义gav名称,引用项目时使用gav名称。相当于项目的身份证号。

  • groupId:组织名称,公司或者组织域名倒序+项目名

  • artifactId:模块或者项目名称

  • version:版本号,SNAPSHOT一般为开发时的临时版本,RELEASE为正式发布版本

Maven仓库(Repository)

仓库指存放jar包的位置,Maven项目中所有引用的依赖都存放于仓库中,仓库分为本地仓库和远程仓库

本地仓库

本地仓库一般存在于当前主机环境,默认存放在"~.m2\repository",你也可以通过修改maven下conf/settings.xml文件中的<localRepository>属性

远程仓库

远程仓库一般指的为存放于远程服务器上的仓库,它分为maven官方提供的中央仓库(https://repo.maven.apache.org/maven2)、以及其它公司或者组织自己提供的仓库(https://mvnrepository.com)

中央仓库是maven默认的仓库,包含了绝大多数流行的开源Java构件,以及源码、作者信息、许可证信息等。一般来说,简单的Java项目依赖的构件都可以在这里下载得到。

Maven依赖

一个Maven 项目正常运行需要其它项目的支持,Maven 会根据坐标自动到本地仓库中进行查找。对于程序员自己的 Maven 项目需要进行安装,才能保存到仓库中。

Maven依赖是通过<dependencies>大标签中的子标签<dependency>,使用gav添加依赖。

<dependencies>
  <dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.11</version>
  </dependency>
</dependencies>

Maven声明周期

对项目的构建是建立在生命周期模型上的,它明确定义项目生命周期各个阶段,并且对于每一个阶段提供相对应的命令,对开发者而言仅仅需要掌握一小堆的命令就可以完成项目各个阶段的构建工作。

构建项目时按照生命周期顺序构建,每一个阶段都有特定的插件来完成。不论现在要执行生命周期中的哪个阶段,都是从这个生命周期的最初阶段开始的。

对于我们程序员而言,无论我们要进行哪个阶段的构建,直接执行相应的命令即可,无需担心它前边阶段是否构建,Maven 都会自动构建。这也就是 Maven 这种自动化构建工具给我们带来的好处。

使用idea后,生命周期要调用的命令被集成化一些按钮,只需要双击即可调用相应的插件来运行。

生命周期对应的Maven命令(了解):

  1. mvn clean 清理(会删除原来编译和测试的目录,即 target 目录,但是已经 install 到仓库里的包不会删除)

  2. mvn compile 编译主程序(会在当前目录下生成一个 target,里边存放编译主程序之后生成的字节码文件)

  3. mvn test-compile 编译测试程序(会在当前目录下生成一个 target,里边存放编译测试程序之后生成的字节码文件)

  4. mvn test 测试(会生成一个目录surefire-reports,保存测试结果)

  5. mvn package 打包主程序(会编译、编译测试、测试、并且按照 pom.xml 配置把主程序打包生成 jar 包或者 war 包)

  6. mvn install 安装主程序(会把本工程打包,并且按照本工程的坐标保存到本地仓库中)

  7. mvn deploy 部署主程序(部署到私服仓库中)。

Maven插件

Maven本质上是一个插件框架,它的核心并不执行任何具体的构建任务,所有这些任务都交给插件来完成,例如编译源代码是由maven- compiler-plugin完成的。进一步说,每个任务对应了一个插件目标(goal),每个插件会有一个或者多个目标,例如maven- compiler-plugin的compile目标用来编译位于src/main/java/目录下的主源码,testCompile目标用来编译位于src/test/java/目录下的测试源码。

Maven支持极简化的插件添加.使用<plugins>大标签中添加<plugin>子标签引用插件.

<plugins>
  <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
      <source>17</source>
      <target>17</target>
      <encoding>UTF-8</encoding>
    </configuration>
  </plugin>
</plugins>

Maven依赖管理

Maven依赖范围

Maven的依赖构件包含一个依赖范围的属性。这个属性描述的是三套classpath的控制,即编译、测试、运行。这说白了就是添加的jar包起作用的范围。

Maven提供了以下几种依赖范围

选项

说明

compile

该范围就是默认依赖范围,此依赖范围对于编译、测试、运行三种classpath都有效。举个简单的例子,假如项目中有spring-core的依赖,那么spring-core不管是在编译,测试,还是运行都会被用到,因此spring-core必须是编译范围(构件默认的是编译范围,所以依赖范围是编译范围的无须显示指定)

test

顾名思义就是针对于测试的,使用此依赖范围的依赖,只对测试classpath有效,在编译主代码和项目运行时,都将无法使用该依赖。最典型的例子就是 Junit,,构件在测试时才需要,所以它的依赖范围是测试,因此它的依赖范围需要显示指定为<scope>test</scope> ,当然不显示指定依赖范围也不会报错,但是该依赖会被加入到编译和运行的classpath中,造成不必要的浪费 。

provided

使用该依赖范围的maven依赖,只对编译和测试的classpath有效,对运行的classpath无效。典型的例子就是servlet-api, 编译和测试该项目的时候需要该依赖,但是在运行时,web容器已经提供的该依赖,所以运行时就不再需要此依赖,如果不显示指定该依赖范围,并且容器依赖的版本和maven依赖的版本不一致的话,可能会引起版本冲突,造成不良影响。

runtime

使用该依赖范围的maven依赖,只对测试和运行的classpath有效,对编译的classpath无效,典型例子就是JDBC的驱动实现,项目主代码编译的时候只需要JDK提供的JDBC接口,只有在测试和运行的时候才需要实现上述接口的具体JDBC驱动。

system

该依赖与classpath的关系与provided依赖范围完全一致,但是系统依赖范围必须通过配置systemPath元素来显示指定依赖文件的路径,此类依赖不是由maven仓库解析的,而且往往与本机系统绑定,可能造成构件的不可移植,因此谨慎使用,systemPath元素可以引用环境变量

system

该依赖范围不会对三种classpath产生影响,该依赖范围只能与dependencyManagement元素配合使用,其功能为将目标pom文件中dependencyManagement的配置导入合并到当前pom的dependencyManagement中

Maven依赖的传递性

什么是依赖传递性

有时候我们在pom.xml文件中引入的依赖,其本身就需要依赖于其他的依赖,这时候我们不需要去考虑这些依赖,Maven会解析各个直接依赖的pom,将那些必要的间接依赖,以传递性依赖的形式引入到当前的项目中。

通过传递性依赖,我们可以在pom.xml文件中少写不少的依赖配置

传递性依赖的依赖范围

假如当前项目为A,A依赖于B,B依赖于C。此时称A对于B是第一直接依赖,B对于C是第二直接依赖,而A对于C是传递性依赖。只要知道B在A项目中的scope,就可以知道C在A中的scope。其依赖范围如下:

表格的第一列是B在A中的依赖范围,第一行是C在B中的依赖范围,交叉的格子是C在A中的依赖范围

 

compile

provided

test

runtime

compile

compile

-

-

runtime

provided

provided

provided

-

provided

runtime

runtime

-

-

runtime

test

test

-

-

test

总结

  • 当C在B中的scope为test时,A不依赖C,C直接被丢弃

  • 当C在B中的scope为provided时,只有当B在A中的scope也是provided时,A才会依赖C,这时候C在A的scope是provided

  • 当C在B中的scope为compile或runtime时,A依赖C,此时C在A中的scope继承自B在A的scope。注意,如果C的scope是runtime,B的scope是compile,此时C在A的scope是runtime,而不是compile

Maven依赖冲突

什么是依赖冲突

在 Maven 项目中,依赖通常被定义在项目的 pom.xml 文件中。当多个依赖项引入了不同版本的相同库时,就会发生依赖冲突。这可能是因为项目的直接依赖和间接依赖导致了同一库的多个版本存在于类路径中。每个显式声明的类包都会依赖于一些其它的隐式类包,这些隐式的类包会被maven间接引入进来,从而造成类包冲突。

依赖冲突的解决方案

版本锁定

在父工程中使用dependencyManagement 进行版本锁定,dependencyManagement可以统一管理整个项目的版本号,确保应用的各个项目的依赖和版本一致。 dependencyManagement只是声明依赖,并不自动实现引入,因此子项目需要显示的声明需要用的依赖,便可以忽略版本号。如果排斥父工程中定义的版本号,可以显示的进行版本号声明。

子工程使用父工程指定的版本号
<artifactId>maven_parent</artifactId>

<modules>
  <module>maven_son</module>
</modules>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</dependencyManagement >
<artifactId>maven_son</artifactId>

<parent>
  <groupId>com.darren</groupId>
  <artifactId>maven_parent</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</parent>

<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>compile</scope>
  </dependency>
</dependencies>
子工程使用自定义的版本号
<artifactId>maven_parent</artifactId>

<modules>
  <module>maven_son</module>
</modules>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.13.2</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</dependencyManagement >
<artifactId>maven_son</artifactId>

<parent>
  <groupId>com.darren</groupId>
  <artifactId>maven_parent</artifactId>
  <version>1.0.0-SNAPSHOT</version>
</parent>

<dependencies>
  <dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
    <scope>compile</scope>
  </dependency>
</dependencies>
父工程不使用<dependencyManagement>标签,则子工程跟父工程完全保持一致
短路径优先

引入路径短者优先,顾名思义,当一个间接依赖存在多条引入路径时,引入路径短的会被解析使用。

声明优先

如果存在短路径,则优先选择短路径,如果路径相同的情况下,先声明者优先,POM 文件中依赖声明的顺序决定了间接依赖会不会被解析使用,顺序靠前的优先使用。

特殊优先(后来者优先)

同一个pom.xml文件中进行了多次依赖jar包不同版本的配置,后面的覆盖前面的配置。

可选依赖

B项目可选择是否传递间接依赖junit_4.13,主动权在当前项目B中。如果当前项目被依赖到其它项目中,当前项目可以拒绝交出间接依赖项。例如A添加了B的依赖,B可以自主设置其依赖项junit_4.13是否被间接传递。<optional>true</optional> 为不传递间接依赖,那么在A项目中就没有junit_4.13的依赖。默认是false,是传递间接依赖。

排除依赖

是当前项目是否主动断开其依赖项目的间接依赖。也就是控制当前项目是否使用其直接依赖传递下来的接间依赖。在A项目中添加B项目的依赖,但不要B项目中的junit_4.13的依赖,可以选择排除依赖。这样可以保证当前项目依赖的纯净性。 排除依赖使用 exclusions 元素排除依赖,说明如下:

  1. exclusions 元素下可以包含若干个 exclusion 子元素,用于排除若干个间接依赖,该元素包含两个子元素:groupId 和 artifactId,用来确定需要排除的间接依赖的坐标信息

  2. exclusion 元素中只需要设置 groupId 和 artifactId 就可以确定需要排除的依赖,无需指定版本version。

<dependencies>
  <dependency>
    <groupId>com.darren</groupId>
    <artifactId>maven_son</artifactId>
    <version>1.0-SNAPSHOT</version>
    <scope>compile</scope>
    <exclusions>
      <exclusion>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
      </exclusion>
    </exclusions>
  </dependency>
</<dependencies>>
可选依赖和排除依赖的区别

排除依赖和可选依赖都能在项目中将间接依赖排除在外,但两者实现机制却完全不一样。

  1. 可选依赖是自己决定是否向外提供间接依赖(maven_03设置拒绝提供间接依赖junit)

  2. 排除依赖是主动拒绝添加直接依赖关联的间接依赖(maven_02项目设置排除maven_03的junit依赖)

  3. 可选依赖的优先级高于排除依赖

  4. 若对于同一个间接依赖同时使用排除依赖和可选依赖进行设置,那么可选依赖的取值必须为 false,否则排除依赖无法生效。

资源文件的指定

src/main/java 和 src/test/java 这两个目录中的所有.java 文件会分别在 comile 和 test-comiple 阶段被编译,编译结果分别放到了 target/classes 和 targe/test-classes 目录中,但是这两个目录中的其他文件(后缀是.properties或.xml等文件)都会被忽略掉(编译后丢失),如果需要把 src 目录下的除.java之外的文件包放到 target/classes 目录,作为输出的 jar 一部分。需要指定资源文件位置。以下内容放到<build>标签中。简单来说就是在resources目录下的.properties文件和.xml文件编译时不丢失,但resources目录外的.properties文件和*.xml文件会丢失,所以要指定位置,保证编译后文件都在。

<!-- 声明项目的目录结构和管理插件之类的事情 -->
<build>
  <!-- 描述项目中资源的位置 -->
  <resources>
    <resource>
      <directory>src/main/java</directory>
      <!-- 包括哪些文件参与打包 -->
      <includes>
        <include>**/*.properties</include>
        <include>**/*.xml</include>
      </includes>
      <!-- 排除哪些文件不参与打包 -->
      <excludes>
        <exclude>**/*.txt</exclude>
        <exclude>**/*.doc</exclude>
      </excludes>
    </resource>
  </resources>
</build>