Dunwu Blog

大道至简,知易行难

Maven 教程之发布 jar 到私服或中央仓库

发布 jar 包到中央仓库

为了避免重复造轮子,相信每个 Java 程序员都想打造自己的脚手架或工具包(自己定制的往往才是最适合自己的)。那么如何将自己的脚手架发布到中央仓库呢?下面我们将一步步来实现。

在 Sonatype 创建 Issue

(1)注册 Sonatype 账号

发布 Java 包到 Maven 中央仓库,首先需要在 Sonatype 网站创建一个工单(Issues)。

第一次使用这个网站的时候需要注册自己的帐号(这个帐号和密码需要记住,后面会用到)。

(2)创建 Issue

注册账号成功后,根据你 Java 包的功能分别写上SummaryDescriptionGroup IdSCM url以及Project URL等必要信息,可以参见我之前创建的 Issue:OSSRH-36187

img

创建完之后需要等待 Sonatype 的工作人员审核处理,审核时间还是很快的,我的审核差不多等待了两小时。当 Issue 的 Status 变为RESOLVED后,就可以进行下一步操作了。

说明:如果你的 Group Id 填写的是自己的网站(我的就是这种情况),Sonatype 的工作人员会询问你那个 Group Id 是不是你的域名,你只需要在上面回答是就行,然后就会通过审核。

使用 GPG 生成公私钥对

(1)安装 Gpg4win

Windows 系统,可以下载 Gpg4win 软件来生成密钥对。

Gpg4win 下载地址

安装后,执行命令 gpg –version 检查是否安装成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\Program Files (x86)\GnuPG\bin>gpg --version
gpg (GnuPG) 2.2.10
libgcrypt 1.8.3
Copyright (C) 2018 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the exdunwu permitted by law.

Home: C:/Users/Administrator/AppData/Roaming/gnupg
Supported algorithms:
Pubkey: RSA, ELG, DSA, ECDH, ECDSA, EDDSA
Cipher: IDEA, 3DES, CAST5, BLOWFISH, AES, AES192, AES256, TWOFISH,
CAMELLIA128, CAMELLIA192, CAMELLIA256
Hash: SHA1, RIPEMD160, SHA256, SHA384, SHA512, SHA224
Compression: Uncompressed, ZIP, ZLIB, BZIP2

(2)生成密钥对

执行命令 gpg --gen-key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
C:\Program Files (x86)\GnuPG\bin>gpg --gen-key
gpg (GnuPG) 2.2.10; Copyright (C) 2018 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the exdunwu permitted by law.

Note: Use "gpg --full-generate-key" for a full featured key generation dialog.

GnuPG needs to construct a user ID to identify your key.

Real name: Zhang Peng
Email address: forbreak@163.com
You selected this USER-ID:
"Zhang Peng <forbreak@163.com>"

Change (N)ame, (E)mail, or (O)kay/(Q)uit? O

说明:按照提示,依次输入用户名、邮箱。

(3)查看公钥

1
2
3
4
5
6
7
8
9
10
11
12
C:\Program Files (x86)\GnuPG\bin>gpg --list-keys

gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 2 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 2u
gpg: next trustdb check due at 2020-11-05
C:/Users/Administrator/AppData/Roaming/gnupg/pubring.kbx
--------------------------------------------------------
pub rsa2048 2018-11-06 [SC] [expires: 2020-11-06]
E4CE537A3803D49C35332221A306519BAFF57F60
uid [ultimate] forbreak <forbreak@163.com>
sub rsa2048 2018-11-06 [E] [expires: 2020-11-06]

说明:其中,E4CE537A3803D49C35332221A306519BAFF57F60 就是公钥

(4)将公钥发布到 PGP 密钥服务器

执行 gpg --keyserver hkp://pool.sks-keyservers.net --send-keys 发布公钥:

1
2
C:\Program Files (x86)\GnuPG\bin>gpg --keyserver hkp://pool.sks-keyservers.net --send-keys E4CE537A3803D49C35332221A306519BAFF57F60
gpg: sending key A306519BAFF57F60 to hkp://pool.sks-keyservers.net

🔔 注意:有可能出现 gpg: keyserver receive failed: No dat 错误,等大约 30 分钟后再执行就不会报错了。

(5)查看公钥是否发布成功

执行 gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys 查看公钥是否发布成功。

1
2
3
4
C:\Program Files (x86)\GnuPG\bin>gpg --keyserver hkp://pool.sks-keyservers.net --recv-keys E4CE537A3803D49C35332221A306519BAFF57F60
gpg: key A306519BAFF57F60: "forbreak <forbreak@163.com>" not changed
gpg: Total number processed: 1
gpg: unchanged: 1

Maven 配置

完成了前两个章节的准备工作,就可以将 jar 包上传到中央仓库了。当然了,我们还要对 maven 做一些配置。

settings.xml 配置

一份完整的 settings.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
40
41
42
43
44
45
46
47
48
<?xml version="1.0" encoding="UTF-8"?>

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">

<pluginGroups>
<pluginGroup>org.sonatype.plugins</pluginGroup>
</pluginGroups>

<!-- 用户名、密码就是 Sonatype 账号、密码 -->
<servers>
<server>
<id>sonatype-snapshots</id>
<username>xxxxxx</username>
<password>xxxxxx</password>
</server>
<server>
<id>sonatype-staging</id>
<username>xxxxxx</username>
<password>xxxxxx</password>
</server>
</servers>

<!-- 使用 aliyun maven 仓库加速下载 -->
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>*</mirrorOf>
<name>Aliyun</name>
<url>http://maven.aliyun.com/nexus/groups/public</url>
</mirror>
</mirrors>

<!-- gpg 的密码,注意,这里不是指公钥 -->
<profiles>
<profile>
<id>sonatype</id>
<properties>
<gpg.executable>C:/Program Files (x86)/GnuPG/bin/gpg.exe</gpg.executable>
<gpg.passphrase>xxxxxx</gpg.passphrase>
</properties>
</profile>
</profiles>

<activeProfiles>
<activeProfile>sonatype</activeProfile>
</activeProfiles>
</settings>

pom.xml 配置

(1)添加 licenses、scm、developers 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
<url>http://www.apache.org/licenses/LICENSE-2.0.txt</url>
<distribution>repo</distribution>
</license>
</licenses>

<developers>
<developer>
<name>xxxxxx</name>
<email>forbreak@163.com</email>
<url>https://github.com/dunwu</url>
</developer>
</developers>

<scm>
<url>https://github.com/dunwu/dunwu</url>
<connection>git@github.com:dunwu/dunwu.git</connection>
<developerConnection>https://github.com/dunwu</developerConnection>
</scm>

(2)添加 distributionManagement 配置

1
2
3
4
5
6
7
8
9
10
<distributionManagement>
<snapshotRepository>
<id>sonatype-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>sonatype-staging</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2</url>
</repository>
</distributionManagement>

说明:<snapshotRepository> 指定的是 snapshot 仓库地址;<repository> 指定的是 staging (正式版)仓库地址。需要留意的是,这里的 id 需要和 settings.xml 中的 <server> 的 id 保持一致。

(3)添加 profiles 配置

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
42
43
44
45
46
47
48
49
50
51
52
 <profiles>
<profile>
<id>sonatype</id>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>1.6.7</version>
<extensions>true</extensions>
<configuration>
<serverId>sonatype-snapshots</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.0.1</version>
<configuration>
<failOnError>false</failOnError>
<quiet>true</quiet>
</configuration>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>sign-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

部署和发布

按照上面的步骤配置完后,一切都已经 OK。

此时,使用 mvn clean deploy -P sonatype 命令就可以发布 jar 包到中央仓库了:

说明:-P 参数后面的 sonatype 需要和 pom.xml 中 <profile> 的 id 保持一致,才能激活 profile。

部署 maven 私服

工作中,Java 程序员开发的商用 Java 项目,一般不想发布到中央仓库,使得人人尽知。这时,我们就需要搭建私服,将 maven 服务器部署在公司内部网络,从而避免 jar 包流传出去。怎么做呢,让我们来一步步学习吧。

下载安装 Nexus

进入官方下载地址,选择合适版本下载。

img

本人希望将 Nexus 部署在 Linux 机器,所以选用的是 Unix 版本。

这里,如果想通过命令方式直接下载(比如用脚本安装),可以在官方历史发布版本页面中找到合适版本,然后执行以下命令:

1
2
wget -O /opt/maven/nexus-unix.tar.gz http://download.sonatype.com/nexus/3/nexus-3.13.0-01-unix.tar.gz
tar -zxf nexus-unix.tar.gz

解压后,有两个目录:

  • nexus-3.13.0-01 - 包含了 Nexus 运行所需要的文件。是 Nexus 运行必须的。
  • sonatype-work - 包含了 Nexus 生成的配置文件、日志文件、仓库文件等。当我们需要备份 Nexus 的时候默认备份此目录即可。

启动停止 Nexus

进入 nexus-3.13.0-01/bin 目录,有一个可执行脚本 nexus。

执行 ./nexus,可以查看允许执行的参数,如下所示,含义可谓一目了然:

1
2
$ ./nexus
Usage: ./nexus {start|stop|run|run-redirect|status|restart|force-reload}
  • 启动 nexus - ./nexus start
  • 停止 nexus -

启动成功后,在浏览器中访问 http://<ip>:8081,欢迎页面如下图所示:

img

点击右上角 Sign in 登录,默认用户名/密码为:admin/admin123。

有必要提一下的是,在 Nexus 的 Repositories 管理页面,展示了可用的 maven 仓库,如下图所示:

img

说明:

  • maven-central - maven 中央库(如果没有配置 mirror,默认就从这里下载 jar 包),从 https://repo1.maven.org/maven2/ 获取资源
  • maven-releases - 存储私有仓库的发行版 jar 包
  • maven-snapshots - 存储私有仓库的快照版(调试版本) jar 包
  • maven-public - 私有仓库的公共空间,把上面三个仓库组合在一起对外提供服务,在本地 maven 基础配置 settings.xml 中使用。

使用 Nexus

如果要使用 Nexus,还必须在 settings.xml 和 pom.xml 中配置认证信息。

配置 settings.xml

一份完整的 settings.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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?xml version="1.0" encoding="UTF-8"?>

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
<pluginGroups>
<pluginGroup>org.sonatype.plugins</pluginGroup>
</pluginGroups>

<!-- Maven 私服账号信息 -->
<servers>
<server>
<id>releases</id>
<username>admin</username>
<password>admin123</password>
</server>
<server>
<id>snapshots</id>
<username>admin</username>
<password>admin123</password>
</server>
</servers>

<!-- jar 包下载地址 -->
<mirrors>
<mirror>
<id>public</id>
<mirrorOf>*</mirrorOf>
<url>http://10.255.255.224:8081/repository/maven-public/</url>
</mirror>
</mirrors>

<profiles>
<profile>
<id>zp</id>
<repositories>
<repository>
<id>central</id>
<url>http://central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>central</id>
<url>http://central</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
<updatePolicy>always</updatePolicy>
</snapshots>
</pluginRepository>
</pluginRepositories>
</profile>
</profiles>

<activeProfiles>
<activeProfile>zp</activeProfile>
</activeProfiles>
</settings>

配置 pom.xml

在 pom.xml 中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
<distributionManagement>
<repository>
<id>releases</id>
<name>Releases</name>
<url>http://10.255.255.224:8081/repository/maven-releases</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<name>Snapshot</name>
<url>http://10.255.255.224:8081/repository/maven-snapshots</url>
</snapshotRepository>
</distributionManagement>

🔔 注意:

  • <repository><snapshotRepository> 的 id 必须和 settings.xml 配置文件中的 <server> 标签中的 id 匹配。
  • <url> 标签的地址需要和 maven 私服的地址匹配。

执行 maven 构建

如果要使用 settings.xml 中的私服配置,必须通过指定 -P zp 来激活 profile。

示例:

1
2
3
4
5
## 编译并打包 maven 项目
$ mvn clean package -Dmaven.skip.test=true -P zp

## 编译并上传 maven 交付件(jar 包)
$ mvn clean deploy -Dmaven.skip.test=true -P zp

参考资料

深入理解 Java 序列化

img

序列化简介

由于,网络传输的数据必须是二进制数据,但调用方请求的出入参数都是对象。对象是不能直接在网络中传输的,所以需要提前把它转成可传输的二进制,并且要求转换算法是可逆的。

  • 序列化(serialize):序列化是将对象转换为二进制数据。
  • 反序列化(deserialize):反序列化是将二进制数据转换为对象。

img

序列化用途

  • 序列化可以将对象的字节序列持久化——保存在内存、文件、数据库中。
  • 在网络上传送对象的字节序列。
  • RMI(远程方法调用)

JDK 序列化

JDK 中内置了一种序列化方式。

ObjectInputStream 和 ObjectOutputStream

Java 通过对象输入输出流来实现序列化和反序列化:

  • java.io.ObjectOutputStream 类的 writeObject() 方法可以实现序列化;
  • java.io.ObjectInputStream 类的 readObject() 方法用于实现反序列化。

序列化和反序列化示例:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class SerializeDemo01 {
enum Sex {
MALE,
FEMALE
}

static class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name = null;
private Integer age = null;
private Sex sex;

public Person() { }

public Person(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}

@Override
public String toString() {
return "Person{" + "name='" + name + '\'' + ", age=" + age + ", sex=" + sex + '}';
}
}

/**
* 序列化
*/
private static void serialize(String filename) throws IOException {
File f = new File(filename); // 定义保存路径
OutputStream out = new FileOutputStream(f); // 文件输出流
ObjectOutputStream oos = new ObjectOutputStream(out); // 对象输出流
oos.writeObject(new Person("Jack", 30, Sex.MALE)); // 保存对象
oos.close();
out.close();
}

/**
* 反序列化
*/
private static void deserialize(String filename) throws IOException, ClassNotFoundException {
File f = new File(filename); // 定义保存路径
InputStream in = new FileInputStream(f); // 文件输入流
ObjectInputStream ois = new ObjectInputStream(in); // 对象输入流
Object obj = ois.readObject(); // 读取对象
ois.close();
in.close();
System.out.println(obj);
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
final String filename = "d:/text.dat";
serialize(filename);
deserialize(filename);
}
}
// Output:
// Person{name='Jack', age=30, sex=MALE}

JDK 的序列化过程是怎样完成的呢?

img

图来自 RPC 实战与核心原理 - 序列化

序列化过程就是在读取对象数据的时候,不断加入一些特殊分隔符,这些特殊分隔符用于在反序列化过程中截断用,这就像是文章中的标点符号被用于断句一样。

  • 头部数据用来声明序列化协议、序列化版本,用于高低版本向后兼容。
  • 对象数据主要包括类名、签名、属性名、属性类型及属性值,当然还有开头结尾等数据,除了属性值属于真正的对象值,其他都是为了反序列化用的元数据。
  • 存在对象引用、继承的情况下,就是递归遍历“写对象”逻辑。

🔔 注意:使用 Java 对象序列化,在保存对象时,会把其状态保存为一组字节,在未来,再将这些字节组装成对象。必须注意地是,对象序列化保存的是对象的”状态”,即它的成员变量。由此可知,对象序列化不会关注类中的静态变量

Serializable 接口

被序列化的类必须属于 EnumArraySerializable 类型其中的任何一种,否则将抛出 NotSerializableException 异常。这是因为:在序列化操作过程中会对类型进行检查,如果不满足序列化类型要求,就会抛出异常。

【示例】NotSerializableException 错误

1
2
3
4
public class UnSerializeDemo {
static class Person { // 其他内容略 }
// 其他内容略
}

输出:结果就是出现如下异常信息。

1
2
Exception in thread "main" java.io.NotSerializableException:
...

serialVersionUID

请注意 serialVersionUID 字段,你可以在 Java 世界的无数类中看到这个字段。

serialVersionUID 有什么作用,如何使用 serialVersionUID

serialVersionUID 是 Java 为每个序列化类产生的版本标识。它可以用来保证在反序列时,发送方发送的和接受方接收的是可兼容的对象。如果接收方接收的类的 serialVersionUID 与发送方发送的 serialVersionUID 不一致,会抛出 InvalidClassException

如果可序列化类没有显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值。尽管这样,还是建议在每一个序列化的类中显式指定 serialVersionUID 的值。因为不同的 jdk 编译很可能会生成不同的 serialVersionUID 默认值,从而导致在反序列化时抛出 InvalidClassExceptions 异常。

serialVersionUID 字段必须是 static final long 类型

我们来举个例子:

(1)有一个可序列化类 Person

1
2
3
4
5
6
7
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
// 构造方法、get、set 方法略
}

(2)开发过程中,对 Person 做了修改,增加了一个字段 email,如下:

1
2
3
4
5
6
7
8
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
private Integer age;
private String address;
private String email;
// 构造方法、get、set 方法略
}

由于这个类和老版本不兼容,我们需要修改版本号:

1
private static final long serialVersionUID = 2L;

再次进行反序列化,则会抛出 InvalidClassException 异常。

综上所述,我们大概可以清楚:**serialVersionUID 用于控制序列化版本是否兼容**。若我们认为修改的可序列化类是向后兼容的,则不修改 serialVersionUID

默认序列化机制

如果仅仅只是让某个类实现 Serializable 接口,而没有其它任何处理的话,那么就会使用默认序列化机制。

使用默认机制,在序列化对象时,不仅会序列化当前对象本身,还会对其父类的字段以及该对象引用的其它对象也进行序列化。同样地,这些其它对象引用的另外对象也将被序列化,以此类推。所以,如果一个对象包含的成员变量是容器类对象,而这些容器所含有的元素也是容器类对象,那么这个序列化的过程就会较复杂,开销也较大。

🔔 注意:这里的父类和引用对象既然要进行序列化,那么它们当然也要满足序列化要求:被序列化的类必须属于 Enum、Array 和 Serializable 类型其中的任何一种

JDK 序列化要点

Java 的序列化能保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是难以处理,这里归纳一下:

  • 父类是 Serializable,所有子类都可以被序列化。
  • 子类是 Serializable ,父类不是,则子类可以正确序列化,但父类的属性不会被序列化(不报错,数据丢失)。
  • 如果序列化的属性是对象,则这个对象也必须是 Serializable ,否则报错。
  • 反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错。
  • 反序列化时,如果 serialVersionUID 被修改,则反序列化会失败。

transient

在现实应用中,有些时候不能使用默认序列化机制。比如,希望在序列化过程中忽略掉敏感数据,或者简化序列化过程。下面将介绍若干影响序列化的方法。

序列化时,默认序列化机制会忽略被声明为 transient 的字段,该字段的内容在序列化后无法访问。

我们将 SerializeDemo01 示例中的内部类 Person 的 age 字段声明为 transient,如下所示:

1
2
3
4
5
6
7
8
9
public class SerializeDemo02 {
static class Person implements Serializable {
transient private Integer age = null;
// 其他内容略
}
// 其他内容略
}
// Output:
// name: Jack, age: null, sex: MALE

从输出结果可以看出,age 字段没有被序列化。

Externalizable 接口

无论是使用 transient 关键字,还是使用 writeObject()readObject() 方法,其实都是基于 Serializable 接口的序列化。

JDK 中提供了另一个序列化接口–Externalizable

可序列化类实现 Externalizable 接口之后,基于 Serializable 接口的默认序列化机制就会失效

我们来基于 SerializeDemo02 再次做一些改动,代码如下:

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
public class ExternalizeDemo01 {
static class Person implements Externalizable {
transient private Integer age = null;
// 其他内容略

private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}

@Override
public void writeExternal(ObjectOutput out) throws IOException { }

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { }
}
// 其他内容略
}
// Output:
// call Person()
// name: null, age: null, sex: null

从该结果,一方面可以看出 Person 对象中任何一个字段都没有被序列化。另一方面,如果细心的话,还可以发现这此次序列化过程调用了 Person 类的无参构造方法。

  • Externalizable 继承于 Serializable,它增添了两个方法:writeExternal()readExternal()。这两个方法在序列化和反序列化过程中会被自动调用,以便执行一些特殊操作。当使用该接口时,序列化的细节需要由程序员去完成。如上所示的代码,由于 writeExternal()readExternal() 方法未作任何处理,那么该序列化行为将不会保存/读取任何一个字段。这也就是为什么输出结果中所有字段的值均为空。
  • 另外,若使用 Externalizable 进行序列化,当读取对象时,会调用被序列化类的无参构造方法去创建一个新的对象;然后再将被保存对象的字段的值分别填充到新对象中。这就是为什么在此次序列化过程中 Person 类的无参构造方法会被调用。由于这个原因,实现 Externalizable 接口的类必须要提供一个无参的构造方法,且它的访问权限为 public

对上述 Person 类作进一步的修改,使其能够对 name 与 age 字段进行序列化,但要忽略掉 gender 字段,如下代码所示:

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
public class ExternalizeDemo02 {
static class Person implements Externalizable {
transient private Integer age = null;
// 其他内容略

private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}

@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeObject(name);
out.writeInt(age);
}

@Override
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
name = (String) in.readObject();
age = in.readInt();
}
}
// 其他内容略
}
// Output:
// call Person()
// name: Jack, age: 30, sex: null

Externalizable 接口的替代方法

实现 Externalizable 接口可以控制序列化和反序列化的细节。它有一个替代方法:实现 Serializable 接口,并添加 writeObject(ObjectOutputStream out)readObject(ObjectInputStream in) 方法。序列化和反序列化过程中会自动回调这两个方法。

示例如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SerializeDemo03 {
static class Person implements Serializable {
transient private Integer age = null;
// 其他内容略

private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}
// 其他内容略
}
// 其他内容略
}
// Output:
// name: Jack, age: 30, sex: MALE

writeObject() 方法中会先调用 ObjectOutputStream 中的 defaultWriteObject() 方法,该方法会执行默认的序列化机制,如上节所述,此时会忽略掉 age 字段。然后再调用 writeInt() 方法显示地将 age 字段写入到 ObjectOutputStream 中。readObject() 的作用则是针对对象的读取,其原理与 writeObject() 方法相同。

🔔 注意:writeObject()readObject() 都是 private 方法,那么它们是如何被调用的呢?毫无疑问,是使用反射。详情可见 ObjectOutputStream 中的 writeSerialData 方法,以及 ObjectInputStream 中的 readSerialData 方法。

readResolve() 方法

当我们使用 Singleton 模式时,应该是期望某个类的实例应该是唯一的,但如果该类是可序列化的,那么情况可能会略有不同。此时对第 2 节使用的 Person 类进行修改,使其实现 Singleton 模式,如下所示:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
public class SerializeDemo04 {

enum Sex {
MALE, FEMALE
}

static class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name = null;
transient private Integer age = null;
private Sex sex;
static final Person instatnce = new Person("Tom", 31, Sex.MALE);

private Person() {
System.out.println("call Person()");
}

private Person(String name, Integer age, Sex sex) {
this.name = name;
this.age = age;
this.sex = sex;
}

public static Person getInstance() {
return instatnce;
}

private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(age);
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
age = in.readInt();
}

public String toString() {
return "name: " + this.name + ", age: " + this.age + ", sex: " + this.sex;
}
}

/**
* 序列化
*/
private static void serialize(String filename) throws IOException {
File f = new File(filename); // 定义保存路径
OutputStream out = new FileOutputStream(f); // 文件输出流
ObjectOutputStream oos = new ObjectOutputStream(out); // 对象输出流
oos.writeObject(new Person("Jack", 30, Sex.MALE)); // 保存对象
oos.close();
out.close();
}

/**
* 反序列化
*/
private static void deserialize(String filename) throws IOException, ClassNotFoundException {
File f = new File(filename); // 定义保存路径
InputStream in = new FileInputStream(f); // 文件输入流
ObjectInputStream ois = new ObjectInputStream(in); // 对象输入流
Object obj = ois.readObject(); // 读取对象
ois.close();
in.close();
System.out.println(obj);
System.out.println(obj == Person.getInstance());
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
final String filename = "d:/text.dat";
serialize(filename);
deserialize(filename);
}
}
// Output:
// name: Jack, age: null, sex: MALE
// false

值得注意的是,从文件中获取的 Person 对象与 Person 类中的单例对象并不相等。为了能在单例类中仍然保持序列的特性,可以使用 readResolve() 方法。在该方法中直接返回 Person 的单例对象。我们在 SerializeDemo04 示例的基础上添加一个 readResolve 方法, 如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SerializeDemo05 {
// 其他内容略

static class Person implements Serializable {

// private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
// in.defaultReadObject();
// age = in.readInt();
// }

// 添加此方法
private Object readResolve() {
return instatnce;
}
// 其他内容略
}

// 其他内容略
}
// Output:
// name: Tom, age: 31, sex: MALE
// true

JDK 序列化的问题

  • 无法跨语言:JDK 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 JDK 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。
  • 容易被攻击:对象是通过在 ObjectInputStream 上调用 readObject() 方法进行反序列化的,它可以将类路径上几乎所有实现了 Serializable 接口的对象都实例化。这意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 hashCode 方法被调用次数呈次方爆发式增长,从而引发栈溢出异常。例如下面这个案例就可以很好地说明。
  • 序列化后的流太大:JDK 序列化中使用了 ObjectOutputStream 来实现对象转二进制编码,编码后的数组很大,非常影响存储和传输效率。
  • 序列化性能太差:Java 的序列化耗时比较大。序列化的速度也是体现序列化性能的重要指标,如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间。
  • 序列化编程限制
    • JDK 序列化一定要实现 Serializable 接口
    • JDK 序列化**需要关注 serialVersionUID**。

二进制序列化

上节详细介绍了 JDK 序列化方式,由于其性能不高,且存在很多其他问题,所以业界有了很多其他优秀的二进制序列化库。

Protobuf

Protobuf 是 Google 公司内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。Protobuf 使用的时候需要定义 IDL(Interface description language),然后使用不同语言的 IDL
编译器,生成序列化工具类。

优点:

  • 序列化后体积相比 JSON、Hessian 小很多
  • 序列化反序列化速度很快,不需要通过反射获取类型
  • 语言和平台无关(基于 IDL),IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似 XML 解析器
  • 消息格式升级和兼容性不错,可以做到后向兼容
  • 支持 Java, C++, Python 三种语言

缺点:

  • Protobuf 对于具有反射和动态能力的语言来说,用起来很费劲。

Thrift

Thrift 是 apache 开源项目,是一个点对点的 RPC 实现。

它具有以下特性:

  • 支持多种语言(目前支持 28 种语言,如:C++、go、Java、Php、Python、Ruby 等等)。
  • 使用了组建大型数据交换及存储工具,对于大型系统中的内部数据传输,相对于 Json 和 xml 在性能上和传输大小上都有明显的优势。
  • 支持三种比较典型的编码方式(通用二进制编码,压缩二进制编码,优化的可选字段压缩编解码)。

Hessian

Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。

RPC 框架 Dubbo 就支持 Thrift 和 Hession。

它具有以下特性:

  • 支持多种语言。如:Java、Python、C++、C#、PHP、Ruby 等。
  • 相对其他二进制序列化库较慢。

Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类型不支持:

  • Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展 CollectionDeserializer 类修复;
  • Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
  • Byte/Short 反序列化的时候变成 Integer。

Kryo

Kryo 是用于 Java 的快速高效的二进制对象图序列化框架。Kryo 还可以执行自动的深拷贝和浅拷贝。 这是从对象到对象的直接复制,而不是从对象到字节的复制。

它具有以下特性:

  • 速度快,序列化体积小
  • 官方不支持 Java 以外的其他语言

FST

FST 是一个 Java 实现二进制序列化库。

它具有以下特性:

  • 近乎于 100% 兼容 JDK 序列化,且比 JDK 原序列化方式快 10 倍
  • 2.17 开始与 Android 兼容
  • (可选)2.29 开始支持将任何可序列化的对象图编码/解码为 JSON(包括共享引用)

JSON 序列化

除了二进制序列化方式,还可以选择 JSON 序列化。它的性能比二进制序列化方式差,但是可读性非常好,且广泛应用于 Web 领域。

JSON 是什么

JSON 起源于 1999 年的 JS 语言规范 ECMA262 的一个子集(即 15.12 章节描述了格式与解析),后来 2003 年作为一个数据格式 ECMA404(很囧的序号有不有?)发布。
2006 年,作为 rfc4627 发布,这时规范增加到 18 页,去掉没用的部分,十页不到。

JSON 的应用很广泛,这里有超过 100 种语言下的 JSON 库:json.org

更多的可以参考这里,关于 json 的一切

JSON 标准

这估计是最简单标准规范之一:

  • 只有两种结构:对象内的键值对集合结构和数组,对象用 {} 表示、内部是 "key":"value",数组用 [] 表示,不同值用逗号分开
  • 基本数值有 7 个: false / null / true / object / array / number / string
  • 再加上结构可以嵌套,进而可以用来表达复杂的数据
  • 一个简单实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"Image": {
"Width": 800,
"Height": 600,
"Title": "View from 15th Floor",
"Thumbnail": {
"Url": "http://www.example.com/image/481989943",
"Height": 125,
"Width": "100"
},
"IDs": [116, 943, 234, 38793]
}
}

扩展阅读:

JSON 优缺点

JSON 优点

  • 基于纯文本,所以对于人类阅读是很友好的。
  • 规范简单,所以容易处理,开箱即用,特别是 JS 类的 ECMA 脚本里是内建支持的,可以直接作为对象使用。
  • 平台无关性,因为类型和结构都是平台无关的,而且好处理,容易实现不同语言的处理类库,可以作为多个不同异构系统之间的数据传输格式协议,特别是在 HTTP/REST 下的数据格式。

JSON 缺点

  • 性能一般,文本表示的数据一般来说比二进制大得多,在数据传输上和解析处理上都要更影响性能。
  • 缺乏 schema,跟同是文本数据格式的 XML 比,在类型的严格性和丰富性上要差很多。XML 可以借由 XSD 或 DTD 来定义复杂的格式,并由此来验证 XML 文档是否符合格式要求,甚至进一步的,可以基于 XSD 来生成具体语言的操作代码,例如 apache xmlbeans。并且这些工具组合到一起,形成一套庞大的生态,例如基于 XML 可以实现 SOAP 和 WSDL,一系列的 ws-*规范。但是我们也可以看到 JSON 在缺乏规范的情况下,实际上有更大一些的灵活性,特别是近年来 REST 的快速发展,已经有一些 schema 相关的发展(例如 理解 JSON Schema使用 JSON Schema在线 schema 测试),也有类似于 WSDL 的 WADL 出现。

JSON 库

Java 中比较流行的 JSON 库有:

  • Fastjson - 阿里巴巴开发的 JSON 库,性能十分优秀。
  • Jackson - 社区十分活跃且更新速度很快。Spring 框架默认 JSON 库。
  • Gson - 谷歌开发的 JSON 库,目前功能最全的 JSON 库 。

从性能上来看,一般情况下:Fastjson > Jackson > Gson

JSON 编码指南

遵循好的设计与编码风格,能提前解决 80%的问题,个人推荐 Google JSON 风格指南。

简单摘录如下:

  • 属性名和值都是用双引号,不要把注释写到对象里面,对象数据要简洁
  • 不要随意结构化分组对象,推荐是用扁平化方式,层次不要太复杂
  • 命名方式要有意义,比如单复数表示
  • 驼峰式命名,遵循 Bean 规范
  • 使用版本来控制变更冲突
  • 对于一些关键字,不要拿来做 key
  • 如果一个属性是可选的或者包含空值或 null 值,考虑从 JSON 中去掉该属性,除非它的存在有很强的语义原因
  • 序列化枚举类型时,使用 name 而不是 value
  • 日期要用标准格式处理
  • 设计好通用的分页参数
  • 设计好异常处理

JSON API 与 Google JSON 风格指南有很多可以相互参照之处。

JSON API 是数据交互规范,用以定义客户端如何获取与修改资源,以及服务器如何响应对应请求。

JSON API 设计用来最小化请求的数量,以及客户端与服务器间传输的数据量。在高效实现的同时,无需牺牲可读性、灵活性和可发现性。

序列化技术选型

市面上有如此多的序列化技术,那么我们在应用时如何选择呢?

序列化技术选型,需要考量的维度,根据重要性从高到低,依次有:

  • 安全性:是否存在漏洞。如果存在漏洞,就有被攻击的可能性。
  • 兼容性:版本升级后的兼容性是否很好,是否支持更多的对象类型,是否是跨平台、跨语言的。服务调用的稳定性与可靠性,要比服务的性能更加重要。
  • 性能
    • 时间开销:序列化、反序列化的耗时性能自然越小越好。
    • 空间开销:序列化后的数据越小越好,这样网络传输效率就高。
  • 易用性:类库是否轻量化,API 是否简单易懂。

鉴于以上的考量,序列化技术的选型建议如下:

  • JDK 序列化:性能较差,且有很多使用限制,不建议使用。
  • ThriftProtobuf:适用于对性能敏感,对开发体验要求不高
  • Hessian:适用于对开发体验敏感,性能有要求
  • JacksonGsonFastjson:适用于对序列化后的数据要求有良好的可读性(转为 json 、xml 形式)。

参考资料

Spring 集成 Mybatis

Mybatis 官网 是一款持久层框架,它支持定制化 SQL、存储过程以及高级映射。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以使用简单的 XML 或注解来配置和映射原生类型、接口和 Java 的 POJO(Plain Old Java Objects,普通老式 Java 对象)为数据库中的记录。

快速入门

要使用 MyBatis, 只需将 mybatis-x.x.x.jar 文件置于类路径(classpath)中即可。

如果使用 Maven 来构建项目,则需将下面的依赖代码置于 pom.xml 文件中:

1
2
3
4
5
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>x.x.x</version>
</dependency>

从 XML 中构建 SqlSessionFactory

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先配置的 Configuration 实例来构建出 SqlSessionFactory 实例。

从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。 但也可以使用任意的输入流(InputStream)实例,比如用文件路径字符串或 file:// URL 构造的输入流。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,使得从类路径或其它位置加载资源文件更加容易。

1
2
3
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

XML 配置文件中包含了对 MyBatis 系统的核心设置,包括获取数据库连接实例的数据源(DataSource)以及决定事务作用域和控制方式的事务管理器(TransactionManager)。后面会再探讨 XML 配置文件的详细内容,这里先给出一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>

当然,还有很多可以在 XML 文件中配置的选项,上面的示例仅罗列了最关键的部分。 注意 XML 头部的声明,它用来验证 XML 文档的正确性。environment 元素体中包含了事务管理和连接池的配置。mappers 元素则包含了一组映射器(mapper),这些映射器的 XML 映射文件包含了 SQL 代码和映射定义信息。

不使用 XML 构建 SqlSessionFactory

如果你更愿意直接从 Java 代码而不是 XML 文件中创建配置,或者想要创建你自己的配置构建器,MyBatis 也提供了完整的配置类,提供了所有与 XML 文件等价的配置项。

1
2
3
4
5
6
DataSource dataSource = BlogDataSourceFactory.getBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.addMapper(BlogMapper.class);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);

注意该例中,configuration 添加了一个映射器类(mapper class)。映射器类是 Java 类,它们包含 SQL 映射注解从而避免依赖 XML 映射文件。不过,由于 Java 注解的一些限制以及某些 MyBatis 映射的复杂性,要使用大多数高级映射(比如:嵌套联合映射),仍然需要使用 XML 映射文件进行映射。有鉴于此,如果存在一个同名 XML 映射文件,MyBatis 会自动查找并加载它(在这个例子中,基于类路径和 BlogMapper.class 的类名,会加载 BlogMapper.xml)。具体细节稍后讨论。

从 SqlSessionFactory 中获取 SqlSession

既然有了 SqlSessionFactory,顾名思义,我们可以从中获得 SqlSession 的实例。SqlSession 提供了在数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如:

1
2
3
try (SqlSession session = sqlSessionFactory.openSession()) {
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}

诚然,这种方式能够正常工作,对使用旧版本 MyBatis 的用户来说也比较熟悉。但现在有了一种更简洁的方式——使用和指定语句的参数和返回值相匹配的接口(比如 BlogMapper.class),现在你的代码不仅更清晰,更加类型安全,还不用担心可能出错的字符串字面值以及强制类型转换。

例如:

1
2
3
4
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);
}

现在我们来探究一下这段代码究竟做了些什么。

探究已映射的 SQL 语句

现在你可能很想知道 SqlSession 和 Mapper 到底具体执行了些什么操作,但 SQL 语句映射是个相当广泛的话题,可能会占去文档的大部分篇幅。 但为了让你能够了解个大概,这里先给出几个例子。

在上面提到的例子中,一个语句既可以通过 XML 定义,也可以通过注解定义。我们先看看 XML 定义语句的方式,事实上 MyBatis 提供的所有特性都可以利用基于 XML 的映射语言来实现,这使得 MyBatis 在过去的数年间得以流行。如果你用过旧版本的 MyBatis,你应该对这个概念比较熟悉。 但相比于之前的版本,新版本改进了许多 XML 的配置,后面我们会提到这些改进。这里给出一个基于 XML 映射语句的示例,它应该可以满足上个示例中 SqlSession 的调用。

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>

为了这个简单的例子,我们似乎写了不少配置,但其实并不多。在一个 XML 映射文件中,可以定义无数个映射语句,这样一来,XML 头部和文档类型声明部分就显得微不足道了。文档的其它部分很直白,容易理解。 它在命名空间 “org.mybatis.example.BlogMapper” 中定义了一个名为 “selectBlog” 的映射语句,这样你就可以用全限定名 “org.mybatis.example.BlogMapper.selectBlog” 来调用映射语句了,就像上面例子中那样:

1
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);

你可能会注意到,这种方式和用全限定名调用 Java 对象的方法类似。这样,该命名就可以直接映射到在命名空间中同名的映射器类,并将已映射的 select 语句匹配到对应名称、参数和返回类型的方法。因此你就可以像上面那样,不费吹灰之力地在对应的映射器接口调用方法,就像下面这样:

1
2
BlogMapper mapper = session.getMapper(BlogMapper.class);
Blog blog = mapper.selectBlog(101);

第二种方法有很多优势,首先它不依赖于字符串字面值,会更安全一点;其次,如果你的 IDE 有代码补全功能,那么代码补全可以帮你快速选择到映射好的 SQL 语句。

提示 对命名空间的一点补充

在之前版本的 MyBatis 中,命名空间(Namespaces)的作用并不大,是可选的。 但现在,随着命名空间越发重要,你必须指定命名空间。

命名空间的作用有两个,一个是利用更长的全限定名来将不同的语句隔离开来,同时也实现了你上面见到的接口绑定。就算你觉得暂时用不到接口绑定,你也应该遵循这里的规定,以防哪天你改变了主意。 长远来看,只要将命名空间置于合适的 Java 包命名空间之中,你的代码会变得更加整洁,也有利于你更方便地使用 MyBatis。

命名解析:为了减少输入量,MyBatis 对所有具有名称的配置元素(包括语句,结果映射,缓存等)使用了如下的命名解析规则。

  • 全限定名(比如 “com.mypackage.MyMapper.selectAllThings)将被直接用于查找及使用。
  • 短名称(比如 “selectAllThings”)如果全局唯一也可以作为一个单独的引用。 如果不唯一,有两个或两个以上的相同名称(比如 “com.foo.selectAllThings” 和 “com.bar.selectAllThings”),那么使用时就会产生“短名称不唯一”的错误,这种情况下就必须使用全限定名。

对于像 BlogMapper 这样的映射器类来说,还有另一种方法来完成语句映射。 它们映射的语句可以不用 XML 来配置,而可以使用 Java 注解来配置。比如,上面的 XML 示例可以被替换成如下的配置:

1
2
3
4
5
package org.mybatis.example;
public interface BlogMapper {
@Select("SELECT * FROM blog WHERE id = #{id}")
Blog selectBlog(int id);
}

使用注解来映射简单语句会使代码显得更加简洁,但对于稍微复杂一点的语句,Java 注解不仅力不从心,还会让本就复杂的 SQL 语句更加混乱不堪。 因此,如果你需要做一些很复杂的操作,最好用 XML 来映射语句。

选择何种方式来配置映射,以及是否应该要统一映射语句定义的形式,完全取决于你和你的团队。 换句话说,永远不要拘泥于一种方式,你可以很轻松地在基于注解和 XML 的语句映射方式间自由移植和切换。

作用域(Scope)和生命周期

理解我们之前讨论过的不同作用域和生命周期类别是至关重要的,因为错误的使用会导致非常严重的并发问题。

提示 对象生命周期和依赖注入框架

依赖注入框架可以创建线程安全的、基于事务的 SqlSession 和映射器,并将它们直接注入到你的 bean 中,因此可以直接忽略它们的生命周期。 如果对如何通过依赖注入框架使用 MyBatis 感兴趣,可以研究一下 MyBatis-Spring 或 MyBatis-Guice 两个子项目。

SqlSessionFactoryBuilder

这个类可以被实例化、使用和丢弃,一旦创建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 实例的最佳作用域是方法作用域(也就是局部方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例,但最好还是不要一直保留着它,以保证所有的 XML 解析资源可以被释放给更重要的事情。

SqlSessionFactory

SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由丢弃它或重新创建另一个实例。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏习惯”。因此 SqlSessionFactory 的最佳作用域是应用作用域。 有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。

SqlSession

每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不是线程安全的,因此是不能被共享的,所以它的最佳的作用域是请求或方法作用域。 绝对不能将 SqlSession 实例的引用放在一个类的静态域,甚至一个类的实例变量也不行。 也绝不能将 SqlSession 实例的引用放在任何类型的托管作用域中,比如 Servlet 框架中的 HttpSession。 如果你现在正在使用一种 Web 框架,考虑将 SqlSession 放在一个和 HTTP 请求相似的作用域中。 换句话说,每次收到 HTTP 请求,就可以打开一个 SqlSession,返回一个响应后,就关闭它。 这个关闭操作很重要,为了确保每次都能执行关闭操作,你应该把这个关闭操作放到 finally 块中。 下面的示例就是一个确保 SqlSession 关闭的标准模式:

1
2
3
try (SqlSession session = sqlSessionFactory.openSession()) {
// 你的应用逻辑代码
}

在所有代码中都遵循这种使用模式,可以保证所有数据库资源都能被正确地关闭。

映射器实例

映射器是一些绑定映射语句的接口。映射器接口的实例是从 SqlSession 中获得的。虽然从技术层面上来讲,任何映射器实例的最大作用域与请求它们的 SqlSession 相同。但方法作用域才是映射器实例的最合适的作用域。 也就是说,映射器实例应该在调用它们的方法中被获取,使用完毕之后即可丢弃。 映射器实例并不需要被显式地关闭。尽管在整个请求作用域保留映射器实例不会有什么问题,但是你很快会发现,在这个作用域上管理太多像 SqlSession 的资源会让你忙不过来。 因此,最好将映射器放在方法作用域内。就像下面的例子一样:

1
2
3
4
try (SqlSession session = sqlSessionFactory.openSession()) {
BlogMapper mapper = session.getMapper(BlogMapper.class);
// 你的应用逻辑代码
}

Mybatis 扩展工具

Mybatis Plus

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

【集成示例】spring-boot-data-mybatis-plus

Mapper

Mapper 是一个 Mybatis CRUD 扩展插件。

Mapper 的基本原理是将实体类映射为数据库中的表和字段信息,因此实体类需要通过注解配置基本的元数据,配置好实体后, 只需要创建一个继承基础接口的 Mapper 接口就可以开始使用了。

【集成示例】spring-boot-data-mybatis-mapper

PageHelper

PageHelper 是一个 Mybatis 通用分页插件。

【集成示例】spring-boot-data-mybatis-mapper

参考资料

Windows 常用技巧总结

软件

扩展阅读:

视频音频

  • Musicbee - 类似 iTunes,但比 iTunes 更好用。
  • ScreenToGif - 它允许你录制屏幕的一部分区域并保存为 gif 或视频。
  • PotPlayer - 多媒体播放器,具有广泛的编解码器集合,它还为用户提供大量配置选项。
  • 射手影音播放器 - 来自射手网,小巧开源,首创自动匹配字幕功能。

压缩

  • 7-Zip - 用于处理压缩包的开源 Windows 实用程序。完美支持 7z,ZIP,GZIP,BZIP2 和 TAR 的全部特性,其他格式也可解压缩。
  • WinRAR - 强大的归档管理器。 它可以备份您的数据并减小电子邮件附件的大小,解压缩 RAR,ZIP 和其他文件。

文件管理

  • Clover - 为资源管理器加上多标签功能。
  • Total Commander - 老牌、功能异常强大的文件管理增强软件。
  • Q-Dir - 轻量级的文件管理器,各种布局视图切换灵活,默认四个小窗口组成一个大窗口,操作快捷。软件虽小,粉丝忠诚。
  • WoX - 新一代文件定位工具,堪称 Windows 上的 Alfred。
  • Everything - 最快的文件/文件夹搜索工具, 通过名称搜索。
  • Listary - 非常优秀的 Windows 文件浏览和搜索增强工具。
  • Beyond Compare - 好用又万能的文件对比工具。
  • CCleaner - 如果你有系统洁癖,那一定要选择一款干净、良心、老牌的清洁软件。
  • chocolatey - 包管理器
  • Ninite - 最简单,最快速的更新或安装软件的方式。
  • Recuva - 来自 piriform 梨子公司产品,免费的数据恢复工具。
  • Launchy:自由的跨平台工具,帮助你忘记开始菜单、桌面图标甚至文件管理器。

开发

  • Fiddler - web 调试代理工具。
  • Postman - 适合 API 开发的完整工具链,最常用的 REST 客户端。
  • SourceTree - 一个免费的 Git & Mercurial 客户端。
  • TortoiseSVN - Subversion(SVN)的图形客户端
  • Wireshark - 一个网络协议分析工具。
  • Switchhosts
  • Cmder - 控制台模拟器包。扩展阅读:Win 下必备神器之 Cmder
  • Babun - 基于 Cygwin,用于替代 Windows shell。

编辑器

  • JetBrain IDE 系列 - 真香!
  • Visual Studio Code - 用于构建和调试现代 Web 和云应用程序。
  • Eclipse - 一款功能强大的 IDE。
  • Visual Studio - 微软官方的 IDE,通过插件可支持大量编程语言。
  • NetBeans IDE - 免费开源的 IDE。
  • Typora - 个人觉得最好用的 Markdown 编辑器。
  • Cmd Markdown - 跨平台优秀 Markdown 编辑器,本文即用其所写。
  • Notepad++ - 一款支持多种编程语言的源码编辑器。
  • Notepad2 - 用于替代默认文本编辑器的轻量快速的编辑器,拥有众多有用的功能。
  • Sublime Text 3 - 高级文本编辑器。
  • Atom - 面向 21 世纪的极客文本编辑器。

文档

  • Microsoft Office - 微软办公软件。
  • WPS Office - 金山免费办公软件。
  • Calibre - 用于电子书管理和转换的强大软件。
  • 福昕阅读器 - 在全球拥有大量用户,最优秀的国产软件之一。Ribbon 界面,支持手写签名、插入印章等。

效率提升

【笔记】

  • XMind - 优秀的思维导图。
  • OneNote - Windows 下综合评价非常高的笔记应用。
  • 印象笔记 - 老牌跨平台笔记工具,国际版 Evernote。一家立志于做百年公司的企业,安全、可靠。
  • 为知笔记 - 越来越好的笔记应用,记录、查阅一切有价值的信息,同样跨平台支持。
  • 有道云笔记 - 网易旗下笔记工具,同样跨主流平台支持,文字、手写、录音、拍照多种记录方式,支持任意附件格式。
  • ShareX - 你要的所有与截图、录屏相关的功能,这里都有了。

【快捷键】

  • AutoHotkey - Windows 平台的终极自动化脚本语言。

技巧:

办公

  • 有道词典 - 最好用的免费全能翻译软件。
  • Outlook - 大名鼎鼎的 Microsoft Office 组件之一,除了电子邮件,还包含了日历、任务管理、联系人、记事本等功能。
  • Gmail - 功能上可以称为业界标杆,用户数量世界第一,或许你真的找不到比它更好的邮件系统。
  • Chrome - 最好的浏览器。
  • Teamviewer - 专业、功能强大的远程控制软件。使用简单,对个人用户免费。

个性化

  • TranslucentTB - 透明化你的 Windows 任务栏。
  • QTTabBar - 通过多标签和额外的文件夹视图扩展资源管理器的功能。
  • Fences - 管理桌面快捷方式。

参考资料

基本操作

软件管理

dmg 格式:双击安装包,然后拖到 applications 文件夹下即可。

浏览器

更改默认搜索引擎

选择“偏好设置–>搜索–>搜索引擎–>Google”。

导入 chrome 浏览器的书签

选择“文件–>导入自–> Google Chrome”,然后选择要导入的项目。

快捷键

Command + R 刷新

上方显示书签栏/收藏栏

选择“显示–> 显示个人收藏栏”。

关闭软件的右上角通知

在 Mac 系统中有对通知的设置,打开系统偏好设置 — 通知 找到 QQ,然后将 QQ 提示样式设置成无即可。

复制文件/文件夹路径

  • OS X 10.11 系统,选中文件夹,“cmd +Option +c” 复制文件夹路径,cmd+v 粘贴。
    之前的系统,利用 Administrator 创建一个到右键菜单,然后到设置里面设置快捷键。具体操作请百度。

打开来自身份不明的开发者的应用程序

在应用程序文件夹,按住 control 键的同时打开应用程序。

复制文件路径

  • 选择文件/文件夹按 Command+C 复制,在终端中 Command+V 粘贴即可。

  • 如果只是想在 Finder 中看到文件的路径, 并方便切换层级, Finder 内置了“显示路径栏”的功能, 并配置了快捷键(Option+Cmd+P). 如下图所示:

20161124-184148.png

参考链接:

隐藏和取消隐藏 Mac App Store 中的已购项目

Mac 同时登陆两个 QQ

在已经打开的 QQ 中,按住“command + N”即可。

系统便好设置

语音播报

打开“系统便好设置–>辅助功能–>语音”,即可设置不同国家的语言。

勾选上图中的红框部分,可以设置全局快捷键。这样的话,在任何一个软件当中,按下“ option+esc”时,就会朗读选中的文本。

调整字体大小

Mac 调整字体大小:“系统偏好设置 -> 显示器 -> 缩放”。如下图:

如何分别设置 Mac 的鼠标和触控板的滚动方向

很多人习惯鼠标使用相反的滚动方向,而触控板类似 iPad 那样的自然滚动,问如何设置,当时我的回答是不知道,因为目前 OS X 的系统设置里,鼠标和触控板的设置是统一
的。今天发现了一个免费的软件 Scroll Reverser,可以实现鼠标和触控板的分别设置。下载地址:https://pilotmoon.com/scrollreverser/
启动后程序显示在顶部菜单栏,设置简单明了,有需要的用户体验一下吧。

Touch Bar 自定义

打开“系统偏好设置-键盘”,下面有个自定义控制条。

色温调节:夜间模式

iOS9.3 的最明显变化,莫过于苹果在发布会上特意提到的 Night Shift 夜间护眼模式。

iCloud 邮箱

如果您用于设置 iCloud 的 Apple ID 不以“@icloud.com”、“@me.com”或“@mac.com”结尾,您必须先设置一个“@icloud.com”电子邮件地址,然后才能使用 iCloud“邮件”。

如果您拥有以“@mac.com”或“@me.com”结尾的电子邮件地址,则您已经拥有了名称相同但以“@icloud.com”结尾的等效地址。如果您使用的电子邮件别名以“@mac.com”或“@me.com”结尾,您也将拥有以“@icloud.com”结尾的等效地址。

操作如下:

  • 在 iOS 设备上,前往“设置”>“iCloud”,开启“邮件”,然后按照屏幕上的说明操作。

  • 在 Mac 上,选取 Apple 菜单 >“系统偏好设置”,点按“iCloud”,再选择“邮件”,然后按照屏幕上的说明操作。

PS:创建 iCloud 电子邮件地址后,您无法对其进行更改。

设置 @icloud.com 电子邮件地址后即可用其登录 iCloud。您也可以用创建 iCloud 帐户时所用的 Apple ID 登录。

您可以从以下任意地址发送 iCloud 电子邮件:

您的 iCloud 电子邮件地址(您的帐号名称@icloud.com)

别名

参考链接:

直接注册以@icloud.com 结尾的 Apple ID:

参考链接:

PodCast

PodCast 中文翻译为播客,是一种特殊的音频 or 视频节目。PodCast 这个单词是由 iPod+Broadcast 这两个单词组成的。

PodCast 可以在 iTunes 中收听。

others

词典

系统有一个自带应用“词典”,可以进行单词的查询。

如何解决 MAC 软件(dmg,akp,app)出现程序已损坏的提示

“xxx.app 已损坏,打不开.你应该将它移到废纸篓”,并非你安装的软件已损坏,而是 Mac 系统的安全设置问题,因为这些应用都是破解或者汉化的,那么解决方法就是临时改变 Mac 系统安全设置。

出现这个问题的解决方法:修改系统配置:系统偏好设置… -> 安全性与隐私。修改为任何来源。

如果没有这个选项的话(macOS Sierra 10.12),打开终端,执行:

1
sudo spctl --master-disable

即可。

参考链接:

备注:这个链接里的各种资源都很不错啊。

终端

在 Finder 的当前目录打开终端

在 Finder 打开 terminal 终端这个功能其实是有的,但是系统默认没有打开。我们可以通过如下方法将其打开:

进入系统偏好设置->键盘->快捷键->服务。

在右边新建位于文件夹位置的终端窗口上打勾。

如此设置后,在 Finder 中右击某文件,在出现的菜单中找到服务,然后点击新建位于文件夹位置的终端窗口即可!

Mac 常用快捷键

Finder

快捷键 作用 备注
Shift + Command + G 前往指定路径的文件夹 包括隐藏文件夹
Shift + Command + . 显示隐藏文件、文件夹 再按一次,恢复隐藏
Command + ↑ 返回上一层
Command + ↓ 进入当前文件夹

编辑

删除文字

快捷键 作用 备注
delete 删除光标的前一个字符 相当于 Windows 键盘上的退格键
fn + delete 删除光标的后一个字符
option + delete 删除光标之前的一个单词 英文有效
command + delete 删除光标之前的整行内容 【荐】
command + delete 在 finder 中删掉该文件
shift + command + delete 清空回收站

剪切文件

首先选中文件,按 Command+C 复制文件;然后按“Command + Option + V”剪切文件。

备注:Command+X 只能剪切文字文本,不要混淆了。

Mac 用户必须知道的 15 组快捷键

参考链接:《轻松玩 Mac》第 6 期:Mac 用户必须知道的 15 组快捷键

“space”键:快速预览

选中文件后, 不需要启动任何应用程序,使用“space”空格键可进行快速预览,再次按下“space”空格键取消预览。

可以预览 mp3、视频、pdf 等文件。

我们还可以选中多张图片, 然后按“space”键,就可以同时对比预览多张图片。这一点,很赞。

改名

选中文件/文件夹后,按 enter 键,就可以改名了。

“command + I”键:查看文件属性

  • 选中文件后,按“command + I”键,可以查看文件的各种属性。

  • 选中文件夹后,按“command + I”键,可以查看文件夹的大小。【荐】

切换输入法

“control + space”

打开 spotlight 搜索框

spotlight 是系统自带的软件,搜索功能不是很强大。我们一般都会用第三方的 Alfred 软件。

编辑相关

Cmd+C、Cmd+V、Cmd+X、Cmd+A、Cmd+Z。

翻页和光标

  • “control + ↑”:将光标定位到文章的最开头(翻页到文档的最上方)

  • “control + ↓”:将光标定位到文章的最末尾(翻页到文档的最下方)

  • “control + ←”:将光标定位到当前行的最左侧

  • “control + →”:将光标定位到当前行的最右侧

“command + shift + Y”:将文字快速保存到便笺

选中你想要的内容(例如文字、链接等),然后按下 command + shift + Y”,那么你选中的内容就会快速保存到系统自带的“便笺”软件中。

如果你想临时性的保存一段内容,这个操作很实用。

程序相关

  • “command + Q”:快速退出程序

  • “command + tab”:切换程序

  • “command + H”:隐藏当前应用程序。这是一个有趣的快捷键。

  • “command + ,”:打开当前应用程序的“偏好设置”。

窗口相关

  • “command + N”:新建一个当前应用程序的窗口

  • “command + `”:在当前应用程序的不同窗口之间切换【很实用】

我们知道,“command + tab”是在不同的软件之间切换。但你不知道的是,“command + `”是在同一个软件的不同窗口之间切换。

  • “command + M”:将当前窗口最小化

  • “command + W”:关闭当前窗口

浏览器相关

  • “command + T”:浏览器中,新建一个标签

  • “command + W”:关闭当前标签

  • “command + R”:强制刷新。
  • “command + L”:定位到地址栏。【重要】

截图相关

  • “command + shift + 3”:截全屏(对整个屏幕截图)。

声音相关

选中文字后,按住“ctrl + esc”键,会将文字进行朗读。(我发现,在触控条版的 mac 上,并没有生效)

Dock 栏相关

  • “option + command + D”:隐藏 dock 栏

强制推出

强制退出的快捷键非常重要

  • “option + command + esc”:打开强制退出的窗口

option 相关

强烈推荐

  • “option + command + H”:隐藏除当前应用程序之外的其他应用程序

  • 在文本中,按住“option”键,配合鼠标的选中,可以进行块状文字选取。

  • “option + command + W”:快速关闭当前应用程序的所有窗口。【很实用】

比如说,你一次性打开了很多文件的详情,然后就可以通过此快捷键,将这些窗口一次性关闭。

  • “option + command + I”:查看多个文件的总的属性。
  • 打开 launchpad,按住“option”键,可以快速卸载应用程序。

  • 在 dock 栏,右键点击软件图标,同时按住“option”键,就可以强制退出该软件。【重要】

  • 在 Safari 浏览器中,按住“option + command + Q”退出 Safari。等下次进入 Safari 的时候,上次退出时的网址会自动被打开。【实用】

推荐一个软件:CheatSheet

打开 CheatSheet 后,长按 command 键,会弹出当前应用程序的所有快捷键。我们还可以对这些快捷键进行保存。

📚 学习资源

🚪 传送

| 回首頁 |

Flume

Flume 简介

Apache Flume 是一个分布式,高可用的数据收集系统。它可以从不同的数据源收集数据,经过聚合后发送到存储系统中,通常用于日志数据的收集。Flume 分为 NG 和 OG (1.0 之前) 两个版本,NG 在 OG 的基础上进行了完全的重构,是目前使用最为广泛的版本。下面的介绍均以 NG 为基础。

Flume 架构

Agent component diagram

外部数据源以特定格式向 Flume 发送 events (事件),当 source 接收到 events 时,它将其存储到一个或多个 channelchannel 会一直保存 events 直到它被 sink 所消费。sink 的主要功能从 channel 中读取 events,并将其存入外部存储系统或转发到下一个 source,成功后再从 channel 中移除 events

Flume 基本概念

  • Event - Event 是 Flume NG 数据传输的基本单元。类似于 JMS 和消息系统中的消息。一个 Event 由标题和正文组成:前者是键/值映射,后者是任意字节数组。
  • Agent - 是一个独立的 (JVM) 进程,包含 SourceChannelSink 等组件。
    • Source - 数据收集组件,从外部数据源收集数据,并存储到 Channel 中。
    • Channel - Channel 是源和接收器之间的管道,用于临时存储数据。可以是内存或持久化的文件系统:
      • Memory Channel : 使用内存,优点是速度快,但数据可能会丢失 (如突然宕机);
      • File Channel : 使用持久化的文件系统,优点是能保证数据不丢失,但是速度慢。
    • Sink - Sink 的主要功能从 Channel 中读取 Event,并将其存入外部存储系统或将其转发到下一个 Source,成功后再从 Channel 中移除 Event

Flume 组件种类

Flume 中的每一个组件都提供了丰富的类型,适用于不同场景:

  • Source 类型 :内置了几十种类型,如 Avro SourceThrift SourceKafka SourceJMS Source
  • Sink 类型 :HDFS SinkHive SinkHBaseSinksAvro Sink 等;
  • Channel 类型 :Memory ChannelJDBC ChannelKafka ChannelFile Channel 等。

对于 Flume 的使用,除非有特别的需求,否则通过组合内置的各种类型的 Source,Sink 和 Channel 就能满足大多数的需求。在 Flume 官网 上对所有类型组件的配置参数均以表格的方式做了详尽的介绍,并附有配置样例;同时不同版本的参数可能略有所不同,所以使用时建议选取官网对应版本的 User Guide 作为主要参考资料。

Flume 架构模式

Flume 支持多种架构模式,分别介绍如下

multi-agent flow

Two agents communicating over Avro RPC

Flume 支持跨越多个 Agent 的数据传递,这要求前一个 Agent 的 Sink 和下一个 Agent 的 Source 都必须是 Avro 类型,Sink 指向 Source 所在主机名 (或 IP 地址) 和端口(详细配置见下文案例三)。

Consolidation

A fan-in flow using Avro RPC to consolidate events in one place

日志收集中常常存在大量的客户端(比如分布式 web 服务),Flume 支持使用多个 Agent 分别收集日志,然后通过一个或者多个 Agent 聚合后再存储到文件系统中。

Multiplexing the flow

A fan-out flow using a (multiplexing) channel selector

Flume 支持从一个 Source 向多个 Channel,也就是向多个 Sink 传递事件,这个操作称之为 Fan Out(扇出)。默认情况下 Fan Out 是向所有的 Channel 复制 Event,即所有 Channel 收到的数据都是相同的。同时 Flume 也支持在 Source 上自定义一个复用选择器 (multiplexing selector) 来实现自定义的路由规则。

Flume 配置格式

Flume 配置通常需要以下两个步骤:

(1)分别定义好 Agent 的 Sources,Sinks,Channels,然后将 Sources 和 Sinks 与通道进行绑定。需要注意的是一个 Source 可以配置多个 Channel,但一个 Sink 只能配置一个 Channel。基本格式如下:

1
2
3
4
5
6
7
8
9
<Agent>.sources = <Source>
<Agent>.sinks = <Sink>
<Agent>.channels = <Channel1> <Channel2>

# set channel for source
<Agent>.sources.<Source>.channels = <Channel1> <Channel2> ...

# set channel for sink
<Agent>.sinks.<Sink>.channel = <Channel1>

(2)分别定义 Source,Sink,Channel 的具体属性。基本格式如下:

1
2
3
4
5
6
7
<Agent>.sources.<Source>.<someProperty> = <someValue>

# properties for channels
<Agent>.channel.<Channel>.<someProperty> = <someValue>

# properties for sinks
<Agent>.sources.<Sink>.<someProperty> = <someValue>

Flume 使用案例

介绍几个 Flume 的使用案例:

  • 案例一:使用 Flume 监听文件内容变动,将新增加的内容输出到控制台。
  • 案例二:使用 Flume 监听指定目录,将目录下新增加的文件存储到 HDFS。
  • 案例三:使用 Avro 将本服务器收集到的日志数据发送到另外一台服务器。

案例一

需求: 监听文件内容变动,将新增加的内容输出到控制台。

实现: 主要使用 Exec Source 配合 tail 命令实现。

配置

新建配置文件 exec-memory-logger.properties, 其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#指定 agent 的 sources,sinks,channels
a1.sources = s1
a1.sinks = k1
a1.channels = c1

#配置 sources 属性
a1.sources.s1.type = exec
a1.sources.s1.command = tail -F /tmp/log.txt
a1.sources.s1.shell = /bin/bash -c

#将 sources 与 channels 进行绑定
a1.sources.s1.channels = c1

#配置 sink
a1.sinks.k1.type = logger

#将 sinks 与 channels 进行绑定
a1.sinks.k1.channel = c1

#配置 channel 类型
a1.channels.c1.type = memory

启动

1
2
3
4
5
flume-ng agent \
--conf conf \
--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/exec-memory-logger.properties \
--name a1 \
-Dflume.root.logger=INFO,console

测试

向文件中追加数据:

img

控制台的显示:

img

案例二

需求: 监听指定目录,将目录下新增加的文件存储到 HDFS。

实现:使用 Spooling Directory SourceHDFS Sink

配置

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
#指定 agent 的 sources,sinks,channels
a1.sources = s1
a1.sinks = k1
a1.channels = c1

#配置 sources 属性
a1.sources.s1.type =spooldir
a1.sources.s1.spoolDir =/tmp/logs
a1.sources.s1.basenameHeader = true
a1.sources.s1.basenameHeaderKey = fileName
#将 sources 与 channels 进行绑定
a1.sources.s1.channels =c1


#配置 sink
a1.sinks.k1.type = hdfs
a1.sinks.k1.hdfs.path = /flume/events/%y-%m-%d/%H/
a1.sinks.k1.hdfs.filePrefix = %{fileName}
#生成的文件类型,默认是 Sequencefile,可用 DataStream,则为普通文本
a1.sinks.k1.hdfs.fileType = DataStream
a1.sinks.k1.hdfs.useLocalTimeStamp = true
#将 sinks 与 channels 进行绑定
a1.sinks.k1.channel = c1

#配置 channel 类型
a1.channels.c1.type = memory

启动

1
2
3
4
flume-ng agent \
--conf conf \
--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/spooling-memory-hdfs.properties \
--name a1 -Dflume.root.logger=INFO,console

测试

拷贝任意文件到监听目录下,可以从日志看到文件上传到 HDFS 的路径:

1
# cp log.txt logs/

img

查看上传到 HDFS 上的文件内容与本地是否一致:

1
# hdfs dfs -cat /flume/events/19-04-09/13/log.txt.1554788567801

img

案例三

需求: 将本服务器收集到的数据发送到另外一台服务器。

实现:使用 avro sourcesavro Sink 实现。

配置日志收集 Flume

新建配置 netcat-memory-avro.properties,监听文件内容变化,然后将新的文件内容通过 avro sink 发送到 hadoop001 这台服务器的 8888 端口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#指定 agent 的 sources,sinks,channels
a1.sources = s1
a1.sinks = k1
a1.channels = c1

#配置 sources 属性
a1.sources.s1.type = exec
a1.sources.s1.command = tail -F /tmp/log.txt
a1.sources.s1.shell = /bin/bash -c
a1.sources.s1.channels = c1

#配置 sink
a1.sinks.k1.type = avro
a1.sinks.k1.hostname = hadoop001
a1.sinks.k1.port = 8888
a1.sinks.k1.batch-size = 1
a1.sinks.k1.channel = c1

#配置 channel 类型
a1.channels.c1.type = memory
a1.channels.c1.capacity = 1000
a1.channels.c1.transactionCapacity = 100

配置日志聚合 Flume

使用 avro source 监听 hadoop001 服务器的 8888 端口,将获取到内容输出到控制台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#指定 agent 的 sources,sinks,channels
a2.sources = s2
a2.sinks = k2
a2.channels = c2

#配置 sources 属性
a2.sources.s2.type = avro
a2.sources.s2.bind = hadoop001
a2.sources.s2.port = 8888

#将 sources 与 channels 进行绑定
a2.sources.s2.channels = c2

#配置 sink
a2.sinks.k2.type = logger

#将 sinks 与 channels 进行绑定
a2.sinks.k2.channel = c2

#配置 channel 类型
a2.channels.c2.type = memory
a2.channels.c2.capacity = 1000
a2.channels.c2.transactionCapacity = 100

启动

启动日志聚集 Flume:

1
2
3
4
flume-ng agent \
--conf conf \
--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/avro-memory-logger.properties \
--name a2 -Dflume.root.logger=INFO,console

在启动日志收集 Flume:

1
2
3
4
flume-ng agent \
--conf conf \
--conf-file /usr/app/apache-flume-1.6.0-cdh5.15.2-bin/examples/netcat-memory-avro.properties \
--name a1 -Dflume.root.logger=INFO,console

这里建议按以上顺序启动,原因是 avro.source 会先与端口进行绑定,这样 avro sink 连接时才不会报无法连接的异常。但是即使不按顺序启动也是没关系的,sink 会一直重试,直至建立好连接。

img

测试

向文件 tmp/log.txt 中追加内容:

img

可以看到已经从 8888 端口监听到内容,并成功输出到控制台:

img

参考资料

Spark

Spark 简介

Spark 概念

  • 大规模分布式通用计算引擎
    • Spark Core:核心计算框架
    • Spark SQL:结构化数据查询
    • Spark Streaming:实时流处理
    • Spark MLib:机器学习
    • Spark GraphX:图计算
  • 具有高吞吐、低延时、通用易扩展、高容错等特点
  • 采用 Scala 语言开发
  • 提供多种运行模式

Spark 特点

  • 计算高效
    • 利用内存计算、Cache 缓存机制,支持迭代计算和数据共享,减少数据读取的 IO 开销
    • 利用 DAG 引擎,减少中间计算结果写入 HDFS 的开销
    • 利用多线程池模型,减少任务启动开销,避免 Shuffle 中不必要的排序和磁盘 IO 操作
  • 通用易用
    • 适用于批处理、流处理、交互式计算、机器学习算法等场景
    • 提供了丰富的开发 API,支持 Scala、Java、Python、R 等
  • 运行模式多样
    • Local 模式
    • Standalone 模式
    • YARN/Mesos 模式
  • 计算高效
    • 利用内存计算、Cache 缓存机制,支持迭代计算和数据共享,减少数据读取的 IO 开销
    • 利用 DAG 引擎,减少中间计算结果写入 HDFS 的开销
    • 利用多线程池模型,减少任务启动开销,避免 Shuffle 中不必要的排序和磁盘 IO 操作
  • 通用易用
    • 适用于批处理、流处理、交互式计算、机器学习等场景
    • 提供了丰富的开发 API,支持 Scala、Java、Python、R 等

Spark 原理

编程模型

RDD

  • 弹性分布式数据集(Resilient Distributed Datesets)
    • 分布在集群中的只读对象集合
    • 由多个 Partition 组成
    • 通过转换操作构造
    • 失效后自动重构(弹性)
    • 存储在内存或磁盘中
  • Spark 基于 RDD 进行计算

RDD 操作(Operator)

  • Transformation(转换)
    • 将 Scala 集合或 Hadoop 输入数据构造成一个新 RDD
    • 通过已有的 RDD 产生新 RDD
    • 惰性执行:只记录转换关系,不触发计算
    • 例如:map、filter、flatmap、union、distinct、sortbykey
  • Action(动作)
    • 通过 RDD 计算得到一个值或一组值
    • 真正触发计算
    • 例如:first、count、collect、foreach、saveAsTextFile

RDD 依赖(Dependency)

  • 窄依赖(Narrow Dependency)
    • 父 RDD 中的分区最多只能被一个子 RDD 的一个分区使用
    • 子 RDD 如果有部分分区数据丢失或损坏,只需从对应的父 RDD 重新计算恢复
    • 例如:map、filter、union
  • 宽依赖(Shuffle/Wide Dependency )
    • 子 RDD 分区依赖父 RDD 的所有分区
    • 子 RDD 如果部分或全部分区数据丢失或损坏,必须从所有父 RDD 分区重新计算
    • 相对于窄依赖,宽依赖付出的代价要高很多,尽量避免使用
    • 例如:groupByKey、reduceByKey、sortByKey

YARN

YARN 简介

Apache YARN (Yet Another Resource Negotiator) 是 hadoop 2.0 引入的集群资源管理系统。用户可以将各种服务框架部署在 YARN 上,由 YARN 进行统一地管理和资源分配。

YARN 架构

ResourceManager

ResourceManager 通常在独立的机器上以后台进程的形式运行,它是整个集群资源的主要协调者和管理者。ResourceManager 负责给用户提交的所有应用程序分配资源,它根据应用程序优先级、队列容量、ACLs、数据位置等信息,做出决策,然后以共享的、安全的、多租户的方式制定分配策略,调度集群资源。

NodeManager

NodeManager 是 YARN 集群中的每个具体节点的管理者。主要负责该节点内所有容器的生命周期的管理,监视资源和跟踪节点健康。具体如下:

  • 启动时向 ResourceManager 注册并定时发送心跳消息,等待 ResourceManager 的指令;
  • 维护 Container 的生命周期,监控 Container 的资源使用情况;
  • 管理任务运行时的相关依赖,根据 ApplicationMaster 的需要,在启动 Container 之前将需要的程序及其依赖拷贝到本地。

ApplicationMaster

在用户提交一个应用程序时,YARN 会启动一个轻量级的进程 ApplicationMasterApplicationMaster 负责协调来自 ResourceManager 的资源,并通过 NodeManager 监视容器内资源的使用情况,同时还负责任务的监控与容错。具体如下:

  • 根据应用的运行状态来决定动态计算资源需求;
  • ResourceManager 申请资源,监控申请的资源的使用情况;
  • 跟踪任务状态和进度,报告资源的使用情况和应用的进度信息;
  • 负责任务的容错。

Container

Container 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当 AM 向 RM 申请资源时,RM 为 AM 返回的资源是用 Container 表示的。YARN 会为每个任务分配一个 Container,该任务只能使用该 Container 中描述的资源。ApplicationMaster 可在 Container 内运行任何类型的任务。例如,MapReduce ApplicationMaster 请求一个容器来启动 map 或 reduce 任务,而 Giraph ApplicationMaster 请求一个容器来运行 Giraph 任务。

YARN 工作原理

  1. Client 提交作业到 YARN 上;

  2. Resource Manager 选择一个 Node Manager,启动一个 Container 并运行 Application Master 实例;

  3. Application Master 根据实际需要向 Resource Manager 请求更多的 Container 资源(如果作业很小,应用管理器会选择在其自己的 JVM 中运行任务);

  4. Application Master 通过获取到的 Container 资源执行分布式计算。

作业提交

client 调用 job.waitForCompletion 方法,向整个集群提交 MapReduce 作业 (第 1 步) 。新的作业 ID(应用 ID) 由资源管理器分配 (第 2 步)。作业的 client 核实作业的输出,计算输入的 split, 将作业的资源 (包括 Jar 包,配置文件,split 信息) 拷贝给 HDFS(第 3 步)。 最后,通过调用资源管理器的 submitApplication() 来提交作业 (第 4 步)。

作业初始化

当资源管理器收到 submitApplciation() 的请求时,就将该请求发给调度器 (scheduler), 调度器分配 container, 然后资源管理器在该 container 内启动应用管理器进程,由节点管理器监控 (第 5 步)。

MapReduce 作业的应用管理器是一个主类为 MRAppMaster 的 Java 应用,其通过创造一些 bookkeeping 对象来监控作业的进度,得到任务的进度和完成报告 (第 6 步)。然后其通过分布式文件系统得到由客户端计算好的输入 split(第 7 步),然后为每个输入 split 创建一个 map 任务,根据 mapreduce.job.reduces 创建 reduce 任务对象。

任务分配

如果作业很小,应用管理器会选择在其自己的 JVM 中运行任务。

如果不是小作业,那么应用管理器向资源管理器请求 container 来运行所有的 map 和 reduce 任务 (第 8 步)。这些请求是通过心跳来传输的,包括每个 map 任务的数据位置,比如存放输入 split 的主机名和机架 (rack),调度器利用这些信息来调度任务,尽量将任务分配给存储数据的节点,或者分配给和存放输入 split 的节点相同机架的节点。

任务运行

当一个任务由资源管理器的调度器分配给一个 container 后,应用管理器通过联系节点管理器来启动 container(第 9 步)。任务由一个主类为 YarnChild 的 Java 应用执行, 在运行任务之前首先本地化任务需要的资源,比如作业配置,JAR 文件,以及分布式缓存的所有文件 (第 10 步。 最后,运行 map 或 reduce 任务 (第 11 步)。

YarnChild 运行在一个专用的 JVM 中,但是 YARN 不支持 JVM 重用。

进度和状态更新

YARN 中的任务将其进度和状态 (包括 counter) 返回给应用管理器,客户端每秒 (通 mapreduce.client.progressmonitor.pollinterval 设置) 向应用管理器请求进度更新,展示给用户。

作业完成

除了向应用管理器请求作业进度外,客户端每 5 分钟都会通过调用 waitForCompletion() 来检查作业是否完成,时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业完成之后,应用管理器和 container 会清理工作状态, OutputCommiter 的作业清理方法也会被调用。作业的信息会被作业历史服务器存储以备之后用户核查。

提交作业到 YARN 上运行

这里以提交 Hadoop Examples 中计算 Pi 的 MApReduce 程序为例,相关 Jar 包在 Hadoop 安装目录的 share/hadoop/mapreduce 目录下:

1
2
# 提交格式:hadoop jar jar 包路径 主类名称 主类参数
# hadoop jar hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3

参考资料

大数据简介

简介

什么是大数据

大数据是指超出传统数据库工具收集、存储、管理和分析能力的数据集。与此同时,及时采集、存储、聚合、管理数据,以及对数据深度分析的新技术和新能力,正在快速增长,就像预测计算芯片增长速度的摩尔定律一样。

  • Volume - 数据规模巨大
  • Velocity - 生成和处理速度极快
  • Variety - 数据规模巨大
  • Value - 生成和处理速度极快

应用场景

基于大数据的数据仓库

基于大数据的实时流处理

Hadoop 编年史

时间 事件
2003.01 Google 发表了 Google File System 论文
2004.01 Google 发表了 MapReduce 论文
2006.02 Apache Hadoop 项目正式启动,并支持 MapReduce 和 HDFS 独立发展
2006.11 Google 发表了 Bigtable 论文
2008.01 Hadoop 成为 Apache 顶级项目
2009.03 Cloudera 推出世界上首个 Hadoop 发行版——CDH,并完全开放源码
2012.03 HDFS NameNode HA 加入 Hadoop 主版本
2014.02 Spark 代替 MapReduce 成为 Hadoop 的缺省计算引擎,并成为 Apache 顶级项目

技术体系

HDFS

概念

  • Hadoop 分布式文件系统(Hadoop Distributed File System)
  • 在开源大数据技术体系中,地位无可替代

特点

  • 高容错:数据多副本,副本丢失后自动恢复
  • 高可用:NameNode HA,安全模式
  • 高扩展:10K 节点规模
  • 简单一致性模型:一次写入多次读取,支持追加,不允许修改
  • 流式数据访问:批量读而非随机读,关注吞吐量而非时间
  • 大规模数据集:典型文件大小 GB~TB 级,百万以上文件数量, PB 以上数据规模
  • 构建成本低且安全可靠:运行在大量的廉价商用机器上,硬件错误是常态,提供容错机制

MapReduce

概念

  • 面向批处理的分布式计算框架
  • 编程模型:将 MapReduce 程序分为 Map、Reduce 两个阶段

核心思想

  • 分而治之,分布式计算
  • 移动计算,而非移动数据

特点

  • 高容错:任务失败,自动调度到其他节点重新执行
  • 高扩展:计算能力随着节点数增加,近似线性递增
  • 适用于海量数据的离线批处理
  • 降低了分布式编程的门槛

Spark

高性能分布式通用计算引擎

  • Spark Core - 基础计算框架(批处理、交互式分析)
  • Spark SQL - SQL 引擎(海量结构化数据的高性能查询)
  • Spark Streaming - 实时流处理(微批)
  • Spark MLlib - 机器学习
  • Spark GraphX - 图计算

采用 Scala 语言开发

特点

  • 计算高效 - 内存计算、Cache 缓存机制、DAG 引擎、多线程池模型
  • 通用易用 - 适用于批处理、交互式计算、流处理、机器学习、图计算等多种场景
  • 运行模式多样 - Local、Standalone、YARN/Mesos

YARN

概念

  • Yet Another Resource Negotiator,另一种资源管理器
  • 为了解决 Hadoop 1.x 中 MapReduce 的先天缺陷
  • 分布式通用资源管理系统
  • 负责集群资源的统一管理
  • 从 Hadoop 2.x 开始,YARN 成为 Hadoop 的核心组件

特点

  • 专注于资源管理和作业调度
  • 通用 - 适用各种计算框架,如 - MapReduce、Spark
  • 高可用 - ResourceManager 高可用、HDFS 高可用
  • 高扩展

Hive

概念

  • Hadoop 数据仓库 - 企业决策支持
  • SQL 引擎 - 对海量结构化数据进行高性能的 SQL 查询
  • 采用 HDFS 或 HBase 为数据存储
  • 采用 MapReduce 或 Spark 为计算框架

特点

  • 提供类 SQL 查询语言
  • 支持命令行或 JDBC/ODBC
  • 提供灵活的扩展性
  • 提供复杂数据类型、扩展函数、脚本等

HBase

概念

  • Hadoop Database
  • Google BigTable 的开源实现
  • 分布式 NoSQL 数据库
  • 列式存储 - 主要用于半结构化、非结构化数据
  • 采用 HDFS 为文件存储系统

特点

  • 高性能 - 支持高并发写入和查询
  • 高可用 - HDFS 高可用、Region 高可用
  • 高扩展 - 数据自动切分和分布,可动态扩容,无需停机
  • 海量存储 - 单表可容纳数十亿行,上百万列

ElasticSearch

  • 开源的分布式全文检索引擎
  • 基于 Lucene 实现全文数据的快速存储、搜索和分析
  • 处理大规模数据 - PB 级以上
  • 具有较强的扩展性,集群规模可达上百台
  • 首选的分布式搜索引擎

术语

数据仓库(Data Warehouse) - 数据仓库,是为企业所有级别的决策制定过程,提供所有类型数据支持的战略集合。它是单个数据存储,出于分析性报告和决策支持目的而创建。 为需要业务智能的企业,提供指导业务流程改进、监视时间、成本、质量以及控制。

资源

HBase 运维

配置文件

  • backup-masters - 默认情况下不存在。列出主服务器应在其上启动备份主进程的主机,每行一个主机。
  • hadoop-metrics2-hbase.properties - 用于连接 HBase Hadoop 的 Metrics2 框架。
  • hbase-env.cmd and hbase-env.sh - 用于 Windows 和 Linux / Unix 环境的脚本,用于设置 HBase 的工作环境,包括 Java,Java 选项和其他环境变量的位置。
  • hbase-policy.xml - RPC 服务器用于对客户端请求进行授权决策的默认策略配置文件。仅在启用 HBase 安全性时使用。
  • hbase-site.xml - 主要的 HBase 配置文件。此文件指定覆盖 HBase 默认配置的配置选项。您可以在 docs / hbase-default.xml 中查看(但不要编辑)默认配置文件。您还可以在 HBase Web UI 的 HBase 配置选项卡中查看群集的整个有效配置(默认值和覆盖)。
  • log4j.properties - log4j 日志配置。
  • regionservers - 包含应在 HBase 集群中运行 RegionServer 的主机列表。默认情况下,此文件包含单个条目 localhost。它应包含主机名或 IP 地址列表,每行一个,并且如果群集中的每个节点将在其 localhost 接口上运行 RegionServer,则应仅包含 localhost。

环境要求

  • Java
    • HBase 2.0+ 要求 JDK8+
    • HBase 1.2+ 要求 JDK7+
  • SSH - 环境要支持 SSH
  • DNS - 环境中要在 hosts 配置本机 hostname 和本机 IP
  • NTP - HBase 集群的时间要同步,可以配置统一的 NTP
  • 平台 - 生产环境不推荐部署在 Windows 系统中
  • Hadoop - 依赖 Hadoop 配套版本
  • Zookeeper - 依赖 Zookeeper 配套版本

运行模式

单点

hbase-site.xml 配置如下:

1
2
3
4
5
6
7
8
9
10
<configuration>
<property>
<name>hbase.rootdir</name>
<value>hdfs://namenode.example.org:8020/hbase</value>
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>false</value>
</property>
</configuration>

分布式

hbase-site.xm 配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<configuration>
<property>
<name>hbase.rootdir</name>
<value>hdfs://namenode.example.org:8020/hbase</value>
</property>
<property>
<name>hbase.cluster.distributed</name>
<value>true</value>
</property>
<property>
<name>hbase.zookeeper.quorum</name>
<value>node-a.example.com,node-b.example.com,node-c.example.com</value>
</property>
</configuration>

引用和引申

扩展阅读