Maven基础

Maven是一个Java项目管理和构建工具,它可以定义项目结构、项目依赖,并使用统一的方式进行自动化构建,是Java项目不可缺少的工具。本章我们详细介绍如何使用Maven。

Maven介绍

我们先来看看一个Java项目需要的东西。首先,我们需要确定引入哪些依赖包。其次,我们要确定项目的目录结构。例如,src目录存放Java源码,resources目录存放配置文件,bin目录存放编译后的生成的.class文件。此外,我们还需要配置环境,比如JDK的版本,编译打包流程,当前代码的版本号。最后,除了使用IDE编译外,我们还必须能通过命令行工具进行编译,才能让项目在一个独立的服务器上编译、测试、部署。

这些工作难度不大,但是非常繁琐和耗时。

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

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

Maven项目结构

一个使用Maven管理的普通的Java项目,它的目录结构默认如下:

1
2
3
4
5
6
7
8
9
10
a-maven-project
├── pom.xml
├── src
│ ├── main
│ │ ├── java
│ │ └── resources
│ └── test
│ ├── java
│ └── resources
└── target

根目录a-maven-project是项目名,它有一个项目描述文件pom.xmlsrc/main/java是存放Java源码的目录,src/main/resources是存放资源文件的目录,src/test/java是存放测试源码的目录,src/test/resources是存放测试资源的目录,最后,所有编译、打包生成的文件都在target目录里。

所有的目录结构都是约定好的标准结构,千万不要随意修改目录结构。标准结构无需任何配置就可以正常使用。

我们来看一下最关键的pom.xml,它长得像下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<project ...>
<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>hello</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<properties>
...
</properties>
<dependencies>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
</dependencies>
</project>

其中,groupId类似Java的包名,通常是公司或组织的名称,artifactId类似于Java的类名,通常是项目名称,再加上version。一个Maven工程就是由groupIdartifactIdversion作为唯一标识。我们在引用其他第三方库时,也是通过这三个变量确定,例如,依赖commons-logging

1
2
3
4
5
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>

使用<dependency>声明一个依赖后,Maven就会自动下载这个依赖包并把它放到classpath中。

依赖管理

如果我们的项目依赖第三方的jar包,例如commons logging,那么commons logging发布的jar包在哪下载?同样的,如果我们也依赖log4j,那么使用log4j需要哪些jar包?类似的依赖还包括JUnit,JavaMail。MySQL驱动等等,一个可行的方案是通过搜索引擎搜索到项目的官网,然后手动下载zip包,解压,放入classpath。但是,这个过程非常繁琐。

Maven解决了依赖管理。

例如,我们的项目依赖abc这个jar包,而abc又依赖xyz这个jar包。当我们声明了abc这个依赖时,Maven自动把abcxyz都加入我们的项目依赖,不需要我们自己研究依赖关系。

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

我们来看一个复杂的依赖示例。

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>1.4.2.RELEASE</version>
</dependency>

当我们声明一个spring-boot-starter-web依赖时,Maven会自动解析并判断最终需要二三十个其他的依赖。如果我们自己去手动管理这些依赖不仅耗时而且出错的概率也很大。

依赖关系

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

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

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

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

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

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

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

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

唯一ID

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

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

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

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

Maven镜像

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

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

1
2
3
4
5
6
7
8
9
10
11
<settings>
<mirrors>
<mirror>
<id>aliyun</id>
<name>aliyun</name>
<mirrorOf>central</mirrorOf>
<!-- 国内推荐阿里云的Maven镜像 -->
<url>https://maven.aliyun.com/repository/central</url>
</mirror>
</mirrors>
</settings>

配置镜像仓库后,Maven的下载速度会非常快。

搜索第三方组件

如果我们要引用一个第三方组件,如何确切地获得它的groupIdartifactIdversion?可以在https://search.maven.org/搜索关键字,找到对应组件后,直接复制。

命令行编译

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

1
$ mvn clean package

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

在IDE中使用Maven

几乎所有的IDE都内置了对Maven的支持。

构建流程

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

Lifecycle和Phase

我们首先了解什么是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:打包

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服务器。

1
mvn tomcat:run

小结

Maven通过lifecycle、phase和goal来提供标准的构建流程。

最常用的构建命令是指定phase,然后让Maven执行到指定的phase:

  • mvn clean
  • mvn clean compile
  • mvn clean test
  • mvn clean package

通常情况,我们总是执行phase默认绑定的goal,因此不必指定goal。

使用插件

我们在前面介绍了Maven的lifecycle,phase和goal。使用Maven构建项目就是执行lifescycle,执行到指定的phase为止,每个phase会执行自己默认的一个或多个goal。goal是最小的任务单元。

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

1
mvn compile

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

实际上,执行每个phase都是通过某个插件(plugin)执行的,Maven本身不知道如何执行compile,它只是负责找到对应的compiler插件,然后执行默认的compiler:compile这个goal来完成编译。所以,使用Maven,实际上就是配置好需要使用的插件,然后通过phase来调用它们。Maven已经内置了一些常用的标准插件:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<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程序的入口,它的配置是:

1
2
3
4
5
6
7
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.itranswarp.learnjava.Main</mainClass>
</transformer>
</transformers>
</configuration>

Maven自带的标准插件是无需声明的,只有引入其他插件才需要声明。插件的配置和用法需参考插件的官方文档。

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

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

模块管理

在软件开发中,把一个大项目拆分为多个模块是降低软件复杂度的有效方法。Maven可以有效的管理多个模块,我们只需要把每个模块当成一个独立的Maven项目,他们有各自独立的pom.xml

例如,A模块的pom.xml和B模块的pom.xml高度相似,可以提取出共同的部分作为parent。注意设置<packaging>pom</packaging>,而不是jar。因为parent本身不包含任何的Java代码,编写parentpom.xml只是为了在各个模块里简化配置。现在我们的工程目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
multiple-project
├── pom.xml
├── parent
│ └── pom.xml
├── module-a
│ ├── pom.xml
│ └── src
├── module-b
│ ├── pom.xml
│ └── src
└── module-c
├── pom.xml
└── src

这样就可以大幅简化配置文件的编写。

如果模块A依赖模块B,则模块A需要模块B的jar包才能正常编译,在模块A的pom.xml中就像这样:

1
2
3
4
5
6
7
8
...
<dependencies>
<dependency>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>module-b</artifactId>
<version>1.0</version>
</dependency>
</dependencies>

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<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,一次性全部编译。

中央仓库,私有仓库和本地仓库

中央仓库和私有仓库略。

本地仓库

本地仓库是指把本地开发的项目“发布”在本地,这样其他项目可以通过本地仓库引用它。但是我们不推荐把自己的模块安装到Maven的本地仓库,因为每次修改某个模块的源码,都需要重新安装,非常容易出现版本不一致的情况。更好的方法是使用模块化编译,在编译的时候,告诉Maven几个模块之间存在依赖关系,需要一块编译,Maven就会自动按依赖顺序编译这些模块。

使用mvnw

mvnw时Maven Wrapper的缩写。我们安装Maven时,默认情况下,系统的所有项目都会使用全局安装的这个Maven版本。但是,对某个项目来说,可能必需要使用特定版本的Maven,这时,就可以使用Maven Wrapper。它负责给这个特定的项目安装指定版本的Maven,而其他项目不受影响。

简单来说,Maven Wrapper就是给一个项目提供一个独立的、指定版本的Maven供该项目使用。

安装Maven Wrapper

最简单的方式是在项目的根目录下运行安装命令:

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

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

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

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

安装后,查看项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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。例如:

1
mvnw clean package

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

发布Artifact

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

那么,当我们自己写了一个开源库,非常希望别人也能使用时,总不能直接放个jar包的链接让别人下载吧?

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

以静态文件发布

如果我们观察一个中央仓库的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仓库。

我们以一个项目为例,先创建Maven工程目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
how-to-become-rich
├── maven-repo <-- Maven本地文件仓库
├── pom.xml <-- 项目文件
├── src
│ ├── main
│ │ ├── java <-- 源码目录
│ │ └── resources <-- 资源目录
│ └── test
│ ├── java <-- 测试源码目录
│ └── resources <-- 测试资源目录
└── target <-- 编译输出目录

pom.xml里添加如下内容:

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
<project ...>
...
<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目录下找到部署后的所有文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
maven-repo
└── com
└── itranswarp
└── rich
└── how-to-become-rich
├── 1.0.0
│ ├── how-to-become-rich-1.0.0-javadoc.jar
│ ├── how-to-become-rich-1.0.0-javadoc.jar.md5
│ ├── how-to-become-rich-1.0.0-javadoc.jar.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
│ ├── 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
├── maven-metadata.xml
├── maven-metadata.xml.md5
└── maven-metadata.xml.sha1

最后一步,把这个项目推到GitHub上,并选择Settings-GitHub Pages,选择master branch启用Pages服务。

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

1
https://michaelliao.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包,我们可以告知如下依赖即可:

1
2
3
4
5
<dependency>
<groupId>com.itranswarp.rich</groupId>
<artifactId>how-to-become-rich</artifactId>
<version>1.0.0</version>
</dependency>

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

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
<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://michaelliao.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/后缀。现在,即可正常引用这个库并编写代码如下:

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

有的童鞋会问,为什么使用commons-logging等第三方库时,并不需要声明repo地址?这是因为这些库都是发布到Maven中央仓库的,发布到中央仓库后,不需要告诉Maven仓库地址,因为它知道中央仓库的地址默认是https://repo1.maven.org/maven2/,也可以通过~/.m2/settings.xml指定一个代理仓库地址以替代中央仓库来提高速度(参考依赖管理的Maven镜像)。

因为GitHub Pages并不会把我们发布的Maven包同步到中央仓库,所以自然使用方必须手动添加一个我们提供的仓库地址。

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

通过Nexus发布到中央仓库

此方法前期需要复杂的申请账号和项目的流程,后期需要安装调试GPG,但只要跑通流程,后续发布都只需要一行命令。

具体过程略,需要时再查看教程。

网络编程 加密与安全
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×