第 2 章 Maven 代码构建系统

作者: admin 分类: ARM64 Linux CICD 研发 发布时间: 2022-07-19 12:08

在了解 Maven 之前,我们先来看看一个 Java 项目需要的东西。首先,我们需要确定引入哪些依赖包。例如,如果我们需要用到 commons logging,我们就必须把 commons logging 的 jar 包放入 classpath。如果我们还需要 log4j,就需要把 log4j 相关的 jar 包都放到 classpath 中。这些就是依赖包的管理。

其次,我们要确定项目的目录结构。例如,src 目录存放 Java 源码,resources 目录存放配置文件,bin 目录存放编译生成的 .class 文件。

此外,我们还需要配置环境,例如 JDK 的版本,编译打包的流程,当前代码的版本号。

最后,除了使用 Eclipse 这样的 IDE 进行编译外,我们还必须能通过命令行工具进行编译,才能够让项目在一个独立的服务器上编译、测试、部署。

这些工作难度不大,但是非常琐碎且耗时。如果每一个项目都自己搞一套配置,肯定会一团糟。我们需要的是一个标准化的 Java 项目管理和构建工具。

这个时候 Maven 出现了。

2.1、Apache Maven 概念

Maven 是一个项目管理和构建自动化工具。但是对于我们程序员来说,我们最关心的是它的项目构建功能。所以这里我们介绍的就是怎样用 maven 来满足我们项目的日常需要。 Maven 使用惯例优于配置的原则 。它要求在没有定制之前,所有的项目都有如下的结构:

目录 目的
${basedir} 存放 pom.xml 和所有的子目录
${basedir}/src/main/java 项目的 java 源代码
${basedir}/src/main/resources 项目的资源,比如说 property 文件
${basedir}/src/test/java 项目的测试类,比如说 JUnit 代码
${basedir}/src/test/resources 测试使用的资源

一个 maven 项目在默认情况下会产生 JAR 文件,另外 ,编译后的 classes 会放在 ${basedir}/target/classes 下面, JAR 文件会放在 ${basedir}/target 下面。

总结起来 Maven 就是专门为 Java 项目打造的管理和构建工具,它的主要功能有:

  • 提供了一套标准化的项目结构;
  • 提供了一套标准化的构建流程(编译,测试,打包,发布……);
  • 提供了一套依赖管理机制。

2.2、Maven 安装

2.2.1、安装 Java JDK 环境

下载地址:https://www.oracle.com/java/technologies/downloads/

[root@armcli ~]# wget https://download.oracle.com/java/17/latest/jdk-17_linux-aarch64_bin.rpm
[root@armcli ~]# ls
jdk-17_linux-aarch64_bin.rpm
[root@armcli ~]# rpm -ivh jdk-17_linux-aarch64_bin.rpm
warning: jdk-17_linux-aarch64_bin.rpm: Header V3 RSA/SHA256 Signature, key ID ec551f03: NOKEY
Preparing...                          ################################# [100%]
Updating / installing...
   1:jdk-17-2000:17.0.2-ga            ################################# [100%]
[root@armcli ~]#
[root@armcli ~]# java -version
java version "17.0.2" 2022-01-18 LTS
Java(TM) SE Runtime Environment (build 17.0.2+8-LTS-86)
Java HotSpot(TM) 64-Bit Server VM (build 17.0.2+8-LTS-86, mixed mode, sharing)
[root@armcli ~]#

2.2.2、Maven 安装

下载地址:https://maven.apache.org/download.cgi

# 下载安装
[root@armcli ~]# wget https://dlcdn.apache.org/maven/maven-3/3.8.4/binaries/apache-maven-3.8.4-bin.tar.gz
[root@armcli ~]# tar xf apache-maven-3.8.4-bin.tar.gz -C /usr/local/
[root@armcli ~]# ln -svf /usr/local/apache-maven-3.8.4/ /usr/local/maven
‘/usr/local/maven’ -> ‘/usr/local/apache-maven-3.8.4/’

# 配置环境
[root@armcli ~]# vim /etc/profile.d/maven.sh
export MAVEN_HOME=/usr/local/maven
export PATH=$PATH:$MAVEN_HOME/bin
[root@armcli ~]# source /etc/profile.d/maven.sh

# 最后验证是否安装成功,出现如下信息,说明安装成功
[root@armcli ~]# mvn --version
Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537)
Maven home: /usr/local/maven
Java version: 17.0.2, vendor: Oracle Corporation, runtime: /usr/java/jdk-17.0.2
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "4.18.0-193.28.1.el7.aarch64", arch: "aarch64", family: "unix"
[root@armcli ~]#

2.3、Maven 使用

2.3.1、建立 Hello World 项目

[root@armcli ~]# mvn archetype:generate -DgroupId=com.greatwall.helloworld -DartifactId=helloworld -Dpackage=com.greatwall.helloworld -Dversion=1.0-SNAPSHOT -DarchetypeCatalog=internal
  • archetype:generate 目标会列出一系列的 archetype 让你选择。
    • Archetype 可以理解成项目的模型。
    • Maven 为我们提供了很多种的项目模型,包括从简单的 Swing 到复杂的 Web 应用。
    • 默认使用 maven-archetype-quickstart 模型,连续回车即可。
  • 项目属性是我们在命令行中用 -D 选项指定的。该选项使用 -Dname=value 的格式。
  • 如果你是第一次运行 maven,你需要 Internet 连接,因为 maven 需要从网上下载需要的插件。

我们看一下 maven 给我们建立的文件目录结构:

[root@armcli ~]# tree helloworld/
helloworld/
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── com
    │           └── greatwall
    │               └── helloworld
    │                   └── App.java
    └── test
        └── java
            └── com
                └── greatwall
                    └── helloworld
                        └── AppTest.java

11 directories, 3 files
[root@armcli ~]#
  • mavenarchetype 插件建立了一个 helloworld 目录,这个名字来自 artifactId
  • 这个目录下面,有一个 Project Object Model(POM) 文件 pom.xml 。这个文件用于描述项目,配置插件和管理依赖关系。
  • 源代码和资源文件放在 src/main 下面,而测试代码和资源放在 src/test 下面。

Maven 已经为我们建立了一个 App.java 文件:

package com.greatwall.helloworld;

/**
 * Hello world!
 *
 */
public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
    }
}

2.3.2、构建 Hello World 程序

[root@armcli ~]# cd helloworld/
[root@armcli helloworld]# mvn package
[INFO] Scanning for projects...
[INFO] 
[INFO] ----------------< com.greatwall.helloworld:helloworld >-----------------
[INFO] Building helloworld 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
......
[INFO] Building jar: /root/helloworld/target/helloworld-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  32.810 s
[INFO] Finished at: 2022-03-14T13:44:49+08:00
[INFO] ------------------------------------------------------------------------
  • 第一次运行 maven 的时候,它会从网上的 maven(repository) 下载需要的程序,存放在你电脑的本地库 (local repository) 中,所以这个时候你需要有 Internet 连接。
  • Maven 默认的本地库是 ~/.m2/repository/,在 Windows 下是 %USER_HOME%\.m2\repository\

构建过程中可能会出现以下报错:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project helloworld: Compilation failure: Compilation failure: 
[ERROR] Source option 5 is no longer supported. Use 7 or later.
[ERROR] Target option 5 is no longer supported. Use 7 or later.

报错分析:由于我们这个项目是基于老的 Archetype 项目模型建立的,所以这种不兼容的问题很常见,这种情况下,我们需要将源和目标的 java-11-openjdk 版本传递给 Maven 编译器。

解决方法:通过 pom.xml 项目配置文件传递 java-11-openjdk 版本。

[root@armcli helloworld]# cat pom.xml 
<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>com.greatwall.helloworld</groupId>
  <artifactId>helloworld</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>helloworld</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>17</maven.compiler.source>   # 这里传递
    <maven.compiler.target>17</maven.compiler.target>   # 这里传递
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

最终构建成功后,会得到类似下面的结果:

mavenhelloworld 下面建立了一个新的目录 target/,构建打包后的 jar 文件 helloworld-1.0-SNAPSHOT.jar 就存放在这个目录下。

编译后的 class 文件放在 target/classes/ 目录下面,测试 class 文件放在 target/test-classes/ 目录下面。

[root@armcli helloworld]# tree .
.
├── pom.xml
├── src
│   ├── main
│   │   └── java
│   │       └── com
│   │           └── greatwall
│   │               └── helloworld
│   │                   └── App.java
│   └── test
│       └── java
│           └── com
│               └── greatwall
│                   └── helloworld
│                       └── AppTest.java
└── target
    ├── classes
    │   └── com
    │       └── greatwall
    │           └── helloworld
    │               └── App.class
    ├── generated-sources
    │   └── annotations
    ├── generated-test-sources
    │   └── test-annotations
    ├── helloworld-1.0-SNAPSHOT.jar
    ├── maven-archiver
    │   └── pom.properties
    ├── maven-status
    │   └── maven-compiler-plugin
    │       ├── compile
    │       │   └── default-compile
    │       │       ├── createdFiles.lst
    │       │       └── inputFiles.lst
    │       └── testCompile
    │           └── default-testCompile
    │               ├── createdFiles.lst
    │               └── inputFiles.lst
    ├── surefire-reports
    │   ├── com.greatwall.helloworld.AppTest.txt
    │   └── TEST-com.greatwall.helloworld.AppTest.xml
    └── test-classes
        └── com
            └── greatwall
                └── helloworld
                    └── AppTest.class

32 directories, 13 files

为了验证我们的程序能运行,执行下面的命令:

[root@armcli helloworld]# java -cp target/helloworld-1.0-SNAPSHOT.jar com.greatwall.helloworld.App
Hello World!

运行成功!

2.4、Maven 深入理解

刚才通过一个 helloworld 的小项目对 maven 有了一点初步认识,接下来的一步是要了解 maven 的核心概念,这样才能在使用 maven 的时候游刃有余。

2.4.1、POM (Project Object Model)

一个项目所有的配置都放置在 POM 文件中:定义项目的类型、名字,管理依赖关系,定制插件的行为等等。比如说,你可以配置 compiler 插件让它使用 java 1.5 来编译。

下面拿刚才的 helloworld POM 配置文件进行一些细节说明:

<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>com.greatwall.helloworld</groupId>
  <artifactId>helloworld</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>helloworld</name>
  <url>http://maven.apache.org</url>

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

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

POM 中,groupIdartifactIdpackagingversion 叫作 maven 坐标,它能唯一地确定一个项目。

  • groupId 类似于 Java 的包名,通常是公司或组织名称;
  • artifactId 类似于 Java 的类名,通常是项目名称;
  • version 是项目版本;
  • packaging 是指打包类型,一般来说所有的父级项目的 packaging 都为 pom 类型,还有 jar、war、ear 和 rar 等类型;packaging 默认类型是 jar 类型,经常省略 packaging 参数。

有了 maven 坐标,我们就可以用它来指定我们的项目所依赖的其他项目,插件,或者父项目。一般 maven 坐标写成如下的格式:

groupId:artifactId:packaging:version

像我们的例子就会写成:

com.greatwall.helloworld: helloworld: jar: 1.0-SNAPSHOT

我们再引用其他第三方库的时候,也是通过 3 个关键变量确定。例如,依赖 commons-logging

<dependency>
    <groupId>commons-logging</groupId>
    <artifactId>commons-logging</artifactId>
    <version>1.2</version>
</dependency>
  • 使用 <dependency> 声明一个依赖后,Maven 就会自动下载这个依赖包并把它放到 classpath 中。

我们的 helloworld 示例很简单,但是大项目一般会分成几个子项目。在这种情况下,每个子项目就会有自己的 POM 文件,然后它们会有一个共同的父项目。这样只要构建父项目就能够构建所有的子项目了。子项目的 POM 会继承父项目的 POM。

另外,所有的 POM 都继承了一个 Super-POMSuper-POM 设置了一些默认值,比如在上面提到的默认的目录结构,默认的插件等等,它遵循了惯例优于配置的原则。

所以尽管我们的这个 POM 很简单,但是这只是你看得见的一部分。运行时候的 POM 要复杂的多。 如果你想看到运行时候的 POM 的全部内容的话,可以运行下面的命令:

$ mvn help:effective-pom

2.4.2、Maven 插件

上面 helloworld 项目中,我们用了 mvn archetype:generate 命令来生成一个项目。那么这里的 archetype:generate 是什么意思呢?

  • archetype 是一个插件的名字,generate 是目标(goal)的名字。

  • 这个命令的意思是告诉 maven 执行 archetype 插件的 generate 目标。

  • 插件目标通常会写成 pluginId:goalId

一个目标是一个工作单元,而插件则是一个或者多个目标的集合。比如说 Jar 插件,Compiler 插件,Surefire 插件等。从看名字就能知道,Jar 插件包含建立 Jar 文件的目标, Compiler 插件包含编译源代码和单元测试代码的目标。Surefire 插件的话,则是运行单元测试。

看到这里,估计你能明白了,mvn 本身不会做太多的事情,它不知道怎么样编译或者怎么样打包。它把构建的任务交给插件去做。插件定义了常用的构建逻辑,能够被重复利用。这样做的好处是,一旦插件有了更新,那么所有的 maven 用户都能得到更新。

我们以 compile 这个 phase 为例,如果执行:

$ mvn compile

Maven 将执行 compile 这个 phase,这个 phase 会调用 compiler 插件执行关联的 compiler:compile 这个 goal。

由此可见,执行每个 phase,都是通过某个插件(plugin)来执行的,Maven 本身其实并不知道如何执行 compile,它只是负责找到对应的 compiler 插件,然后执行默认的 compiler:compile 这个 goal 来完成编译。

所以,使用 Maven,实际上就是配置好需要使用的插件,然后通过 phase 调用它们。

Maven 已经内置了一些常用的标准插件:

插件名称 对应执行的 phase
clean clean
compiler compile
surefire test
jar package

如果标准插件无法满足需求,我们还可以使用自定义插件。使用自定义插件的时候,需要声明。例如,使用 maven-shade-plugin 可以创建一个可执行的 jar,要使用这个插件,需要在 pom.xml 中声明它:

<project>
    ...
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.2.1</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            ...
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

自定义插件往往需要一些配置,例如,maven-shade-plugin 需要指定 Java 程序的入口,它的配置是:

<configuration>
    <transformers>
        <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
            <mainClass>com.itranswarp.learnjava.Main</mainClass>
        </transformer>
    </transformers>
</configuration>

注意,Maven 自带的标准插件例如 compiler 是无需声明的,只有引入其它的插件才需要声明。

下面列举了一些常用的插件:

  • maven-shade-plugin:打包所有依赖包并生成可执行 jar;
  • cobertura-maven-plugin:生成单元测试覆盖率报告;
  • findbugs-maven-plugin:对 Java 源码进行静态分析以找出潜在问题。

2.4.3、Maven 构建流程

Maven 不但有标准化的项目结构,而且还有一套标准化的构建流程,可以自动化实现编译,打包,发布,等等。

2.4.3.1、Lifecycle 和 Phase

使用 Maven 时,我们首先要了解什么是 Maven 的生命周期(lifecycle)。

Maven 的生命周期由一系列阶段(phase)构成,以内置的生命周期 default 为例,它包含以下 phase:

validate
initialize
generate-sources
process-sources
generate-resources
process-resources
compile
process-classes
generate-test-sources
process-test-sources
generate-test-resources
process-test-resources
test-compile
process-test-classes
test
prepare-package
package
pre-integration-test
integration-test
post-integration-test
verify
install
deploy

如果我们运行 mvn package,Maven 就会执行 default 生命周期,它会从开始一直运行到 package 这个 phase 为止:

validate
...
package

如果我们运行 mvn compile,Maven 也会执行 default 生命周期,但这次它只会运行到 compile,即以下几个 phase:

validate
...
compile

Maven 另一个常用的生命周期是 clean,它会执行 3 个 phase:

pre-clean
clean (注意这个clean不是lifecycle而是phase)
post-clean

所以,我们使用 mvn 这个命令时,后面的参数是 phase,Maven 自动根据生命周期运行到指定的 phase。

更复杂的例子是指定多个 phase,例如,运行 mvn clean package,Maven 先执行 clean 生命周期并运行到 clean 这个 phase,然后执行 default 生命周期并运行到 package 这个 phase,实际执行的 phase 如下:

pre-clean
clean (注意这个clean是phase)
validate
...
package

在实际开发过程中,经常使用的命令有:

mvn clean:清理所有生成的 class 和 jar;

mvn clean compile:先清理,再执行到 compile

mvn clean test:先清理,再执行到 test,因为执行 test 前必须执行 compile,所以这里不必指定 compile

mvn clean package:先清理,再执行到 package

大多数 phase 在执行过程中,因为我们通常没有在 pom.xml 中配置相关的设置,所以这些 phase 什么事情都不做。

经常用到的 phase 其实只有几个:

  • clean:清理
  • compile:编译
  • test:运行测试
  • package:打包

2.4.3.2、Goal

执行一个 phase 又会触发一个或多个 goal:

执行的 Phase 对应执行的 Goal
compile compiler:compile
test compiler:testCompile
surefire:test

goal 的命名总是 abc:xyz 这种形式。

我们类比一下就明白了:

  • lifecycle 相当于 Java 的 package,它包含一个或多个 phase;
  • phase 相当于 Java 的 class,它包含一个或多个 goal;
  • goal 相当于 class 的 method,它其实才是真正干活的。

大多数情况,我们只要指定 phase,就默认执行这些 phase 默认绑定的 goal,只有少数情况,我们可以直接指定运行一个 goal,例如,启动 Tomcat 服务器:

$ mvn tomcat:run

2.4.4、Maven 依赖管理

如果我们的项目依赖第三方的 jar 包,例如 commons logging,那么问题来了:commons logging 发布的 jar 包在哪下载?如果我们还希望依赖 log4j,那么使用 log4j 需要哪些 jar 包?

类似的依赖还包括:JUnit,JavaMail,MySQL 驱动等等,一个可行的方法是通过搜索引擎搜索到项目的官网,然后手动下载 zip 包,解压,放入 classpath。但是,这个过程非常繁琐。

Maven 解决了依赖管理问题。之前我们说过,maven 坐标能够确定一个项目。换句话说,我们可以用它来解决依赖关系。在 POM 中,依赖关系是在 dependencies 部分中定义的。在上面的 POM 例子中,我们用 dependencies 定义了对于 junit 的依赖:

<dependencies> 
    <dependency> 
      <groupid>junit</groupid> 
      <artifactid>junit</artifactid> 
      <version>3.8.1</version> 
      <scope>test</scope> 
    </dependency> 
  </dependencies>

那这个例子很简单,但是实际开发中我们会有复杂得多的依赖关系,因为被依赖的 jar 文件会有自己的依赖关系。那么我们是不是需要把那些间接依赖的 jar 文件也都定义在 POM 中呢?答案是不需要,因为 maven 提供了传递依赖的特性。

所谓传递依赖是指 maven 会检查被依赖的 jar 文件,把它的依赖关系纳入最终解决的依赖关系链中。针对上面的 junit 依赖关系,如果你看一下 maven 的本地库 ~/.m2/repository/junit/junit/3.8.1/

[root@armcli ~]# cd ~/.m2/repository/junit/junit/3.8.1/
[root@armcli 3.8.1]# 
[root@armcli 3.8.1]# ls
junit-3.8.1.jar  junit-3.8.1.jar.sha1  junit-3.8.1.pom  junit-3.8.1.pom.sha1  _remote.repositories
[root@armcli 3.8.1]#

你会发现 maven 不但下载了 junit-3.8.1.jar,还下载了它的 POM 文件。这样 maven 就能检查 junit 的依赖关系,把它所需要的依赖也包括进来。

因此,Maven 的第一个作用就是解决依赖管理。我们声明了自己的项目需要 abc,Maven 会自动导入 abc 的 jar 包,再判断出 abc 需要 xyz,又会自动导入 xyz 的 jar 包,这样,最终我们的项目会依赖 abcxyz 两个 jar 包。

我们再来看一个复杂依赖示例:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>1.4.2.RELEASE</version>
</dependency>

当我们声明一个 spring-boot-starter-web 依赖时,Maven 会自动解析并判断最终需要大概二三十个其他依赖:

spring-boot-starter-web
  spring-boot-starter
    spring-boot
    sprint-boot-autoconfigure
    spring-boot-starter-logging
      logback-classic
        logback-core
        slf4j-api
      jcl-over-slf4j
        slf4j-api
      jul-to-slf4j
        slf4j-api
      log4j-over-slf4j
        slf4j-api
    spring-core
    snakeyaml
  spring-boot-starter-tomcat
    tomcat-embed-core
    tomcat-embed-el
    tomcat-embed-websocket
      tomcat-embed-core
  jackson-databind
  ...

如果我们自己去手动管理这些依赖是非常费时费力的,而且出错的概率很大。

2.4.4.1、依赖关系

Maven 定义了几种依赖关系,分别是 compiletestruntimeprovided

scope 说明 示例
compile 编译时需要用到该 jar 包(默认) commons-logging
test 编译 Test 时需要用到该 jar 包 junit
runtime 编译时不需要,但运行时需要用到 mysql
provided 编译时需要用到,但运行时由 JDK 或某个服务器提供 servlet-api

其中,默认的 compile 是最常用的,Maven 会把这种类型的依赖直接放入 classpath。

test 依赖表示仅在测试时使用,正常运行时并不需要。最常用的 test 依赖就是 JUnit:

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.3.2</version>
    <scope>test</scope>
</dependency>

runtime 依赖表示编译时不需要,但运行时需要。最典型的 runtime 依赖是 JDBC 驱动,例如 MySQL 驱动:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.48</version>
    <scope>runtime</scope>
</dependency>

provided 依赖表示编译时需要,但运行时不需要。最典型的 provided 依赖是 Servlet API,编译的时候需要,但是运行时,Servlet 服务器内置了相关的 jar,所以运行期不需要:

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>4.0.0</version>
    <scope>provided</scope>
</dependency>

最后一个问题是,Maven 如何知道从何处下载所需的依赖?也就是相关的 jar 包?答案是 Maven 维护了一个中央仓库(repo1.maven.org),所有第三方库将自身的 jar 以及相关信息上传至中央仓库,Maven 就可以从中央仓库把所需依赖下载到本地。

Maven 并不会每次都从中央仓库下载 jar 包。一个 jar 包一旦被下载过,就会被 Maven 自动缓存在本地目录(用户主目录的 .m2 目录),所以,除了第一次编译时因为下载需要时间会比较慢,后续过程因为有本地缓存,并不会重复下载相同的 jar 包。

2.4.4.2、唯一 ID

对于某个依赖,Maven 只需要 3 个关键变量即可唯一确定某个 jar 包:

  • groupId:属于组织的名称,类似 Java 的包名;
  • artifactId:该 jar 包自身的名称,类似 Java 的类名;
  • version:该 jar 包的版本。

通过上述 3 个变量,即可唯一确定某个 jar 包。Maven 通过对 jar 包进行 PGP 签名确保任何一个 jar 包一经发布就无法修改。修改已发布 jar 包的唯一方法是发布一个新版本。

因此,某个 jar 包一旦被 Maven 下载过,即可永久地安全缓存在本地。

注:只有以 -SNAPSHOT 结尾的版本号会被 Maven 视为开发版本,开发版本每次都会重复下载,这种 SNAPSHOT 版本只能用于内部私有的 Maven repo,公开发布的版本不允许出现 SNAPSHOT。

2.4.4.3、Maven 镜像

除了可以从 Maven 的中央仓库下载外,还可以从 Maven 的镜像仓库下载。如果访问 Maven 的中央仓库非常慢,我们可以选择一个速度较快的 Maven 的镜像仓库。Maven 镜像仓库定期从中央仓库同步:

中国区用户可以使用阿里云提供的 Maven 镜像仓库。使用 Maven 镜像仓库需要一个配置,在用户主目录下进入 .m2 目录,创建一个 settings.xml 配置文件,内容如下:

<settings>
    <mirrors>
        <mirror>
            <id>aliyun</id>
            <name>aliyun</name>
            <mirrorOf>central</mirrorOf>
            <!-- 国内推荐阿里云的Maven镜像 -->
            <url>https://maven.aliyun.com/repository/central</url>
        </mirror>
    </mirrors>
</settings>

2.4.4.4、搜索第三方组件

最后一个问题:如果我们要引用一个第三方组件,比如 okhttp,如何确切地获得它的 groupIdartifactIdversion?方法是通过 search.maven.org 搜索关键字,找到对应的组件后,直接复制:

maven search plugin

2.4.4.5、命令行编译

在命令中,进入到 pom.xml 所在目录,输入以下命令:

$ mvn clean package

如果一切顺利,即可在 target 目录下获得编译后自动打包的 jar。

2.4.5、Maven 库

当第一次运行 maven 命令的时候,你需要 Internet 连接,因为它要从网上下载一些文件。那么它从哪里下载呢?它是从 maven 默认的远程库(http://repo1.maven.org/maven2) 下载的。这个远程库有 maven 的核心插件和可供下载的 jar 文件。

但是不是所有的 jar 文件都是可以从默认的远程库下载的,比如说我们自己开发的项目。这个时候,有两个选择:要么在公司内部设置定制库,要么手动下载和安装所需的 jar 文件到本地库。

本地库是指 maven 下载了插件或者 jar 文件后存放在本地机器上的拷贝。在 Linux 上,它的位置在 ~/.m2/repository,在 Windows XP 上,在 C:\Documents and Settings\username\.m2\repository,在 Windows 7 上,在 C:\Users\username\.m2\repository。当 maven 查找需要的 jar 文件时,它会先在本地库中寻找,只有在找不到的情况下,才会去远程库中找。

运行下面的命令能把我们的 helloworld 项目安装到本地库:

$ mvn install

一旦一个项目被安装到了本地库后,你别的项目就可以通过 maven 坐标和这个项目建立依赖关系。比如如果我现在有一个新项目需要用到 helloworld,那么在运行了上面的 mvn install 命令后,我就可以如下所示来建立依赖关系:

<dependency>
      <groupid>com.greatwall.helloworld</groupid>
      <artifactid>helloworld</artifactid>
      <version>1.0-SNAPSHOT</version>
</dependency>

2.4.6、Maven 模块管理

在软件开发中,把一个大项目分拆为多个模块是降低软件复杂度的有效方法:

                        ┌ ─ ─ ─ ─ ─ ─ ┐
                          ┌─────────┐
                        │ │Module A │ │
                          └─────────┘
┌──────────────┐ split  │ ┌─────────┐ │
│Single Project│───────>  │Module B │
└──────────────┘        │ └─────────┘ │
                          ┌─────────┐
                        │ │Module C │ │
                          └─────────┘
                        └ ─ ─ ─ ─ ─ ─ ┘

对于 Maven 工程来说,原来是一个大项目:

single-project
├── pom.xml
└── src

现在可以分拆成 3 个模块:

mutiple-project
├── module-a
│   ├── pom.xml
│   └── src
├── module-b
│   ├── pom.xml
│   └── src
└── module-c
    ├── pom.xml
    └── src

Maven 可以有效地管理多个模块,我们只需要把每个模块当作一个独立的 Maven 项目,它们有各自独立的 pom.xml。例如,模块 A 的 pom.xml

<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>com.itranswarp.learnjava</groupId>
    <artifactId>module-a</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <name>module-a</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

模块B的 pom.xml

<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>com.itranswarp.learnjava</groupId>
    <artifactId>module-b</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <name>module-b</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

可以看出来,模块 A 和模块 B 的 pom.xml 高度相似,因此,我们可以提取出共同部分作为 parent

<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>com.itranswarp.learnjava</groupId>
    <artifactId>parent</artifactId>
    <version>1.0</version>
    <packaging>pom</packaging>

    <name>parent</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.28</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

注意到 parent 的 <packaging>pom 而不是 jar,因为 parent 本身不含任何 Java 代码。编写 parentpom.xml 只是为了在各个模块中减少重复的配置。现在我们的整个工程结构如下:

multiple-project
├── pom.xml
├── parent
│   └── pom.xml
├── module-a
│   ├── pom.xml
│   └── src
├── module-b
│   ├── pom.xml
│   └── src
└── module-c
    ├── pom.xml
    └── src

这样模块 A 就可以简化为:

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

    <parent>
        <groupId>com.itranswarp.learnjava</groupId>
        <artifactId>parent</artifactId>
        <version>1.0</version>
        <relativePath>../parent/pom.xml</relativePath>
    </parent>

    <artifactId>module-a</artifactId>
    <packaging>jar</packaging>
    <name>module-a</name>
</project>

模块 B、模块 C 都可以直接从 parent 继承,大幅简化了 pom.xml 的编写。

如果模块 A 依赖模块 B,则模块 A 需要模块 B 的 jar 包才能正常编译,我们需要在模块 A 中引入模块 B:

    ...
    <dependencies>
        <dependency>
            <groupId>com.itranswarp.learnjava</groupId>
            <artifactId>module-b</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

最后,在编译的时候,需要在根目录创建一个 pom.xml 统一编译:

<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/maven-v4_0_0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itranswarp.learnjava</groupId>
    <artifactId>build</artifactId>
    <version>1.0</version>
    <packaging>pom</packaging>
    <name>build</name>

    <modules>
        <module>parent</module>
        <module>module-a</module>
        <module>module-b</module>
        <module>module-c</module>
    </modules>
</project>

这样,在根目录执行 mvn clean package 时,Maven 根据根目录的 pom.xml 找到包括 parent 在内的共 4 个 <module>,一次性全部编译。

中央仓库

其实我们使用的大多数第三方模块都是这个用法,例如,我们使用 commons logging、log4j 这些第三方模块,就是第三方模块的开发者自己把编译好的 jar 包发布到 Maven 的中央仓库中。

私有仓库

私有仓库是指公司内部如果不希望把源码和 jar 包放到公网上,那么可以搭建私有仓库。私有仓库总是在公司内部使用,它只需要在本地的 ~/.m2/settings.xml 中配置好,使用方式和中央仓位没有任何区别。

本地仓库

本地仓库是指把本地开发的项目“发布”在本地,这样其他项目可以通过本地仓库引用它。

但是我们不推荐把自己的模块安装到 Maven 的本地仓库,因为每次修改某个模块的源码,都需要重新安装,非常容易出现版本不一致的情况。

更好的方法是使用模块化编译,在编译的时候,告诉 Maven 几个模块之间存在依赖关系,需要一块编译,Maven 就会自动按依赖顺序编译这些模块。

2.4.7、mvnw 使用

mvnw 是 Maven Wrapper 的缩写。因为我们安装 Maven 时,默认情况下,系统所有项目都会使用全局安装的这个 Maven 版本。

但是,对于某些项目来说,它可能必须使用某个特定的 Maven 版本,这个时候,就可以使用 Maven Wrapper,它可以负责给这个特定的项目安装指定版本的 Maven,而其他项目不受影响。

简单地说,Maven Wrapper 就是给一个项目提供一个独立的,指定版本的 Maven。

2.4.7.1、安装 Maven Wrapper

安装 Maven Wrapper 最简单的方式是在项目的根目录(即 pom.xml 所在的目录)下运行安装命令:

$ mvn -N io.takari:maven:0.7.6:wrapper

它会自动使用最新版本的 Maven。注意 0.7.6 是 Maven Wrapper的版本。最新的 Maven Wrapper 版本可以去官方网站查看。

如果要指定使用的 Maven 版本,使用下面的安装命令指定版本,例如 3.3.3

$ mvn -N io.takari:maven:0.7.6:wrapper -Dmaven=3.3.3

安装后,查看项目结构:

my-project
├── .mvn
│   └── wrapper
│       ├── MavenWrapperDownloader.java
│       ├── maven-wrapper.jar
│       └── maven-wrapper.properties
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   └── resources
    └── test
        ├── java
        └── resources

发现多了 mvnwmvnw.cmd.mvn 目录,我们只需要把 mvn 命令改成 mvnw 就可以使用跟项目关联的 Maven。例如:

$ mvnw clean package

在 Linux 或 macOS 下运行时需要加上 ./

./mvnw clean package

Maven Wrapper 的另一个作用是把项目的 mvnwmvnw.cmd.mvn 提交到版本库中,可以使所有开发人员使用统一的 Maven 版本。

2.4.8、发布 Artifact

当我们使用 commons-logging 这些第三方开源库的时候,我们实际上是通过 Maven 自动下载它的 jar 包,并根据其 pom.xml 解析依赖,自动把相关依赖包都下载后加入到 classpath。

如果我们把自己的开源库放到 Maven 的 repo 中,那么,别人只需按标准引用 groupId:artifactId:version,即可自动下载 jar 包以及相关依赖。因此,本节我们介绍如何发布一个库到 Maven 的 repo 中。

2.4.8.1、以静态文件发布

如果我们观察一个中央仓库的 Artifact 结构,例如 Commons Math,它的 groupId 是 org.apache.commons,artifactId 是 commons-math3,以版本 3.6.1 为例,发布在中央仓库的文件夹路径就是 https://repo1.maven.org/maven2/org/apache/commons/commons-math3/3.6.1/

在此文件夹下,commons-math3-3.6.1.jar 就是发布的 jar 包,commons-math3-3.6.1.pom 就是它的 pom.xml 描述文件,commons-math3-3.6.1-sources.jar 是源代码,commons-math3-3.6.1-javadoc.jar 是文档。其它以 .asc.md5.sha1 结尾的文件分别是 GPG 签名、MD5 摘要和 SHA-1 摘要。

我们只要按照这种目录结构组织文件,它就是一个有效的 Maven 仓库。

我们以广受好评的开源项目 how-to-become-rich 为例,先创建 Maven 工程目录结构如下:

how-to-become-rich
├── maven-repo        <-- Maven本地文件仓库
├── pom.xml           <-- 项目文件
├── src
│   ├── main
│   │   ├── java      <-- 源码目录
│   │   └── resources <-- 资源目录
│   └── test
│       ├── java      <-- 测试源码目录
│       └── resources <-- 测试资源目录
└── target            <-- 编译输出目录

pom.xml 中添加如下内容:

<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.itranswarp.rich</groupId>
    <artifactId>how-to-become-rich</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <name>how-to-become-rich</name>

    <developers>
        <developer>
            <name>Brinnatt</name>
            <email>brinnatt@gmail.com</email>
            <organization>Greatwall</organization>
            <organizationUrl>https://brinnatt.com</organizationUrl>
        </developer>
    </developers>

    <properties>
        <!-- source encoding -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <maven.javadoc.failOnError>false</maven.javadoc.failOnError>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.2</version>
        </dependency>
    </dependencies>

    <distributionManagement>
        <repository>
            <id>local-repo-release</id>
            <name>GitHub Release</name>
            <url>file://${project.basedir}/maven-repo</url>
        </repository>
    </distributionManagement>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-source-plugin</artifactId>
                <executions>
                    <execution>
                        <id>attach-sources</id>
                        <phase>package</phase>
                        <goals>
                            <goal>jar-no-fork</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-javadoc-plugin</artifactId>
                <executions>
                    <execution>
                        <id>attach-javadocs</id>
                        <phase>package</phase>
                        <goals>
                            <goal>jar</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

注意到 <distributionManagement>,它指示了发布的软件包的位置,这里的 <url> 是项目根目录下的 maven-repo 目录,在 <build> 中定义的两个插件 maven-source-pluginmaven-javadoc-plugin 分别用来创建源码和 javadoc,如果不想发布源码,可以把对应的插件去掉。

我们直接在项目根目录下运行 Maven 命令 mvn clean package deploy,如果一切顺利,我们就可以在 maven-repo 目录下找到部署后的所有文件如下:

[root@armcli how-to-become-rich]# tree maven-repo/
maven-repo/
└── com
    └── itranswarp
        └── rich
            └── how-to-become-rich
                ├── 1.0.0
                │   ├── how-to-become-rich-1.0.0.jar
                │   ├── how-to-become-rich-1.0.0.jar.md5
                │   ├── how-to-become-rich-1.0.0.jar.sha1
                │   ├── how-to-become-rich-1.0.0.pom
                │   ├── how-to-become-rich-1.0.0.pom.md5
                │   ├── how-to-become-rich-1.0.0.pom.sha1
                │   ├── how-to-become-rich-1.0.0-sources.jar
                │   ├── how-to-become-rich-1.0.0-sources.jar.md5
                │   └── how-to-become-rich-1.0.0-sources.jar.sha1
                ├── maven-metadata.xml
                ├── maven-metadata.xml.md5
                └── maven-metadata.xml.sha1

5 directories, 12 files

最后一步,是把这个工程推到 GitHub 上,并选择 Settings-GitHub Pages,选择 master branch 启用 Pages 服务:

[root@armcli how-to-become-rich]# git init
[root@armcli how-to-become-rich]# git remote add origin git@github.com:liangtiansheng/how-to-become-rich.git
[root@armcli how-to-become-rich]# git config user.name 'Brinnatt'
[root@armcli how-to-become-rich]# git config user.email "brinnatt@gmail.com"
[root@armcli how-to-become-rich]# git add .
[root@armcli how-to-become-rich]# git commit -m "This is a maven project for testing publishing"
[master (root-commit) 8be3b1a] This is a maven project for testing publishing
 14 files changed, 447 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0-sources.jar
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0-sources.jar.md5
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0-sources.jar.sha1
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.jar
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.jar.md5
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.jar.sha1
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.pom
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.pom.md5
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.pom.sha1
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/maven-metadata.xml
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/maven-metadata.xml.md5
 create mode 100644 maven-repo/com/itranswarp/rich/how-to-become-rich/maven-metadata.xml.sha1
 create mode 100644 pom.xml
[root@armcli how-to-become-rich]# git pull origin master    # 非clone项目,要先把原仓库代码拉下来合并
warning: no common commits
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
From github.com:liangtiansheng/how-to-become-rich
 * branch            master     -> FETCH_HEAD
Merge made by the 'recursive' strategy.
 README.md | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 README.md
[root@armcli how-to-become-rich]#
[root@armcli how-to-become-rich]# git push -u origin master # 不合并直接上传会失败
Counting objects: 24, done.
Delta compression using up to 8 threads.
Compressing objects: 100% (11/11), done.
Writing objects: 100% (23/23), 5.64 KiB | 0 bytes/s, done.
Total 23 (delta 2), reused 0 (delta 0)
remote: Resolving deltas: 100% (2/2), done.
To git@github.com:liangtiansheng/how-to-become-rich.git
   e0b9371..7978741  master -> master
Branch master set up to track remote branch master from origin.

maven publish to github

这样,把全部内容推送至 GitHub 后,即可作为静态网站访问 Maven 的 repo,它的地址是 https://liangtiansheng.github.io/how-to-become-rich/。版本 1.0.0 对应的 jar 包地址是:

https://liangtiansheng.github.io/how-to-become-rich/maven-repo/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.jar

现在,如果其他人希望引用这个 Maven 包,我们可以告知如下依赖即可:

<dependency>
    <groupId>com.itranswarp.rich</groupId>
    <artifactId>how-to-become-rich</artifactId>
    <version>1.0.0</version>
</dependency>

但是,除了正常导入依赖外,对方还需要再添加一个 <repository> 的声明,即使用方完整的 pom.xml 如下:

<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>example</groupId>
    <artifactId>how-to-become-rich-usage</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
        <java.version>11</java.version>
    </properties>

    <repositories>
        <repository>
            <id>github-rich-repo</id>
            <name>The Maven Repository on Github</name>
            <url>https://liangtiansheng.github.io/how-to-become-rich/maven-repo/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>com.itranswarp.rich</groupId>
            <artifactId>how-to-become-rich</artifactId>
            <version>1.0.0</version>
        </dependency>
    </dependencies>
</project>

<repository> 中,我们必须声明发布的 Maven 的 repo 地址,其中 <id><name> 可以任意填写,<url> 填入 GitHub Pages 提供的地址 + /maven-repo/ 后缀。现在,即可正常引用这个库并编写代码如下:

Millionaire millionaire = new Millionaire();
System.out.println(millionaire.howToBecomeRich());

此外,通过 GitHub Pages 发布 Maven repo 时需要注意一点,即不要改动已发布的版本。因为 Maven 的仓库是不允许修改任何版本的,对一个库进行修改的唯一方法是发布一个新版本。但是通过静态文件的方式发布 repo,实际上我们是可以修改 jar 文件的,但最好遵守规范,不要修改已发布版本。

2.4.8.2、发布到 Nexus3 私服

Nexus3 私服是什么以及如何搭建,请参考下面的 2.5 章节,这里也是先搭建好了,直接用来 Maven 构建发布。

  1. 将 2.4.8.1 小节示例中的 pom.xml 作如下修改。
<distributionManagement>
    <repository>
        <id>nexus3</id>
        <name>private nexus3</name>
        <url>http://172.16.4.13:8081/repository/maven-releases/</url>
    </repository>
    <!-- 如果项目版本号中有SNAPSHOT标识,会发布到下面这个Nexus Snapshots Repository
    <snapshotRepository>    
        <id>nexus-snapshots</id>    
        <name>Nexus Snapshot Repository</name>    
        <url>http://<ip>:8081/repository/maven-snapshots/</url>    
    </snapshotRepository>
    -->
</distributionManagement>
  1. 用户家目录下创建 maven 配置文件 ~/.m2/settings.xml,添加如下内容。
<settings>
    <servers>
        <server>
            <id>nexus3</id>
            <username>admin</username>
            <password>${MYPASSWORD}</password>
        </server>

        <!-- 对照pom.xml文件,通过ID匹配用户名和密码
        <server>  
            <id>nexus-snapshots</id>  
            <username>admin</username>  
            <password>admin123</password>  
        </server>
        -->
    </servers>
</settings>
  • 用户和密码就是 nexus3 服务器上定义的,关于如何给用户授权请参考 2.5 章节。
  • 注意:server 的 id 名称一定要和 pom.xml 配置文件中的 repository id 名称保持一致,用来匹配用户信息。
  1. 执行 mvn clean package deploy 即可完成清除、构建、打包以及发布到我们的 nexus3 服务器上去,注意目录结构还是 2.4.8.1 小节中的目录结构。
[root@armcli how-to-become-rich]# mvn clean package deploy
......
[INFO] --- maven-deploy-plugin:2.7:deploy (default-deploy) @ how-to-become-rich ---
Uploading to nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.jar
Uploaded to nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.jar (2.0 kB at 9.3 kB/s)
Uploading to nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.pom
Uploaded to nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0.pom (1.9 kB at 27 kB/s)
Downloading from nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/maven-metadata.xml
Uploading to nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/maven-metadata.xml
Uploaded to nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/maven-metadata.xml (317 B at 4.3 kB/s)
Uploading to nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0-sources.jar
Uploaded to nexus3: http://172.16.4.13:8081/repository/maven-releases/com/itranswarp/rich/how-to-become-rich/1.0.0/how-to-become-rich-1.0.0-sources.jar (1.9 kB at 31 kB/s)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  4.597 s
[INFO] Finished at: 2022-03-27T16:14:11+08:00
[INFO] ------------------------------------------------------------------------

2.5、Maven 私服 Nexus3

官方文档:https://help.sonatype.com/repomanager3

下载地址:https://help.sonatype.com/repomanager3/product-information/download

Nexus3 是一个强大的 Maven、Nuget 仓库管理器,它极大地简化了本地内部仓库的维护和外部仓库的访问。

Nexus3 在代理远程仓库的同时维护本地仓库,以降低中央仓库的负荷,节省外网带宽和时间。

Nexus3 是一套"开箱即用"的系统,不需要数据库,它使用文件系统加 Lucene 来组织数据。

Nexus3 使用 ExtJS 来开发界面,利用 Restlet 来提供完整的 REST APIs,通过 m2eclipse 与 Eclipse 集成使用。

Nexus3 支持 WebDAV 与 LDAP 安全身份认证。

Nexus3 还提供了强大的仓库管理功能,构件搜索功能,它基于 REST,友好的 UI 是一个 extjs 的 REST 客户端,它占用较少的内存,基于简单文件系统而非数据库。

在项目开发过程中,研发团队所需的构件都需要通过 Maven 的中央仓库和第三方的 Maven 仓库下载到本地,而一个团队中的所有人都重复的从 Maven 仓库下载构件无疑加大了仓库的负载和浪费了外网带宽,如果网速慢的话,还会影响项目的进程。

这时我们会考虑在内网搭建属于自己的 Maven 私服,这样既节省了网络带宽也会加速项目搭建的进程,当然前提条件就是你的私服中拥有项目所需的所有构件。

2.5.1、Nexus3 安装

运行 nexus3 需要 java jdk 环境,刚才 Maven 安装使用的是 java17(jdk17) 版本,这个版本太高,换成 java8(jdk1.8):

[root@armcli ~]# yum remove jdk-17.aarch64
[root@armcli ~]# yum install java -y
[root@armcli ~]# java -version
openjdk version "1.8.0_322"
OpenJDK Runtime Environment (build 1.8.0_322-b06)
OpenJDK 64-Bit Server VM (build 25.322-b06, mixed mode)

下载 nexus3 安装:

[root@armcli ~]# wget https://sonatype-download.global.ssl.fastly.net/repository/downloads-prod-group/3/nexus-3.38.0-01-unix.tar.gz
[root@armcli ~]# tar xf nexus-3.38.0-01-unix.tar.gz -C /usr/local/
[root@armcli ~]# ln -sv /usr/local/nexus-3.38.0-01/ /usr/local/nexus3
‘/usr/local/nexus3’ -> ‘/usr/local/nexus-3.38.0-01/’
[root@armcli ~]# vim /etc/profile.d/nexus3.sh
export PATH=$PATH:/usr/local/nexus3/bin
[root@armcli ~]# source /etc/profile.d/nexus3.sh
[root@armcli ~]# nexus --help
WARNING: ************************************************************
WARNING: Detected execution as "root" user.  This is NOT recommended!
WARNING: ************************************************************
Usage: /usr/local/nexus3/bin/nexus {start|stop|run|run-redirect|status|restart|force-reload}
[root@armcli ~]#

启动 nexus 服务:

# 启动需要点时间
[root@armcli ~]# nexus start
WARNING: ************************************************************
WARNING: Detected execution as "root" user.  This is NOT recommended!
WARNING: ************************************************************
Starting nexus
[root@armcli ~]#
[root@armcli ~]# nexus status
WARNING: ************************************************************
WARNING: Detected execution as "root" user.  This is NOT recommended!
WARNING: ************************************************************
nexus is running.

访问 nexus 网页:服务的默认端口是 8081,地址:http://172.16.4.13:8081/

nexus3

点击右上角 Sign in 按钮,按提示输入用户名和密码,点击 Sign in 登陆。然后出现导航设置,重新设置密码和是否授权匿名用户访问,按照要求设置即可。

2.5.2、Nexus3 功能介绍

Nexus3 的核心功能就是项目构件仓库(Repository),其他的很多功能都是围绕着仓库功能进行配套使用。在 UI 界面直接操作理解更简单,有疑问再查官方文档,可以事半功倍。

2.5.2.1、仓库类型

  1. 代理仓库(Proxy Repository)

    带有 proxy 属性的仓库就是代理仓库,是用来连接远程仓库的。向 Nexus3 发起的项目构件请求都会先进行本地构件校验,如果存在则直接返回;如果没有则向远程代理仓库请求,请求返回的组件会存储在本地,相当于缓存一样。因此,消除了再次从远程仓库检索组件的网络带宽和时间开销。

    Nexus3 当前默认有下面两个远程代理仓库:

    maven-central:Apache Maven 内置的默认组件库,并得到了其他构建工具(如 Gradle、SBT 或 Ant/Ivy)的良好支持。访问地址:https://repo1.maven.org/maven2

    nuget.org-proxy:它是 .net 开发中使用的 nuget 包管理工具所使用的默认组件库。访问地址:https://www.nuget.org/

  2. 托管仓库

    带有 hosted 属性的仓库就是托管仓库,作为内部组件的权威仓库。

    Nexus3 当前默认有下面几个权威内部组件仓库:

    maven-releases:用来发布 maven2 格式的正式版 jar 包,可以用作内部正式版本,也可以用作外部仓库没有的第三方组件,所以通过代理仓库不可获取,但是可以用作专利库等商业用途。

    maven-snapshots:用来发布 maven2 格式的开发版 jar 包,相当于快照。

    nuget-hosted:用来发布 nuget 格式的正式版 jar 包。

  3. 仓库组

    带有 group 属性的仓库就是仓库组,代表 Nexus3 的强大特性,它可以让你将其他的多个仓库或者多个仓库组集成在一个仓库当中。这意味着你的用户可以依赖一个 URL 来满足他们的配置需求,而管理员可以向仓库组添加更多的仓库和组件。

    Nexus3 当前默认有下面几个仓库组:

    maven-public:Maven-public 组是一个 maven2 格式的仓库组,集成了远程的 Maven 官方中央仓库、maven-releases 内部权威仓库以及 maven-snapshots 仓库。可以通过一个简单仓库组的 url 来发布集成的多样式仓库中的组件。

    nuget-group:nuget-group 仓库组集成了 nuget 格式的 nuget-hosted 仓库和 nuget.org-proxy 代理仓库,用作 .net 开发。

2.5.2.2、Blob 存储

blob 存储是一个内部存储机制,用来存储二进制组件及其附件,可以基于本地文件系统或者 Amazon S3 云存储等,一个 blob 存储可以被多个仓库或者仓库组使用。

2.5.3、Nexus3 使用

2.5.3.1、创建 role 角色

在项目开发中,特别是多个项目交叉开发时,代码有多个分支,人员也会交替开发。这就会带来权限干扰的问题,所以通过 role 角色控制项目人员的权限是很有必要的。

菜单栏 Server administration and configuration --> Security --> Roles --> Create role --> Nexus role,根据要求创建 role。

nexus3 security role

2.5.3.2、创建用户

创建好 role 角色后可以创建用户了,将用户与具有权限集的角色绑定后,用户就拥有该权限集。下面创建一个 manager 项目经理用户,将 nx-manager 角色与 manager 用户进行绑定。

菜单栏 Server administration and configuration --> Security --> Users --> Create local user,根据要求填写用户信息,并根据预先设计思路绑定用户角色。

nexus3 security user

2.5.3.3、新用户创建仓库

  1. 刚才创建角色的时候有一个权限没有加上去,把 nx-repository-admin-*-*-add 加到 nx-manager 角色上去,不然绑定该角色的用户不能创建仓库。

  2. 使用 manager 用户登陆 UI --> 菜单栏 Server administration and configuration --> Repositories --> Create repository --> maven2(hosted),根据要求填写必要信息。

nexus3 newuser Crepo

  1. 切换到 admin 用户,将 nx-repository-admin-maven2-grepo-* nx-repository-view-maven2-*-* nx-component-upload 权限赋给 nx-manager 角色,然后切回 manager 用户。

    • 第一个权限是将新仓库 grepo 的自身配置权交给 nx-manager 角色。
    • 第二个权限是将 maven2 格式的所有仓库中的资产控制权交给 nx-manager 角色,目的是可以把 grepo 仓库加入到 maven-public 组当中。
    • 第三个权限是允许上传文件到 grepo 仓库中。
  2. 上传一个测试文件到 grepo 仓库。

    菜单栏 Browse server contents --> Upload --> grepo,填写必要信息,点击 Upload 即可上传。

    nexus3 upload

  3. 查看上传的文件。

    nexus3 upload content

关于 maven 和 nexus3 就介绍到这里,上项目还会有具体需求,不过问题不大。需要说明的是,nexus3 用户权限管理我这里梳理的一个思路,实际上是我一个权限一个权限试出来的,即便官方文档也没有具体说明,思路很重要,至于精细化管理,多试几次就行了。

标签云