20221128 Maven - 尚硅谷(9-10)

9. 重新认识 Maven

Maven 的完整功能

在入门的时候我们介绍说 Maven 是一款『构建管理』和『依赖管理』的工具。但事实上这只是 Maven 的一部分功能。

Maven 本身的产品定位是一款『项目管理工具

项目管理功能的具体体现

下面是 spring-boot-starter 的 POM 文件,可以看到:除了我们熟悉的坐标标签、dependencies 标签,还有 description、url、organization、licenses、developers、scm、issueManagement 等这些描述项目信息的标签。

scm:Software configuration management 软件配置管理

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <!-- This module was also published with a richer model, Gradle metadata,  -->
  <!-- which should be used instead. Do not delete the following line which  -->
  <!-- is to indicate to Gradle or any Gradle module metadata file consumer  -->
  <!-- that they should prefer consuming it instead. -->
  <!-- do_not_remove: published-with-gradle-metadata -->
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter</artifactId>
  <version>2.6.12</version>
  <name>spring-boot-starter</name>
  <description>Core starter, including auto-configuration support, logging and YAML</description>
  <url>https://spring.io/projects/spring-boot</url>
  <organization>
    <name>Pivotal Software, Inc.</name>
    <url>https://spring.io</url>
  </organization>
  <licenses>
    <license>
      <name>Apache License, Version 2.0</name>
      <url>https://www.apache.org/licenses/LICENSE-2.0</url>
    </license>
  </licenses>
  <developers>
    <developer>
      <name>Pivotal</name>
      <email>info@pivotal.io</email>
      <organization>Pivotal Software, Inc.</organization>
      <organizationUrl>https://www.spring.io</organizationUrl>
    </developer>
  </developers>
  <scm>
    <connection>scm:git:git://github.com/spring-projects/spring-boot.git</connection>
    <developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git</developerConnection>
    <url>https://github.com/spring-projects/spring-boot</url>
  </scm>
  <issueManagement>
    <system>GitHub</system>
    <url>https://github.com/spring-projects/spring-boot/issues</url>
  </issueManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot</artifactId>
      <version>2.6.12</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
      <version>2.6.12</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
      <version>2.6.12</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>jakarta.annotation</groupId>
      <artifactId>jakarta.annotation-api</artifactId>
      <version>1.3.5</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>5.3.23</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.yaml</groupId>
      <artifactId>snakeyaml</artifactId>
      <version>1.29</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

所以从『项目管理』的角度来看,Maven 提供了如下这些功能:

  • 项目对象模型(POM):将整个项目本身抽象、封装为应用程序中的一个对象,以便于管理和操作。

  • 全局性构建逻辑重用:Maven 对整个构建过程进行封装之后,程序员只需要指定配置信息即可完成构建。让构建过程从 Ant 的『编程式』升级到了 Maven 的『声明式』。

  • 构件的标准集合:在 Maven 提供的标准框架体系内,所有的构件都可以按照统一的规范生成和使用。

  • 构件关系定义:Maven 定义了构件之间的三种基本关系,让大型应用系统可以使用 Maven 来进行管理

    • 继承关系:通过从上到下的继承关系,将各个子构件中的重复信息提取到父构件中统一管理

    • 聚合关系:将多个构件聚合为一个整体,便于统一操作

    • 依赖关系:Maven 定义了依赖的范围、依赖的传递、依赖的排除、版本仲裁机制等一系列规范和标准,让大型项目可以有序容纳数百甚至更多依赖

  • 插件目标系统:Maven 核心程序定义抽象的生命周期,然后将插件的目标绑定到生命周期中的特定阶段,实现了标准和具体实现解耦合,让 Maven 程序极具扩展性

  • 项目描述信息的维护:我们不仅可以在 POM 中声明项目描述信息,更可以将整个项目相关信息收集起来生成 HTML 页面组成的一个可以直接访问的站点。这些项目描述信息包括:

    • 公司或组织信息

    • 项目许可证

      • 开发成员信息
    • issue 管理信息

    • SCM 信息

POM 的四个层次

超级 POM

Maven 在构建过程中有很多默认的设定。例如:源文件存放的目录、测试源文件存放的目录、构建输出的目录……等等。但是其实这些要素也都是被 Maven 定义过的。定义的位置就是:超级 POM

我们自己的 POM 即使没有明确指定一个父工程(父 POM),其实也默认继承了超级 POM。就好比一个 Java 类默认继承了 Object 类

Maven 3.5.4 的超级 POM:

<project>
  <modelVersion>4.0.0</modelVersion>

  <repositories>
    <repository>
      <id>central</id>
      <name>Central Repository</name>
      <url>https://repo.maven.apache.org/maven2</url>
      <layout>default</layout>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
    </repository>
  </repositories>

  <pluginRepositories>
    <pluginRepository>
      <id>central</id>
      <name>Central Repository</name>
      <url>https://repo.maven.apache.org/maven2</url>
      <layout>default</layout>
      <snapshots>
        <enabled>false</enabled>
      </snapshots>
      <releases>
        <updatePolicy>never</updatePolicy>
      </releases>
    </pluginRepository>
  </pluginRepositories>

  <build>
    <directory>${project.basedir}/target</directory>
    <outputDirectory>${project.build.directory}/classes</outputDirectory>
    <finalName>${project.artifactId}-${project.version}</finalName>
    <testOutputDirectory>${project.build.directory}/test-classes</testOutputDirectory>
    <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>
    <scriptSourceDirectory>${project.basedir}/src/main/scripts</scriptSourceDirectory>
    <testSourceDirectory>${project.basedir}/src/test/java</testSourceDirectory>
    <resources>
      <resource>
        <directory>${project.basedir}/src/main/resources</directory>
      </resource>
    </resources>
    <testResources>
      <testResource>
        <directory>${project.basedir}/src/test/resources</directory>
      </testResource>
    </testResources>
    <pluginManagement>
      <!-- NOTE: These plugins will be removed from future versions of the super POM -->
      <!-- They are kept for the moment as they are very unlikely to conflict with lifecycle mappings (MNG-4453) -->
      <plugins>
        <plugin>
          <artifactId>maven-antrun-plugin</artifactId>
          <version>1.3</version>
        </plugin>
        <plugin>
          <artifactId>maven-assembly-plugin</artifactId>
          <version>2.2-beta-5</version>
        </plugin>
        <plugin>
          <artifactId>maven-dependency-plugin</artifactId>
          <version>2.8</version>
        </plugin>
        <plugin>
          <artifactId>maven-release-plugin</artifactId>
          <version>2.5.3</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

  <reporting>
    <outputDirectory>${project.build.directory}/site</outputDirectory>
  </reporting>

  <profiles>
    <!-- NOTE: The release profile will be removed from future versions of the super POM -->
    <profile>
      <id>release-profile</id>

      <activation>
        <property>
          <name>performRelease</name>
          <value>true</value>
        </property>
      </activation>

      <build>
        <plugins>
          <plugin>
            <inherited>true</inherited>
            <artifactId>maven-source-plugin</artifactId>
            <executions>
              <execution>
                <id>attach-sources</id>
                <goals>
                  <goal>jar-no-fork</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <inherited>true</inherited>
            <artifactId>maven-javadoc-plugin</artifactId>
            <executions>
              <execution>
                <id>attach-javadocs</id>
                <goals>
                  <goal>jar</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <inherited>true</inherited>
            <artifactId>maven-deploy-plugin</artifactId>
            <configuration>
              <updateReleaseInfo>true</updateReleaseInfo>
            </configuration>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>

</project>

父 POM

和 Java 类一样,POM 之间其实也是单继承的。如果我们给一个 POM 指定了父 POM,那么继承关系如下图所示:

有效 POM

有效 POM 英文翻译为 effective POM ,它的概念是这样的——在 POM 的继承关系中,子 POM 可以覆盖父 POM 中的配置;如果子 POM 没有覆盖,那么父 POM 中的配置将会被继承。按照这个规则,继承关系中的所有 POM 叠加到一起,就得到了一个最终生效的 POM。

显然 Maven 实际运行过程中,执行构建操作就是按照这个最终生效的 POM 来运行的。这个最终生效的 POM 就是有效 POM,英文叫 effective POM

查看有效 POM

mvn help:effective-pom

四层 POM

  • 超级 POM:所有 POM 默认继承,只是有直接和间接之分。

  • 父 POM:这一层可能没有,可能有一层,也可能有很多层。

  • 当前 pom.xml 配置的 POM:我们最多关注和最多使用的一层。

  • 有效 POM:隐含的一层,但是实际上真正生效的一层。

属性的声明与引用

help 插件的各个目标

Apache Maven Help Plugin

目标 说明
help:active-profiles 列出当前已激活的 profile
help:all-profiles 列出当前工程所有可用 profile
help:describe 描述一个插件和/或 Mojo 的属性
help:effective-pom 以 XML 格式展示有效 POM
help:effective-settings 为当前工程以 XML 格式展示计算得到的 settings 配置
help:evaluate 计算用户在交互模式下给出的 Maven 表达式
help:system 显示平台详细信息列表,如系统属性和环境变量

使用 help:evaluate

mvn help:evaluate
# 系统属性 System.getProperties()
${user.dir}

# 系统环境变量 System.getenv()
${env.SESSIONNAME}

# Maven pom 属性值 <properties>
${maven.compiler.encoding}

# Maven pom project 属性 
${project.artifactId}

# Maven pom 子标签 <properties>
${project.properties}


# Maven pom 列表标签,输出有效pom的内容
${project.dependencies[0]} 


# settings 全局配置,访问 settings.xml 中配置的元素值
${settings.localRepository}

用途

  • 在当前 pom.xml 文件中引用属性

  • 资源过滤功能(profile):在非 Maven 配置文件中引用属性,由 Maven 在处理资源时将引用属性的表达式替换为属性值

build 标签详解

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

所以本质上来说:我们配置的 build 标签都是对超级 POM 配置的叠加。那我们又为什么要在默认配置的基础上叠加呢?很简单,在默认配置无法满足需求的时候定制构建过程。

build 标签组成

定义约定的目录结构
<sourceDirectory>D:\Develop\workspace\study\study-spring-boot\src\main\java</sourceDirectory>
<scriptSourceDirectory>D:\Develop\workspace\study\study-spring-boot\src\main\scripts</scriptSourceDirectory>
<testSourceDirectory>D:\Develop\workspace\study\study-spring-boot\src\test\java</testSourceDirectory>
<outputDirectory>D:\Develop\workspace\study\study-spring-boot\target\classes</outputDirectory>
<testOutputDirectory>D:\Develop\workspace\study\study-spring-boot\target\test-classes</testOutputDirectory>
<resources>
    <resource>
        <filtering>true</filtering>
        <directory>D:\Develop\workspace\study\study-spring-boot\src\main\resources</directory>
        <includes>
            <include>**/application*.yml</include>
            <include>**/application*.yaml</include>
            <include>**/application*.properties</include>
        </includes>
    </resource>
    <resource>
        <directory>D:\Develop\workspace\study\study-spring-boot\src\main\resources</directory>
        <excludes>
            <exclude>**/application*.yml</exclude>
            <exclude>**/application*.yaml</exclude>
            <exclude>**/application*.properties</exclude>
        </excludes>
    </resource>
</resources>
<testResources>
    <testResource>
        <directory>D:\Develop\workspace\study\study-spring-boot\src\test\resources</directory>
    </testResource>
</testResources>
<directory>D:\Develop\workspace\study\study-spring-boot\target</directory>
<finalName>study-spring-boot-0.0.1-SNAPSHOT</finalName>

各个目录的作用如下:

目录名 作用
sourceDirectory 主体源程序存放目录
scriptSourceDirectory 脚本源程序存放目录
testSourceDirectory 测试源程序存放目录
outputDirectory 主体源程序编译结果输出目录
testOutputDirectory 测试源程序编译结果输出目录
resources 主体资源文件存放目录
testResources 测试资源文件存放目录
directory 构建结果输出目录
备用插件管理

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

  • maven-antrun-plugin

  • maven-assembly-plugin

  • maven-dependency-plugin

  • maven-release-plugin

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

被 spring-boot-dependencies 管理的插件信息:

<build>
    <pluginManagement>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <version>2.6.7</version>
        </plugin>
    </pluginManagement>
</build>

子工程使用的插件信息:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
生命周期插件

Introduction to the Build Lifecycle

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <executions>
        <execution>
            <id>default-compile</id>
            <phase>compile</phase>
            <goals>
                <goal>compile</goal>
            </goals>
            <configuration>
                <parameters>true</parameters>
            </configuration>
        </execution>
        <execution>
            <id>default-testCompile</id>
            <phase>test-compile</phase>
            <goals>
                <goal>testCompile</goal>
            </goals>
            <configuration>
                <parameters>true</parameters>
            </configuration>
        </execution>
    </executions>
    <configuration>
        <parameters>true</parameters>
    </configuration>
</plugin>
坐标部分

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

执行部分

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

  • id:指定唯一标识

  • phase:关联的生命周期阶段

  • goals/goal:关联指定生命周期的目标

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

另外,插件目标的执行过程可以进行配置,例如 maven-site-plugin 插件的 site 目标:

<plugin>
    <artifactId>maven-site-plugin</artifactId>
    <version>3.3</version>
    <executions>
        <execution>
            <id>default-site</id>
            <phase>site</phase>
            <goals>
                <goal>site</goal>
            </goals>
            <configuration>
                <outputDirectory>D:\Develop\workspace\study\study-spring-boot\target\site</outputDirectory>
                <reportPlugins>
                    <reportPlugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-project-info-reports-plugin</artifactId>
                    </reportPlugin>
                </reportPlugins>
            </configuration>
        </execution>
        <execution>
            <id>default-deploy</id>
            <phase>site-deploy</phase>
            <goals>
                <goal>deploy</goal>
            </goals>
            <configuration>
                <outputDirectory>D:\Develop\workspace\study\study-spring-boot\target\site</outputDirectory>
                <reportPlugins>
                    <reportPlugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-project-info-reports-plugin</artifactId>
                    </reportPlugin>
                </reportPlugins>
            </configuration>
        </execution>
    </executions>
    <configuration>
        <outputDirectory>D:\Develop\workspace\study\study-spring-boot\target\site</outputDirectory>
        <reportPlugins>
            <reportPlugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-project-info-reports-plugin</artifactId>
            </reportPlugin>
        </reportPlugins>
    </configuration>
</plugin>

configuration 标签内进行配置时使用的标签是插件本身定义的。就以 maven-site-plugin 插件为例,它的核心类是 org.apache.maven.plugins.site.render.SiteMojo ,在这个类中我们看到了 outputDirectory 属性

SiteMojo 的父类是:AbstractSiteRenderingMojo ,在父类中我们看到 reportPlugins 属性

结论:每个插件能够做哪些设置都是各个插件自己规定的,无法一概而论

典型应用:指定 JDK 版本

<!-- 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>

maven-compiler-plugin 插件的核心类是 org.apache.maven.plugin.compiler.CompilerMojo ,通过 Maven 引入

<dependency>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
</dependency>

依赖信息可以通过有效POM查看

通过查看源码,可以看到对应的属性值和默认值,例如 sourcetargetencoding

以及关联的属性配置 maven.compiler.sourcemaven.compiler.target

两种配置方式比较:

  • settings.xml 中配置:仅在本地生效,如果脱离当前 settings.xml 能够覆盖的范围,则无法生效。

  • 在当前 Maven 工程 pom.xml 中配置:无论在哪个环境执行编译等构建操作都有效。

典型应用: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 这个插件来定制打包行为。

有效 POM 中:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>2.6.7</version>
    <executions>
        <execution>
            <id>repackage</id>
            <goals>
                <goal>repackage</goal>
            </goals>
            <configuration>
                <mainClass>${start-class}</mainClass>
            </configuration>
        </execution>
    </executions>
    <configuration>
        <mainClass>${start-class}</mainClass>
    </configuration>
</plugin>

引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <version>2.6.7</version>
</dependency>

查看核心类 org.springframework.boot.maven.RepackageMojo ,其父类中包含属性 mainClass

插件的七个目标:

目标名称
spring-boot:build-image Package an application into a OCI image using a buildpack.
spring-boot:build-info Generate a build-info.properties file based on the content of the current MavenProject.
spring-boot:help Display help information on spring-boot-maven-plugin. Call mvn spring-boot:help -Ddetail=true -Dgoal= to display parameter details.
spring-boot:repackage Repackage existing JAR and WAR archives so that they can be executed from the command line using java -jar. With layout=NONE can also be used simply to package a JAR with nested dependencies (and no main class, so not executable).
spring-boot:run Run an application in place.
spring-boot:start Start a spring application. Contrary to the run goal, this does not block and allows other goals to operate on the application. This goal is typically used in integration test scenario where the application is started before a test suite and stopped after.
spring-boot:stop Stop an application that has been started by the 'start' goal. Typically invoked once a test suite has completed.

小结

通常需要用到 build 标签的时候底层都会帮我们封装好,需要我们配置的地方不多。即使有些地方需要我们配置,也不会真的我们自己去写,把现成的案例复制过来就行。

所以对 build 标签来说,我们的掌握要求就是:能大致看懂就行。

依赖配置补充

Introduction to the Dependency Mechanism

依赖范围 scope

import

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

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

<dependencyManagement>
    <dependencies>

        <!-- SpringBoot 依赖导入 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>2.3.6.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!-- SpringCloud 依赖导入 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR9</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <!-- SpringCloud Alibaba 依赖导入 -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-dependencies</artifactId>
            <version>2.2.6.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

import 依赖范围使用要求:

  • 打包类型必须是 pom

  • 必须放在 dependencyManagement 中

system

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

<dependency>
    <groupId>com.atguigu.maven</groupId>
    <artifactId>atguigu-maven-test-aaa</artifactId>
    <version>1.0-SNAPSHOT</version>
    <systemPath>D:\tempare\atguigu-maven-test-aaa-1.0-SNAPSHOT.jar</systemPath>
    <scope>system</scope>
</dependency>

但是很明显:这样引入依赖完全不具有可移植性,所以不要使用。如果需要引入体系外 jar 包我们后面会讲专门的办法。

runtime

专门用于编译时不需要,但是运行时需要的 jar 包。

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

<!--热部署 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>

可选依赖 optional

可选其实就是『可有可无』

Project X 依赖 Project A,A 中一部分 X 用不到的代码依赖了 B,那么对 X 来说 B 就是『可有可无』的。

版本仲裁

1. 最短路径优先

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

2. 路径相同时先声明者优先

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

小结

其实 Maven 的版本仲裁机制只是在没有人为干预的情况下,自主决定 jar 包版本的一个办法。而实际上我们要使用具体的哪一个版本,还要取决于项目中的实际情况。

所以在项目正常运行的情况下,jar 包版本可以由 Maven 仲裁,不必我们操心;而发生冲突时 Maven 仲裁决定的版本无法满足要求,此时就应该由程序员明确指定 jar 包版本。

Maven 自定义插件

其实实际开发中几乎没有什么场景需要我们开发自定义 Maven 插件,所以本节只是通过这个角度帮助我们更好的理解插件的目标和生命周期阶段之间的关系。

插件开发

1. 创建工程

2. 设定打包方式
<packaging>maven-plugin</packaging>
3. 引入依赖

下面两种方式二选一:

[1] 将来在文档注释中使用注解
<dependency>
    <groupId>org.apache.maven</groupId>
    <artifactId>maven-plugin-api</artifactId>
    <version>3.5.2</version>
</dependency>
[2] 将来直接使用注解
<dependency>
    <groupId>org.apache.maven.plugin-tools</groupId>
    <artifactId>maven-plugin-annotations</artifactId>
    <version>3.5.2</version>
</dependency>
4. 创建 Mojo 类

Mojo 类是一个 Maven 插件的核心类。

Mojo 这个单词的意思是:Maven Old Java Object,其实 mojo 这个单词本身包含魔力;符咒(袋);护身符; (人的)魅力的含义,Maven 用 Mojo 是因为它是对 POJO 开的一个小玩笑。

每一个 Mojo 都需要实现 org.apache.maven.plugin.Mojo 接口

我们实现 Mojo 接口比较困难,幸好可以继承 AbstractMojo,此时我们只要实现 execute() 这一个方法即可。

public class MyHelloPlugin extends AbstractMojo {
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info("---> This is my first maven plugin. <---");
    }
}

插件配置

1. Mojo 类中的配置
[1] 文档注释中用注解

对应的 pom.xml 中的依赖: maven-plugin-api

[2] 直接在类上标记注解

对应 pom.xml 中的依赖:maven-plugin-annotations

// name 属性:指定目标名称
@Mojo(name = "firstBlood")
public class MyPluginOfFistBlood extends AbstractMojo {
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException {
        getLog().info("---> first blood <---");
    }
}
2. 安装插件

要在后续使用插件,就必须至少将插件安装到本地仓库

3. 注册插件

我们需要将插件坐标中的 groupId 部分注册到全局 settings.xml 中。

这样注册后,不需要在项目内使用 <plugin> 注册插件就可以使用命令 mvn 执行

<pluginGroups>
    <!-- pluginGroup
        | Specifies a further group identifier to use for plugin lookup.
        <pluginGroup>com.your.plugins</pluginGroup>
    -->
    <pluginGroup>com.atguigu.maven</pluginGroup>
</pluginGroups>

使用插件

1. 识别插件前缀

Maven 根据插件的 artifactId 来识别插件前缀。例如下面两种情况:

[1] 前置匹配
  • 匹配规则:${prefix}-maven-plugin

  • artifactId:hello-maven-plugin

  • 前缀:hello

[2] 中间匹配
  • 匹配规则:maven-${prefix}-plugin

  • artifactId:maven-good-plugin

  • 前缀:good

2. 在命令行直接用
mvn hello:sayHello
3. 配置到 build 标签里
<build>
    <plugins>
        <plugin>
            <groupId>com.atguigu.maven</groupId>
            <artifactId>hello-maven-plugin</artifactId>
            <version>1.0-SNAPSHOT</version>
            <executions>
                <execution>
                    <id>hello</id>
                    <!-- 指定和目标关联的生命周期阶段 -->
                    <phase>clean</phase>
                    <goals>
                        <goal>sayHello</goal>
                    </goals>
                </execution>
                <execution>
                    <id>blood</id>
                    <phase>validate</phase>
                    <goals>
                        <goal>firstBlood</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

profile 详解

profile 概述

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

项目的不同运行环境

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

  • 开发环境:供不同开发工程师开发的各个模块之间互相调用、访问;内部使用

  • 测试环境:供测试工程师对项目的各个模块进行功能测试;内部使用

  • 生产环境:供最终用户访问——所以这是正式的运行环境,对外提供服务

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

  • MySQL

  • Redis

  • ElasticSearch

  • RabbitMQ

  • FastDFS

  • Nginx

  • Tomcat

  • ……

就拿其中的 MySQL 来说,不同环境下的访问参数肯定完全不同

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

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

所以 Maven 的 profile 我们了解一下即可,不必深究。

profile 声明和使用的基本逻辑
  • 首先为每一个环境声明一个 profile

    • 环境 A:profile A

    • 环境 B:profile B

    • 环境 C:profile C

    • ……

  • 然后激活某一个 profile

默认 profile

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

project 标签下除了 modelVersion 和坐标标签之外,其它标签都可以配置到 profile 中。

profile 配置

外部视角:配置文件

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

  • settings.xml:全局生效。其中我们最熟悉的就是配置 JDK 1.8。

  • pom.xml:当前 POM 生效

内部实现:具体标签

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

[1] profiles/profile 标签

由于 profile 天然代表众多可选配置中的一个,所以由复数形式的 profiles 标签统一管理。

由于 profile 标签覆盖了 pom.xml 中的默认配置,所以 profiles 标签通常是 pom.xml 中的最后一个标签。

[2] id 标签

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

[3] 其它允许出现的标签

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

  • build

  • defaultGoal

    • finalName

    • resources

    • testResources

    • plugins

  • reporting

  • modules

  • dependencies

  • dependencyManagement

  • repositories

  • pluginRepositories

  • properties

激活 profile

默认配置默认被激活

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

基于环境信息激活

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

大家可以参考下面的标签结构:

<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 时被激活:

<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

命令行激活
[1] 列出活动的 profile
# 列出所有激活的 profile,以及它们在哪里定义
mvn help:active-profiles
[2] 指定某个具体 profile
mvn compile -P<profile id>

资源属性过滤

Maven 为了能够通过 profile 实现各不同运行环境切换,提供了一种『资源属性过滤』的机制。通过属性替换实现不同环境使用不同的参数。

示例
[1] 配置 profile
<profiles>
    <profile>
        <id>devJDBCProfile</id>
        <properties>
            <dev.jdbc.user>root</dev.jdbc.user>
            <dev.jdbc.password>atguigu</dev.jdbc.password>
            <dev.jdbc.url>http://localhost:3306/db_good</dev.jdbc.url>
            <dev.jdbc.driver>com.mysql.jdbc.Driver</dev.jdbc.driver>
        </properties>
        <build>
            <resources>
                <resource>
                    <!-- 表示为这里指定的目录开启资源过滤功能 -->
                    <directory>src/main/resources</directory>
                    <!-- 将资源过滤功能打开 -->
                    <filtering>true</filtering>
                </resource>
            </resources>
        </build>
    </profile>
</profiles>
[2] 创建待处理的资源文件
dev.user=${dev.jdbc.user}
dev.password=${dev.jdbc.password}
dev.url=${dev.jdbc.url}
dev.driver=${dev.jdbc.driver}
[3] 执行处理资源命令
mvn clean resources:resources -PdevJDBCProfile
[4] 找到处理得到的资源文件

在 target 目录下查看文件内容

[5] 延伸

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

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

  • excludes:指定执行 resource 阶段时要排除的资源

这里只是以 properties 文件为例,并不是只能处理 properties 文件。

10. 生产实践

搭建 Maven 私服:Nexus

repository 和 mirror 标签的区别

internal repository 和 mirror 是两码事。前者本身是一个 repository ,可以和其它 repository 一起提供服务,比如它可以用来提供公司内部的 maven 构件;而后者本身并不是 repository ,它只是远程 repository 的网络加速器。

不过,很多 internal repository 搭建工具往往也提供 mirror 服务,比如 Nexus 就可以让同一个 URL ,既用作 internal repository ,又使它成为所有 repository 的 mirror

jar 包冲突问题

表现形式

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

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

抛异常:找不到类

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

  • java.lang.ClassNotFoundException:编译过程中找不到类

  • java.lang.NoClassDefFoundError:运行过程中找不到类

  • java.lang.NoSuchMethodError:运行过程中找不到方法

  • java.lang.LinkageError:不同类加载器分别加载的多个类有相同的全限定名

举个例子:

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。

抛异常:找不到方法

程序找不到符合预期的方法。这种情况多见于通过反射调用方法,所以经常会导致:java.lang.NoSuchMethodError 。比如 antlr:antlr:x.x.x 这个包中有一个接口:antlr.collections.AST

版本 getLine()方法
2.7.2
2.7.6
没报错但结果不对

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

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

本质

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

  • 同一jar包的不同版本

  • 不同jar包中包含同名类

不同jar包中包含同名类,拿 netty 来举个例子

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

这两个『不同的包』里面又有很多『全限定名相同』的类

如果全限定名相同,类中的代码也完全相同,那么用着也行。问题是如果『全限定名相同』,但是『代码不同』,那可太坑了。

解决办法

基本思路无非是这么两步:

  • 第一步:把彼此冲突的 jar 包找到

  • 第二步:在冲突的 jar 包中选定一个。具体做法无非是通过 exclusions 排除依赖,或是明确声明依赖。

IDEA 的 Maven Helper 插件

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

新版本 IDEA 自带 Maven 依赖分析功能,和插件功能相似,可以直接使用

Maven 的 enforcer 插件

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

<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>
mvn clean package enforcer:enforce

体系外 jar 包引入

将该 jar 包安装到 Maven 仓库

mvn install:install-file -Dfile=[体系外 jar 包路径] \
-DgroupId=[给体系外 jar 包强行设定坐标] \
-DartifactId=[给体系外 jar 包强行设定坐标] \
-Dversion=1 \
-Dpackage=jar

Windows 系统下使用 ^ 符号换行;Linux 系统用 \

举例:Windows 系统下的外部 jar 包

mvn install:install-file ^
-Dfile=D:\idea2019workspace\atguigu-maven-outer\out\artifacts\atguigu_maven_outer\atguigu-maven-outer.jar ^
-DgroupId=com.atguigu.maven ^
-DartifactId=atguigu-maven-outer ^
-Dversion=1 ^
-Dpackaging=jar

引入 Maven 依赖:

<dependency>
    <groupId>com.atguigu.maven</groupId>
    <artifactId>atguigu-maven-outer</artifactId>
    <version>1</version>
</dependency>
posted @ 2022-11-29 19:43  流星<。)#)))≦  阅读(71)  评论(0编辑  收藏  举报