《Maven 实战》笔记

当项目需要被其他项目引用时,使用 mvn clean install 安装到本地仓库,其他项目就可以引用该项目。

我们打包的默认 jar 包不能直接运行的,因为带有 main 方法的信息类不会添加到 mainfest 中( jar 文件中 META-INF/MANIFEST.MF 文件,没有 Main-Class一行)。为了生成可执行的 jar 文件,需要借助 maven-shade-plugin,配置该插件。

使用 Maven 命令快速创建一个 maven 项目

使用命令 mvn archetype:generate 根据提示选择需要的骨架,输入 groupId,artifactId,version 等信息,创建项目。需要注意的是:maven2 中使用上面的命令,会去下载最新的版本,而不是稳定的版本,可能导致运行失败。maven3 则默认是下载稳定的版本。

坐标

  • groupId:命名通常为域名反向一一对应。比如 google.com 可以命名为 com.google
  • artifactId:命名推荐的做法是使用实际的项目名称作为 artifactId 的前缀。比如项目名是 demo,下面有个 hello 模块,就可以命名为 demo-hello
  • version:定义当前项目的版本信息。
  • packaging:定义元素打包方式,打包方式通常与所生成的构件文件扩展名对应。例如指定为 jar 则构件的文件扩展名为 .jar,war 也如此。不定义 packaging 时,默认打包为 jar。打包方式会影响到构建的生命周期,比如 jar 和 war 打包会使用不同的命令。
  • classifier:定义构建输出的一些附属构建。例如项目打包生成 demo.jar 文件,打包过程中可能还需要 javadoc 文件,就可以配置对应的插件来生成 javadoc 文件。

依赖的配置

  • type:依赖的类型,对应于项目坐标定义的 packaging。大部分情况下,该元素不必声明,默认为 jar。
  • scope:依赖的作用范围。
  • optional:标记依赖是否可选。
  • exclusions:用来排除传递性依赖。

依赖的范围

前置知识:Maven 编译项目主代码的时候使用一套 classpath,在编译执行测试的时候使用另一套 classpath,而实际运行 Maven 项目时候又会使用一套 classpath,因此总共有三套 classpath。

依赖范围就是来控制与这三种 classath(编译 classpath,测试 classpath,运行 classpath)的关系。如果没有指定依赖范围,就会默认使用 compile依赖范围。

依赖范围与 classpath 关系

| 依赖范围

(Scope) 对于编译 classpath 有效 对于测试 classpath有效 对于运行时 classpath 有效 例子
compile Y Y Y spring-core
test Y JUnit
provided Y Y servlet-api
runtime Y Y JDBC 驱动实现
system Y Y 本地的 maven 仓库外的类库文件

传递性依赖和依赖范围

传递性依赖:A –> B –> C,假设 A 依赖 B,B 依赖 C。称 A 对于 B 是第一直接依赖,B 对于 C 是第二直接依赖,A 对于 C 是传递性依赖。

上面说了依赖范围不仅与三种 classpath 有关系,还对传递性依赖产生影响。如下表格所示,最左边的一行表示第一依赖范围,最上面一行表示第二直接依赖范围。

依赖范围影响传递性依赖

compile test provided runtime
compile compile runtime
test test test
provided provided provided provided
runtime runtime runtime

助记规律:

  • 当第二直接依赖范围是 compile 时,传递性依赖范围与第一直接依赖范围一致。
  • 当第二直接依赖范围是 test 时,依赖不会得以传递。
  • 当第二直接依赖范围是 provided 时,只传递第一依赖范围也为 provided的依赖。
  • 当第二直接依赖范围是 runtime 时,传递性依赖范围与第一直接依赖范围一致,但 compile 除外,此时传递性依赖范围为 runtime

依赖调节

就近原则:A –> B –> C(1.0),A –> B –> D –> C(2.0),前者的路径长度为2,后者为3,因此 C(1.0) 被解析使用。

先声明先使用:遇到长度一样的,无法使用就近原则,就根据声明顺序,先声明的先解析使用。

可选依赖

假设 A 依赖 B,B 依赖 X 和 Y,B 对于 X 和 Y 的依赖是可选依赖。A –> B,B –> X(可选),B –> Y(可选),可选依赖不会得到传递,换句话说选择 X 或者 Y,对于 A 都不会有任何影响。

可选依赖在 dependency标签中需要添加 <optional>true</optional>用来标记为可选依赖,因此 B 需要添加 X 和 Y 的依赖,并且分别在 dependency标签加上 <optional>true</optional>

当需要指定所使用的依赖时,需要在外层声明需要使用的依赖,上面的例子就需要在 A 中引入 X 或者 Y 的依赖。在理想的情况下,尽量避免可选依赖的使用。

远程仓库认证

当我们使用 <repository></repository> 配置好一个远程仓库后,大部分情况下无需认证就可以直接访问,但是处于安全考虑,我们需要提供一些认证信息才能访问。此时,我们就需要配置认证信息,一般在 setting.xml 文件中配置,如下:

1
2
3
4
5
6
7
8
9
<setting>
...
<server>
<id>remoteRepository</id>
<username>admin</username>
<password>admin</password>
</server>
...
</setting>

文件中配置了认证信息,账号密码都是 admin,需要注意的是 id 的值要与配置的远程仓库的 id 一致。

部署至远程仓库

编写项目的 pom.xml 文件,配置 distributionManagement 元素,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<project>
...
<distributionManagement>
<repository>
<id></id>
<name></name>
<url></url>
</repository>

<snapshotRepository>
<id></id>
<name></name>
<url></url>
</snapshotRepository>
</distributionManagement>
...
</project>

其中 distributionManagement 包含两个子元素,前者表示发布版本构件的仓库,后者表示快照版本的仓库。两个元素都需要配置 id,name,url,id 为远程仓库的唯一标识,name 为了方便阅读(取名),url 远程仓库地址。

发布到远程仓库的时候,往往需要认证,认证步骤与远程仓库认证一致。之后使用命令 mvn clean deploy maven 就会把项目构建输出到对应的远程仓库。

镜像

如果仓库 X 可以提供仓库 Y 存储的所有内容,那么就可以认为 X 是 Y 的一个镜像。换句话说,任何一个从 Y 仓库获取的构件,都可以从它的镜像中获取。由于地理原因,国内访问 maven 中央仓库很慢,因此就需要配置一个镜像,通常使用阿里云的镜像居多。配置文件如下:

1
2
3
4
5
6
<mirror>
<id>aliyunmaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>

改配置文件中 <mirrOf> 的值为 central,表示任何访问该仓库的请求都会转到这个镜像中。如果镜像需要认证,也可以基于这个 <id> aliyunmaven,配置仓库认证。

Maven 的 <mirrOf> 标签还支持一些更高级的配置

  • <mirrorOf>*</mirrorOf>匹配所有远程仓库
  • <mirrorOf>external: *</mirrorOf>匹配所有不在本机上的远程仓库
  • <mirrorOf>repo1,repo2</mirrorOf>匹配仓库 rpeo1,repo2,使用逗号分隔多个远程仓库。
  • <mirrorOf>*,!repo1</mirrorOf>匹配所有远程仓库,repo1除外。

生命周期

maven 的生命周期其实就是软件从初始化到发布上线经历的一系列过程。maven 将这个过程抽象化了,每个周期所执行的操作都是交给插件完成。

maven 拥有三套相互独立的生命周期,分别是 clean,default,site。clean 负责清理项目,default 负责构建项目,site 负责建立项目站点。

每个生命周期都包含一些阶段(phase),这些阶段都是有顺序的,并且后面的阶段依赖于前面的阶段。比如 clean 生命周期包含: pre-cleancleanpost-clean 阶段,当用户调用 pre-clean,只有 pre-clean 阶段会执行,当调用 post-clean 时,pre-clean,clean,post-clean 都会执行。由于三个生命周期互不影响,当调用某个生命周期的某个阶段时,对其他的生命周期没有任何影响。

clean 生命周期阶段

  • pre-clean 执行清理前需要完成的工作
  • clean 清理上一次构建生成的文件
  • post-clean 执行清理后需要完成的工作

default 生命周期阶段(主要)

  • validate 验证项目是否正确并且所有必要的信息都可用
  • compile 编译项目的源代码
  • test 使用合适的单元测试框架测试编译的源代码。这些测试不应该要求打包或部署代码
  • package 将编译后的代码打包成可分发的格式,例如 JAR。
  • verify 对集成测试的结果进行任何检查,以确保满足质量标准
  • install 将包安装到本地存储库中,作为本地其他项目的依赖项
  • deploy 在构建环境中完成,将最终包复制到远程存储库以与其他开发人员和项目共享。

site 生命周期阶段

  • pre-site 执行生成项目站点前需要完成的工作
  • site 生成项目站点文档
  • post-site 执行生成项目站点后需要完成的工作
  • site-deploy 将生成的项目站点发布到服务器上

命令行与生命周期

一些常见的 Maven 命令,解释其执行生命周期:

  • mvn clean 调用 clean 生命周期的 clean 阶段,即执行 pre-cleanclean阶段。
  • mvn test 调用 default 生命周期的 test 阶段,即执行 validatecompiletest 阶段。
  • mvn clean install 调用 clean 生命周期的 clean 阶段,以及 default 生命周期的 install 阶段。即执行 pre-cleanclean阶段,以及 validateinstall的所有阶段。
  • mvn clean deploy site-deploy 调用 clean 生命周期的 clean 阶段,default 生命周期的 deploy 阶段,以及 site 生命周期的 site-deploy 阶段,实际执行过程按照上面类推。

插件目标和插件绑定

插件目标

一个插件往往能执行多个任务,因此插件具有多个功能,插件的每个功能就是一个**插件目标 (goal)**。

maven-dependency-plugin就有十多个目标,每个目标对应一个功能。例如:dependency:listdependency:tree分别是不同的功能,这是一种通用的写法,冒号前面是插件前缀,冒号后面是该插件的目标。类似的,compiler:compile(这是 maven-compiler-plugin 的 compile 目标)和 surefire:test(这是 maven-surefire-plugin 的 test 目标)。

插件绑定

maven 的生命周期与插件相互绑定,具体而言就是生命周期的阶段与插件的目标相互绑定。例如 mvn compile就是 default 生命周期 compile 阶段与 maven-compiler-plugin 插件的 compile目标绑定。

内置绑定

为了简化配置,maven 为一些核心的生命周期阶段绑定了很多插件的目标。当用户调用对于的生命周期阶段时,绑定的插件目标就会执行相应的任务。

例如:clean 生命周期的 clean 阶段,就绑定了 maven-clean-plugin插件的 clean 目标绑定。site 生命周期的 site 阶段绑定 maven-site-plugin 插件的 site 目标,site-deploy 阶段绑定到 maven-site-pugin插件的 deploy目标。

由于项目的打包类型会影响构建的具体过程,因此 default 的生命周期阶段与插件的目标绑定关系与打包类型有关系。
default 生命周期的内置插件绑定关系(打包类型为 jar)

生命周期阶段 插件目标 执行任务
process-resources maven-resources-plugin:resources 复制主资源文件至输出目录
compile maven-compiler-plugin:compile 编译主代码至输出目录
process-test-resources maven-resources-plugin:testResources 复制测试资源文件至测试输出目录
test-compile maven-compiler-plugin:testCompile 编译测试代码至测试输出目录
test maven-surefire-plugin:test 执行测试用例
package maven-jar-plugin:jar 创建项目 jar 包
install maven-install-plugin:install 将项目构建输出到本地仓库
deploy maven-deploy-plugin:deploy 将项目构建输出到远程仓库

上面只是列举了 default 生命周期绑定了插件的阶段,并非 default 的所有阶段。

自定义绑定

除了上面的内置绑定外,插件还支持自定义绑定,拿 maven-compiler-plugin插件举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<executions>
<execution>
<id>diy-compile</id>
<phase>verify</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>

上面代码将 maven-compiler-plugin插件的 testCompile目标绑定到 default 上面周期的 verify 阶段。当运行 mvn verify 时,该插件的 testCompile 目标就会执行。phase 表示绑定的声明周期阶段,因为插件可以在一个生命周期阶段运行多个方法,所以可以配置多个目标,即多个 goal。

有的时候,没有使用 phase,插件目标目标也能绑定到声明周期的阶段中去。这是因为很多插件在目标的编写时候已经定义好了默认绑定的阶段。可以使用命令查看 mvn help:describe -Dplugin=compiler -Ddetail 查看插件的详细信息,默认绑定的阶段等。

当一个阶段有多个插件目标绑定时,执行顺序与插件的声明顺序有关,先声明先执行

插件配置

插件目标像 Java 中的函数一样,执行相应的功能,并且还有参数,这些参数的具体值,我们可以配置,传递给插件目标。

命令行插件配置

命令行插件配置算是比较经典的配置手段了,在 Java 中可以使用 -D 参数配置系统属性。Maven 简单的重用了该参数,在检查插件的时候检查系统属性,实现了插件参数的配置。

例如:maven-compiler-plugin 插件就提供了一个 maven.test.skip的系统属性,通过配置是否跳过测试。以下是该插件 testCompile目标的 skip 参数详细信息。

1
2
3
4
5
skip
User property: maven.test.skip
Set this to 'true' to bypass compilation of test sources. Its use is NOT
RECOMMENDED, but quite convenient on occasion.

可以看到该参数可以使用 maven.test.skip,因此可以编写命令 maven install -Dmaven.test.skip=true 用来跳过测试。

POM 插件配置

对于一些固定常用的参数,可以直接在 pom 文件中配置,使用 configuration 配置 参数,如下:

1
2
3
4
5
6
7
8
 <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<warSourceExcludes>WEB-INF/classes</warSourceExcludes>
</configuration>
</plugin>

如果需要插件在不同声明周期执行相同的目标,但是执行的参数不一样,就需要另外配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
 <plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<executions>
<execution>
<id>diy-compile</id>
<phase>install</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
...
</configuration>
</execution>

<execution>
<id>diy-test</id>
<phase>verify</phase>
<goals>
<goal>testCompile</goal>
</goals>
<configuration>
...
</configuration>
</execution>
</executions>
</plugin>
</plugins>

如上同一插件的同一目标绑定了不同的声明周期阶段,具体的参数配置只需要在各自的 configuration 配置即可。

聚合与继承

聚合

为了方便使用命令管理多个模块,就需要用到聚合。例如:项目中有 A,B 两个模块,需要对两个项目都执行清理 操作,因此你需要跑到 A 模块下执行 mvn clean命令,然后再跑到 B 模块下执行相同的命令。模块较多的话就比较麻烦。

使用聚合就可以解决上述问题。新建一个聚合模块,然后将聚合模块的打包方式修改成 pom,并使用标签 modules引入模块 A,B,然后对聚合模块操作即可。示例代码:

1
2
3
4
5
<packaging>pom</packaging>
<modules>
<module>A模块目录名称</module>
<module>B模块目录名称</module>
</modules>

聚合模块可以与其他模块同级,同级只需要将 module 改为 <module>../A模块目录名称</module> ,特别注意的是聚合模块的打包方式一定要修改成pom。

继承

继承的出现是为了消除重复,Java 如此,Maven 中的 pom 也是如此。假如有 A,B两个模块都需要 spring-core 2.0 版本的依赖,正常的做法就是在两个模块中分别声明同一个 spring-core 依赖。

使用继承消除重复配置,新建一个模块 C,修改打包方式为 pom (与聚合操作一致),接着引入 spring-core依赖,即可。这时 A,B 模块只需要继承 C 的 pom 即可,代码如下。

1
2
3
4
5
6
<parent>
<groupId>C 模块groupId</groupId>
<artifactId>C 模块artifactId</artifactId>
<version>C 模块version</version>
<relativePath>C 模块pom.xml路径(相对路径)</relativePath>
</parent>

引入 C 模块的坐标信息,并使用 relativePath 指向 C 模块的 pom 文件路径。Maven 会首先根据 relativePath 找 pom,找不到就去本地仓库找,如果不配置 realtivePath,那默认值是 ../pom.xml,也就是说 Maven 默认父 模块在当前模块的上一层目录。

在上面的例子中 A,B 模块可以不用声明 gruopId 和 version,这些信息都从 C 模块中继承过来了,如果子模块要使用自己的 groupId 和 version 直接显示的声明即可。

Maven 可继承的 POM 元素

  • groupId 项目组 ID ,项目坐标的核心元素。
  • version 项目版本,项目坐标的核心元素。
  • description 项目的描述信息。
  • organization 项目的组织信息。
  • inceptionYear 项目的创始年份。
  • url 项目的 url 地址。
  • develoers 项目的开发者信息。
  • contributors 项目的贡献者信息。
  • distributionManagerment 项目的部署信息。
  • issueManagement 缺陷跟踪系统信息。
  • ciManagement 项目的持续继承信息。
  • scm 项目的版本控制信息。
  • mailingListserv 项目的邮件列表信息。
  • properties 自定义的 Maven 属性。
  • dependencies 项目的依赖配置。
  • dependencyManagement 醒目的依赖管理配置。
  • repositories 项目的仓库配置。
  • build 包括项目的源码目录配置、输出目录配置、插件配置、插件管理配置等。
  • reporting 包括项目的报告输出目录配置、报告插件配置等。

依赖管理

上面说到依赖可以继承,但是并不是每一个模块都需要引入父模块的依赖。例如一个工具模块,就不太需要 spring这些依赖包,但是因为继承关系,导致工具模块也引入了这些包,为了避免这种情况就需要使用新的元素 dependencyManagement。

dependencyManagement 可以让子类自行的选择继承父模块的依赖,只需要将父模块的依赖用元素 dependencyManagement包裹起来即可。例如父模块依赖代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependencyManagement>
<dependencies>

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

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>5.0</version>
<scope>test</scope>
</dependency>

</dependencies>
</dependencyManagement>

子模块引用父模块依赖,代码如下:

1
2
3
4
5
6
7
8
<dependencies>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>

</dependencies>

子模块 pom 直接引用所需要的依赖坐标即可,上面代码只引用了父模块的 junit 依赖,因此 spring-core 不会被引入。子模块还去掉了依赖的 version 和 scope 信息,这是因为继承了父模块的 pom,完整的依赖声明已经在父 pom 中了,因此子模块只需要 groupId 和 artifactId 就能获取对应的依赖信息,这样使用也能使所有子模块统一依赖版本,降低依赖冲突概率。

import 和 dependencyManagement

上面的章节说到过依赖范围 import,import 需要配合 dependencyManagement 使用。作用是将目标模块的 pom 中的 dependencyManagement 配置,导入并合并到当前模块的 pom 的 dependencyManagement 配置中。

例如想要在另一个模块中使用父模块完全一样的 dependencyManagement 配置,需要在 pom 编写如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
<dependencyManagement>

<dependencies>
<dependency>
<groupId>父模块 groupId</groupId>
<artifactId>父模块 artifactId</artifactId>
<version>父模块 version</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>

</dependencyManagement>

插件管理

插件管理的思想和依赖管理一致。在父 pom 中使用 pluginManagement 包裹插件,即可达到与依赖管理一样的效果。

子模块使用插件只需要声明 groupidartifactId即可。若子模块需要修改插件的配置信息,只需要在模块中覆盖父类的配置即可。

继承和聚合关系

都要将打包方式改为 pom,一个模块可以既是聚合模块,又是父模块。

约定优于配置

我们都知道 Maven 项目的源代码文件夹是 src/main/java,编译输出文件夹是 target/classes/,这是 maven 的约定,我们只需要准守即可。当然想要自己改变这些约定也可以做到,但是不推荐。我们准守的这些约定实际上是 maven 的超级 pom 帮我们配置了这些信息。

所有的 maven 项目都隐式地继承了超级 pom,这个超级 pom 配置了源代码目录路径,编译输出路径,测试代码路径等等,这些配置信息就是我们要准守的约定。超级 pom 可以到 maven 的安装目录下 /lib/maven-model-builder-x.x.x.jar文件(x.x.x 代表的是 maven 版本 ),解压之后找到 org/apache/maven/pom-4.0.0.xml 文件即可查看配置信息。这是我本机的超级 pom 的部分配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<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>

反应堆

之前使用到聚合的知识,用一个聚合模块包含多个模块,只要对聚合模块操作即可影响到包含的模块。我们把包含的这一堆模块叫做反应堆。为啥叫反应堆呢,因为需要发生反应,在构建这些模块的时候肯定存在一个构建顺序,依次构建。

例如聚合模块 A,按顺序包含模块 B,C,D。按照正常理解,对 A 模块进行构建时,我们会认为,B,C,D 会按照顺序依次构建,但实际的构建顺序却不一定是我们预期的那样。

实际的构建顺序是这样的:Maven 先按照顺序读取 pom,如果 pom 没有依赖模块,那么就构建该模块,否则就构建其依赖模块,若依赖模块还依赖其他模块,则进一步构建依赖模块。

在实际开发中可以使用一些参数,来决定需要构建的模块,或者指定模块构建的顺序。假设模块 A,模块 B 都依赖于模块 C。

  • -am, 同时构建所列模块的依赖模块。例如:mvn clean install -pl A -am,结果顺序:C ,A。
  • -amd 同时构建依赖于所列模块的模块。例如:mvn clean install -pl C -amd,结果顺序:C,A,B
  • -pl 构建指定的模块,模块间用逗号分隔。例如:mvn clean install -pl A,B
  • -rf 在完整的构建顺序基础上指定从哪个模块开始构建。例如:mvn clean install -pl C -amd -rf B 结果顺序:C,B,A

《Maven 实战》笔记
http://wszzf.top/2021/08/20/《Maven 实战》/
作者
Greek
发布于
2021年8月20日
许可协议