Dunwu Blog

大道至简,知易行难

MongoDB 建模

MongoDB 的数据模式是一种灵活模式,关系型数据库要求你在插入数据之前必须先定义好一个表的模式结构,而 MongoDB 的集合则并不限制 document 结构。这种灵活性让对象和数据库文档之间的映射变得很容易。即使数据记录之间有很大的变化,每个文档也可以很好的映射到各条不同的记录。 当然在实际使用中,同一个集合中的文档往往都有一个比较类似的结构。

数据模型设计中最具挑战性的是在应用程序需求,数据库引擎性能要求和数据读写模式之间做权衡考量。当设计数据模型的时候,一定要考虑应用程序对数据的使用模式(如查询,更新和处理)以及数据本身的天然结构。

MongoDB 数据建模入门

参考:https://docs.mongodb.com/guides/server/introduction/#what-you-ll-need

(一)定义数据集

当需要建立数据存储时,首先应该思考以下问题:需要存储哪些数据?这些字段之间如何关联?

这是一个数据建模的过程。目标是将业务需求抽象为逻辑模型

假设这样一个场景:我们需要建立数据库以跟踪物料及其数量,大小,标签和等级。

如果是存储在 RDBMS,可能以下的数据表:

name quantity size status tags rating
journal 25 14x21,cm A brown, lined 9
notebook 50 8.5x11,in A college-ruled,perforated 8
paper 100 8.5x11,in D watercolor 10
planner 75 22.85x30,cm D 2019 10
postcard 45 10x,cm D double-sided,white 2

(二)思考 JSON 结构

从上例中可以看出,表似乎是存储数据的好地方,但该数据集中的字段需要多个值,如果在单个列中建模,则不容易搜索或显示(对于 例如–大小和标签)。

在 SQL 数据库中,您可以通过创建关系表来解决此问题。

在 MongoDB 中,数据存储为文档(document)。 这些文档以 JSON(JavaScript 对象表示法)格式存储在 MongoDB 中。 JSON 文档支持嵌入式字段,因此相关数据和数据列表可以与文档一起存储,而不是与外部表一起存储。

JSON 格式为键/值对。 在 JSON 文档中,字段名和值用冒号分隔,字段名和值对用逗号分隔,并且字段集封装在“大括号”({})中。

如果要开始对上面的行之一进行建模,例如此行:

name quantity size status tags rating
notebook 50 8.5x11,in A college-ruled,perforated 8

您可以从 name 和 quantity 字段开始。 在 JSON 中,这些字段如下所示:

1
{ "name": "notebook", "qty": 50 }

(三)确定哪些字段作为嵌入式数据

接下来,需要确定哪些字段可能需要多个值。可以考虑将这些字段作为嵌入式文档或嵌入式文档中的 列表/数组 对象。

例如,在上面的示例中,size 可能包含三个字段:

1
{ "h": 11, "w": 8.5, "uom": "in" }

And some items have multiple ratings, so ratings might be represented as a list of documents containing the field scores:

1
[{ "score": 8 }, { "score": 9 }]

And you might need to handle multiple tags per item. So you might store them in a list too.

1
["college-ruled", "perforated"]

Finally, a JSON document that stores an inventory item might look like this:

1
2
3
4
5
6
7
8
{
"name": "notebook",
"qty": 50,
"rating": [{ "score": 8 }, { "score": 9 }],
"size": { "height": 11, "width": 8.5, "unit": "in" },
"status": "A",
"tags": ["college-ruled", "perforated"]
}

This looks very different from the tabular data structure you started with in Step 1.

数据模型简介

数据建模中的关键挑战是平衡应用程序的需求、数据库引擎的性能以及数据检索模式。 在设计数据模型时,始终需要考虑数据的应用程序使用情况(即数据的查询,更新和处理)以及数据本身的固有结构。

灵活的 Schema

在关系型数据库中,必须在插入数据之前确定并声明表的结构。而 MongoDB 的 collection 默认情况下不需要其文档具有相同的架构。也就是说:

同一个 collection 中的 document 不需要具有相同的 field 集,并且 field 的数据类型可以在集合中的不同文档之间有所不同。

要更改 collection 中的 document 结构,例如添加新 field,删除现有 field 或将 field 值更改为新类型,只需要将文档更新为新结构即可。

这种灵活性有助于将 document 映射到实体或对象。每个 document 都可以匹配所表示实体的数据字段,即使该文档与集合中的其他文档有很大的不同。但是,实际上,集合中的文档具有相似的结构,并且您可以在更新和插入操作期间对 collection 强制执行 document 校验规则。

Document 结构

嵌入式数据模型

嵌入式 document 通过将相关数据存储在单个 document 结构中来捕获数据之间的关系。 MongoDB document 可以将 document 结构嵌入到另一个 document 中的字段或数组中。这些非规范化的数据模型允许应用程序在单个数据库操作中检索和操纵相关数据。

img

对于 MongoDB 中的很多场景,非规范化数据模型都是最佳的。

嵌入式 document 有大小限制:必须小于 16 MB。

如果是较大的二进制数据,可以考虑 GridFS

引用式数据模型

引用通过包含从一个 document 到另一个 document 的链接或引用来存储数据之间的关系。 应用程序可以解析这些引用以访问相关数据。 广义上讲,这些是规范化的数据模型。

img

通常,在以下场景使用引用式的数据模型:

  • 嵌入时会导致数据重复,但无法提供足够的读取性能优势,无法胜过重复的含义。
  • 代表更复杂的多对多关系。
  • 为大规模分层数据集建模。

为了 join collection,MongoDB 支持聚合 stage:

MongoDB 还提供了引用来支持跨集合 join 数据:

原子写操作

单 document 的原子性

在 MongoDB 中,针对单个 document 的写操作是原子性的,即使该 document 中嵌入了多个子 document。 具有嵌入数据的非规范化数据模型将所有相关数据合并在一个 document 中,而不是在多个 document 和 collection 中进行规范化。 该数据模型有助于原子操作。 当单个写入操作(例如 db.collection.updateMany())修改多个 document 时,每个 document 的独立修改是原子的,但整个操作不是原子的。

多 document 事务

对于需要对多个 document(在单个或多个集合中)进行读写原子性的情况,MongoDB 支持多 document 事务。

  • 在版本 4.0 中,MongoDB 在副本集上支持多 document 事务。
  • 在版本 4.2 中,MongoDB 引入了分布式事务,它增加了对分片群集上多 document 事务的支持,并合并了对副本集上多 document 事务的现有支持。

在大多数情况下,多 document 事务会比单 document 的写入产生更高的性能消耗,并且多 document 事务的可用性不能替代高效的结构设计。 在许多情况下,非规范化数据模型(嵌入式 document 和数组)仍是最佳选择。 也就是说,合理的数据建模,将最大程度地减少对多 document 事务的需求。

数据使用和性能

在设计数据模型时,请考虑应用程序将如何使用您的数据库。 例如,如果您的应用程序仅使用最近插入的 document,请考虑使用上限集合。 或者,如果您的应用程序主要是对 collection 的读取操作,则添加索引以提高性能。

Schema 校验

指定校验规则

如果创建新 collection 时要指定校验规则,需要在使用 db.createCollection() 时指定 validator 选项。

如果要将 document 校验添加到现有 collection 中,需要使用带有 validator 选项的 collMod 命令。

MongoDB 还提供以下相关选项:

  • validationLevel 选项(用于确定 MongoDB 在更新过程中,对现有 document 应用校验规则的严格程度)
  • validationAction 选项(用于确定 MongoDB 发现违反校验规则的 document 时,是选择报错并拒绝,还是接受数据但在日志中告警)。

JSON Schema

从 3.6 版本开始,MongoDB 开始支持 JSON Schema 校验。

可以通过在 validator 表达式中使用 $jsonSchema 操作来指定 JSON Schema 校验。

【示例】

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
db.createCollection('students', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['name', 'year', 'major', 'address'],
properties: {
name: {
bsonType: 'string',
description: 'must be a string and is required'
},
year: {
bsonType: 'int',
minimum: 2017,
maximum: 3017,
description: 'must be an integer in [ 2017, 3017 ] and is required'
},
major: {
enum: ['Math', 'English', 'Computer Science', 'History', null],
description: 'can only be one of the enum values and is required'
},
gpa: {
bsonType: ['double'],
description: 'must be a double if the field exists'
},
address: {
bsonType: 'object',
required: ['city'],
properties: {
street: {
bsonType: 'string',
description: 'must be a string if the field exists'
},
city: {
bsonType: 'string',
description: 'must be a string and is required'
}
}
}
}
}
}
})

其它查询表达式

除了使用 $jsonSchema 查询运算符的 JSON Schema 校验外,MongoDB 还支持其它查询运算符的校验,但以下情况除外:

【示例】查询表达式中指定校验规则

1
2
3
4
5
6
7
8
9
db.createCollection('contacts', {
validator: {
$or: [
{ phone: { $type: 'string' } },
{ email: { $regex: /@mongodb\.com$/ } },
{ status: { $in: ['Unknown', 'Incomplete'] } }
]
}
})

行为

校验发生在更新和插入期间。添加校验规则到 collection 时,不会对现有的 document 进行校验,除非发生修改操作。

现有的 document

validationLevel 选项确定 MongoDB 进行规则校验时执行的操作:

  • 如果 validationLevel 是 strict(严格级别。这是 MongoDB 默认级别),则 MongoDB 将校验规则应用于所有插入和更新。
  • 如果 validationLevel 是 moderate(中等级别),则 MongoDB 只对已满足校验条件的现有文档的插入和更新操作进行校验;对不符合校验标准的现有文档的更新操作不进行校验。

【示例】

下面是一个正常的插入操作:

1
2
3
4
5
6
7
8
9
10
db.contacts.insert([
{
_id: 1,
name: 'Anne',
phone: '+1 555 123 456',
city: 'London',
status: 'Complete'
},
{ _id: 2, name: 'Ivan', city: 'Vancouver' }
])

在 collection 上配置一个校验规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
db.runCommand({
collMod: 'contacts',
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['phone', 'name'],
properties: {
phone: {
bsonType: 'string',
description: 'must be a string and is required'
},
name: {
bsonType: 'string',
description: 'must be a string and is required'
}
}
}
},
validationLevel: 'moderate'
})

contacts collection 现在添加了含中等级别(moderate) validationLevel 的 validator

  • 如果尝试更新 _id为 1 的文档,则 MongoDB 将应用校验规则,因为现有文档符合条件。

  • 相反,MongoDB 不会将校验 _id 为 2 的文档,因为它不符合校验规则。

如果要完全禁用校验,可以将 validationLevel 置为 off

接受或拒绝无效的 document

  • 如果 validationAction 是 Error(默认),则 MongoDB 拒绝任何违反校验规则的插入或更新。
  • 如果 validationAction 是 Warn,MongoDB 会记录所有的违规,但允许进行插入或更新。

【示例】

创建集合时,配置 validationAction 为 warn。

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
db.createCollection('contacts2', {
validator: {
$jsonSchema: {
bsonType: 'object',
required: ['phone'],
properties: {
phone: {
bsonType: 'string',
description: 'must be a string and is required'
},
email: {
bsonType: 'string',
pattern: '@mongodb.com$',
description:
'must be a string and match the regular expression pattern'
},
status: {
enum: ['Unknown', 'Incomplete'],
description: 'can only be one of the enum values'
}
}
}
},
validationAction: 'warn'
})

尝试插入一条违规记录

1
2
> db.contacts2.insert( { name: "Amanda", status: "Updated" } )
WriteResult({ "nInserted" : 1 })

MongoDB 允许这条操作执行,但是服务器会记录下告警信息。

1
{"t":{"$date":"2020-09-11T16:35:57.754+08:00"},"s":"W",  "c":"STORAGE",  "id":20294,   "ctx":"conn14","msg":"Document would fail validation","attr":{"namespace":"test.contacts2","document":{"_id":{"$oid":"5f5b36ed8ea53d62a0b51c4e"},"name":"Amanda","status":"Updated"}}}

限制

不能在 adminlocalconfig 这几个特殊的数据库中指定校验规则。

不能在 system.* collection 中指定校验。

参考资料

MongoDB 运维

MongoDB 安装

Windows

(1)下载并解压到本地

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

(2)创建数据目录

MongoDB 将数据目录存储在 db 目录下。但是这个数据目录不会主动创建,我们在安装完成后需要创建它。

例如:D:\Tools\Server\mongodb\mongodb-4.4.0\data\db

(3)运行 MongoDB 服务

1
mongod --dbpath D:\Tools\Server\mongodb\mongodb-4.4.0\data\db

(4)客户端连接 MongoDB

可以在命令窗口中运行 mongo.exe 命令即可连接上 MongoDB

(5)配置 MongoDB 服务

Linux

(1)使用安装包安装

安装前我们需要安装各个 Linux 平台依赖包。

Red Hat/CentOS:

1
sudo yum install libcurl openssl

Ubuntu 18.04 LTS (“Bionic”)/Debian 10 “Buster”:

1
sudo apt-get install libcurl4 openssl

Ubuntu 16.04 LTS (“Xenial”)/Debian 9 “Stretch”:

1
sudo apt-get install libcurl3 openssl

(2)创建数据目录

默认情况下 MongoDB 启动后会初始化以下两个目录:

  • 数据存储目录:/var/lib/mongodb
  • 日志文件目录:/var/log/mongodb

我们在启动前可以先创建这两个目录并设置当前用户有读写权限:

1
2
3
4
sudo mkdir -p /var/lib/mongo
sudo mkdir -p /var/log/mongodb
sudo chown `whoami` /var/lib/mongo # 设置权限
sudo chown `whoami` /var/log/mongodb # 设置权限

(3)运行 MongoDB 服务

1
mongod --dbpath /var/lib/mongo --logpath /var/log/mongodb/mongod.log --fork

打开 /var/log/mongodb/mongod.log 文件看到以下信息,说明启动成功。

1
2
3
4
# tail -10f /var/log/mongodb/mongod.log
2020-07-09T12:20:17.391+0800 I NETWORK [listener] Listening on /tmp/mongodb-27017.sock
2020-07-09T12:20:17.392+0800 I NETWORK [listener] Listening on 127.0.0.1
2020-07-09T12:20:17.392+0800 I NETWORK [listener] waiting for connections on port 27017

(4)客户端连接 MongoDB

1
2
cd /usr/local/mongodb4/bin
./mongo

Linux 安装脚本

设置用户名、密码

1
2
3
4
5
6
7
8
9
10
11
12
13
> use admin
switched to db admin
> db.createUser({"user":"root","pwd":"root","roles":[{"role":"userAdminAnyDatabase","db":"admin"}]})
Successfully added user: {
"user" : "root",
"roles" : [
{
"role" : "userAdminAnyDatabase",
"db" : "admin"
}
]
}
>

备份和恢复

数据备份

在 Mongodb 中,使用 mongodump 命令来备份 MongoDB 数据。该命令可以导出所有数据到指定目录中。

mongodump 命令可以通过参数指定导出的数据量级转存的服务器。

mongodump 命令语法如下:

1
mongodump -h dbhost -d dbname -o dbdirectory
  • -h:MongDB 所在服务器地址,例如:127.0.0.1,当然也可以指定端口号:127.0.0.1:27017

  • -d:需要备份的数据库实例,例如:test

  • -o:备份的数据存放位置,例如:c:\data\dump,当然该目录需要提前建立,在备份完成后,系统自动在 dump 目录下建立一个 test 目录,这个目录里面存放该数据库实例的备份数据。

mongodump 命令可选参数列表如下所示:

语法 描述 实例
mongodump –host HOST_NAME –port PORT_NUMBER 该命令将备份所有 MongoDB 数据 mongodump –host runoob.com –port 27017
mongodump –dbpath DB_PATH –out BACKUP_DIRECTORY mongodump –dbpath /data/db/ –out /data/backup/
mongodump –collection COLLECTION –db DB_NAME 该命令将备份指定数据库的集合。 mongodump –collection mycol –db test

【示例】备份全量数据

1
2
3
4
5
6
7
8
$ mongodump -h 127.0.0.1 --port 27017 -o test2
...
2020-09-11T11:55:58.086+0800 done dumping test.company (18801 documents)
2020-09-11T11:56:00.725+0800 [#############...........] test.people 559101/1000000 (55.9%)
2020-09-11T11:56:03.725+0800 [###################.....] test.people 829496/1000000 (82.9%)
2020-09-11T11:56:06.725+0800 [#####################...] test.people 884614/1000000 (88.5%)
2020-09-11T11:56:08.088+0800 [########################] test.people 1000000/1000000 (100.0%)
2020-09-11T11:56:08.350+0800 done dumping test.people (1000000 documents)

【示例】备份指定数据库

1
mongodump -h 127.0.0.1 --port 27017 -d admin -o test3

数据恢复

mongodb 使用 mongorestore 命令来恢复备份的数据。

mongorestore 命令语法如下:

1
> mongorestore -h <hostname><:port> -d dbname <path>
  • --host <:port>, -h <:port>:MongoDB 所在服务器地址,默认为: localhost:27017

  • --db , -d :需要恢复的数据库实例,例如:test,当然这个名称也可以和备份时候的不一样,比如 test2

  • --drop:恢复的时候,先删除当前数据,然后恢复备份的数据。就是说,恢复后,备份后添加修改的数据都会被删除,慎用哦!

  • <path>:mongorestore 最后的一个参数,设置备份数据所在位置,例如:c:\data\dump\test。你不能同时指定 <path>--dir 选项,--dir 也可以设置备份目录。

  • --dir:指定备份的目录。你不能同时指定 <path>--dir 选项。

【示例】

1
2
3
4
5
6
7
8
9
10
11
12
13
$ mongorestore -h 127.0.0.1 --port 27017 -d test --dir test --drop
...
2020-09-11T11:46:16.053+0800 finished restoring test.tweets (966 documents, 0 failures)
2020-09-11T11:46:18.256+0800 [###.....................] test.people 164MB/1.03GB (15.6%)
2020-09-11T11:46:21.255+0800 [########................] test.people 364MB/1.03GB (34.6%)
2020-09-11T11:46:24.256+0800 [############............] test.people 558MB/1.03GB (53.0%)
2020-09-11T11:46:27.255+0800 [###############.........] test.people 700MB/1.03GB (66.5%)
2020-09-11T11:46:30.257+0800 [###################.....] test.people 846MB/1.03GB (80.3%)
2020-09-11T11:46:33.255+0800 [######################..] test.people 990MB/1.03GB (94.0%)
2020-09-11T11:46:34.542+0800 [########################] test.people 1.03GB/1.03GB (100.0%)
2020-09-11T11:46:34.543+0800 no indexes to restore
2020-09-11T11:46:34.543+0800 finished restoring test.people (1000000 documents, 0 failures)
2020-09-11T11:46:34.544+0800 1000966 document(s) restored successfully. 0 document(s) failed to restore.

导入导出

mongoimportmongoexport 并不能可靠地保存所有的富文本 BSON 数据类型,因为 JSON 仅能代表一种 BSON 支持的子集类型。因此,数据用这些工具导出导入或许会丢失一些精确程度。

导入操作

在 MongoDB 中,使用 mongoimport 来导入数据。 默认情况下,mongoimport 会将数据导入到本地主机端口 27017 上的 MongoDB 实例中。要将数据导入在其他主机或端口上运行的 MongoDB 实例中,请通过包含 --host--port 选项来指定主机名或端口。 使用 --drop 选项删除集合(如果已经存在)。 这样可以确保该集合仅包含您要导入的数据。

语法格式:

1
mongoimport -h IP --port 端口 -u 用户名 -p 密码 -d 数据库 -c 表名 --type 类型 --headerline --upsert --drop 文件名

【示例】导入表数据

1
2
3
4
$ mongoimport -h 127.0.0.1 --port 27017 -d test -c book --drop test/book.dat
2020-09-11T10:53:56.359+0800 connected to: mongodb://127.0.0.1:27017/
2020-09-11T10:53:56.372+0800 dropping: test.book
2020-09-11T10:53:56.628+0800 431 document(s) imported successfully. 0 document(s) failed to import.

【示例】从 json 文件中导入表数据

1
2
3
$ mongoimport -h 127.0.0.1 --port 27017 -d test -c student --upsert test/student.json
2020-09-11T11:02:55.907+0800 connected to: mongodb://127.0.0.1:27017/
2020-09-11T11:02:56.068+0800 200 document(s) imported successfully. 0 document(s) failed to import.

【示例】从 csv 文件中导入表数据

1
2
3
$ mongoimport -h 127.0.0.1 --port 27017 -d test -c product --type csv --headerline test/product.csv
2020-09-11T11:07:49.788+0800 connected to: mongodb://127.0.0.1:27017/
2020-09-11T11:07:51.051+0800 11 document(s) imported successfully. 0 document(s) failed to import.

【示例】导入部分表字段数据

1
2
3
$ mongoimport -h 127.0.0.1 --port 27017 -d test -c product --type json --upsertFields name,price test/product.json
2020-09-11T11:14:05.410+0800 connected to: mongodb://127.0.0.1:27017/
2020-09-11T11:14:05.612+0800 11 document(s) imported successfully. 0 document(s) failed to import.

导出操作

语法格式:

1
mongoexport -h <IP> --port <端口> -u <用户名> -p <密码> -d <数据库> -c <表名> -f <字段> -q <条件导出> --csv -o <文件名>
  • -f:导出指字段,以逗号分割,-f name,email,age 导出 name,email,age 这三个字段
  • -q:可以根查询条件导出,-q '{ "uid" : "100" }' 导出 uid 为 100 的数据
  • --csv:表示导出的文件格式为 csv 的,这个比较有用,因为大部分的关系型数据库都是支持 csv,在这里有共同点

【示例】导出整张表

1
2
3
$ mongoexport -h 127.0.0.1 --port 27017 -d test -c product -o test/product.dat
2020-09-11T10:44:23.161+0800 connected to: mongodb://127.0.0.1:27017/
2020-09-11T10:44:23.177+0800 exported 11 records

【示例】导出表到 json 文件

1
2
3
$ mongoexport -h 127.0.0.1 --port 27017 -d test -c product --type json -o test/product.json
2020-09-11T10:49:52.735+0800 connected to: mongodb://127.0.0.1:27017/
2020-09-11T10:49:52.750+0800 exported 11 records

【示例】导出表中部分字段到 csv 文件

1
2
3
$ mongoexport -h 127.0.0.1 --port 27017 -d test -c product --type csv -f name,price -o test/product.csv
2020-09-11T10:47:33.160+0800 connected to: mongodb://127.0.0.1:27017/
2020-09-11T10:47:33.176+0800 exported 11 records

参考资料

Sqoop

Sqoop 是一个主要在 Hadoop 和关系数据库之间进行批量数据迁移的工具。

Sqoop 简介

Sqoop 是一个主要在 Hadoop 和关系数据库之间进行批量数据迁移的工具。

  • Hadoop:HDFS、Hive、HBase、Inceptor、Hyperbase
  • 面向大数据集的批量导入导出
  • 将输入数据集分为 N 个切片,然后启动 N 个 Map 任务并行传输
  • 支持全量、增量两种传输方式

提供多种 Sqoop 连接器

内置连接器

  • 经过优化的专用 RDBMS 连接器:MySQL、PostgreSQL、Oracle、DB2、SQL Server、Netzza 等
  • 通用的 JDBC 连接器:支持 JDBC 协议的数据库

第三方连接器

  • 数据仓库:Teradata
  • NoSQL 数据库:Couchbase

Sqoop 版本

Sqoop 1 优缺点

img

优点

  • 架构简单
  • 部署简单
  • 功能全面
  • 稳定性较高
  • 速度较快

缺点

  • 访问方式单一
  • 命令行方式容易出错,格式紧耦合
  • 安全机制不够完善,存在密码泄露风险

Sqoop 2 优缺点

img

优点

  • 访问方式多样
  • 集中管理连接器
  • 安全机制较完善
  • 支持多用户

缺点

  • 架构较复杂
  • 部署较繁琐
  • 稳定性一般
  • 速度一般

Sqoop 原理

导入

img

导出

img

Mysql 锁

不同存储引擎对于锁的支持粒度是不同的,由于 InnoDB 是 Mysql 的默认存储引擎,所以本文以 InnoDB 对于锁的支持进行阐述。

锁的分类

为了解决并发一致性问题,Mysql 支持了很多种锁来实现不同程度的隔离性,以保证数据的安全性。

独享锁和共享锁

InnoDB 实现标准行级锁定,根据是否独享资源,可以把锁分为两类:

  • 独享锁(Exclusive),简写为 X 锁,又称为“写锁”、“排它锁”。
    • 独享锁锁定的数据只允许进行锁定操作的事务使用,其他事务无法对已锁定的数据进行查询或修改。
    • 使用方式:SELECT ... FOR UPDATE;
  • 共享锁(Shared),简写为 S 锁,又称为“读锁”。
    • 共享锁锁定的资源可以被其他用户读取,但不能修改。在进行 SELECT 的时候,会将对象进行共享锁锁定,当数据读取完毕之后,就会释放共享锁,这样就可以保证数据在读取时不被修改。
    • 使用方式:SELECT ... LOCK IN SHARE MODE;

为什么要引入读写锁机制?

实际上,读写锁是一种通用的锁机制,并非 Mysql 的专利。在很多软件领域,都存在读写锁机制。

因为读操作本身是线程安全的,而一般业务往往又是读多写少的情况。因此,如果对读操作进行互斥,是不必要的,并且会大大降低并发访问效率。正式为了应对这种问题,产生了读写锁机制。

读写锁的特点是:读读不互斥读写互斥写写互斥。简言之:只要存在写锁,其他事务就不能做任何操作

注:InnoDB 下的行锁、间隙锁、next-key 锁统统属于独享锁。

悲观锁和乐观锁

基于加锁方式分类,Mysql 可以分为悲观锁和乐观锁。

  • 悲观锁 - 假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作
    • 在查询完数据的时候就把事务锁起来,直到提交事务(COMMIT
    • 实现方式:使用数据库中的锁机制
  • 乐观锁 - 假设最好的情况——每次访问数据时,都假设数据不会被其他线程修改,不必加锁。只在更新的时候,判断一下在此期间是否有其他线程更新该数据。
    • 实现方式:更新数据时,先使用版本号机制或 CAS 算法检查数据是否被修改

为什么要引入乐观锁?

乐观锁也是一种通用的锁机制,在很多软件领域,都存在乐观锁机制。

锁,意味着互斥,意味着阻塞。在高并发场景下,锁越多,阻塞越多,势必会拉低并发性能。那么,为了提高并发度,能不能尽量不加锁呢?

乐观锁,顾名思义,就是假设最好的情况——每次访问数据时,都假设数据不会被其他线程修改,不必加锁。虽然不加锁,但不意味着什么都不做,而是在更新的时候,判断一下在此期间是否有其他线程更新该数据。乐观锁最常见的实现方式,是使用版本号机制或 CAS 算法(Compare And Swap)去实现。

  • 乐观锁的优点是:减少锁竞争,提高并发度。

  • 乐观锁的缺点是:

    • 存在 ABA 问题。所谓的 ABA 问题是指在并发编程中,如果一个变量初次读取的时候是 A 值,它的值被改成了 B,然后又其他线程把 B 值改成了 A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了
    • 如果乐观锁所检查的数据存在大量锁竞争,会由于不断循环重试,产生大量的 CPU 开销

【示例】Mysql 乐观锁示例

假设,order 表中有一个字段 status,表示订单状态:status 为 1 代表订单未支付;status 为 2 代表订单已支付。现在,要将 id 为 1 的订单状态置为已支付,则操作如下:

1
2
3
4
5
select status, version from order where id=#{id}

update order
set status=2, version=version+1
where id=#{id} and version=#{version};

乐观锁更多详情可以参考:使用 mysql 乐观锁解决并发问题

全局锁、表级锁、行级锁

前文提到了,锁,意味着互斥,意味着阻塞。在高并发场景下,锁越多,阻塞越多,势必会拉低并发性能。在不得不加锁的情况下,显然,加锁的范围越小,锁竞争的发生频率就越小,系统的并发程度就越高。但是,加锁也需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销,锁粒度越小,系统的锁操作开销就越大。因此,在选择锁粒度时,也需要在锁开销和并发程度之间做一个权衡。

根据加锁的范围,MySQL 的锁大致可以分为:

  • 全局锁 - “全局锁”会锁定整个数据库
  • 表级锁(table lock) - “表级锁”锁定整张表。用户对表进行写操作前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他用户才能获得读锁,读锁之间不会相互阻塞。表级锁有:
    • 表锁 - 表锁就是对数据表进行锁定,锁定粒度很大,同时发生锁冲突的概率也会较高,数据访问的并发度低。不过好处在于对锁的使用开销小,加锁会很快。表锁一般是在数据库引擎不支持行锁的时候才会被用到的。
    • 元数据锁(MDL) - MDL 不需要显式使用,在访问一个表的时候会被自动加上。
    • 意向锁(Intention Lock)
    • 自增锁(AUTO-INC)
  • 行级锁(row lock) - “行级锁”锁定指定的行记录。这样其它线程还是可以对同一个表中的其它行记录进行操作。行级锁有:
    • 记录锁(Record Lock)
    • 间隙锁(Gap Lock)
    • 临键锁(Next-Key Lock)
    • 插入意向锁

以上各种加锁粒度,在不同存储引擎中的支持情况并不相同。如:InnoDB 支持全局锁、表级锁、行级锁;而 MyISAM 只支持全局锁、表级锁。

每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如 InnoDB 中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。

全局锁

“全局锁”会锁定整个数据库

要给整个数据库加全局锁,可以执行以下命令:

1
flush tables with read lock

执行命名后,整个库处于只读状态,之后其他线程的以下语句会被阻塞:数据更新语句(数据的增删改)、数据定义语句(包括建表、修改表结构等)和更新类事务的提交语句。

如果要释放全局锁,可以执行以下命令:

1
unlock tables

此外,在客户端断开的时候会自动释放锁。

全局锁的典型使用场景是,做全库逻辑备份。

官方自带的逻辑备份工具是 mysqldump。当 mysqldump 使用参数 –single-transaction 的时候,导数据之前就会启动一个事务,来确保拿到一致性视图。而由于 MVCC 的支持,这个过程中数据是可以正常更新的。对于全部是 InnoDB 引擎的库,建议选择使用 –single-transaction 参数,对应用会更友好。如果有的表使用了不支持事务的引擎,那么备份就只能通过 FTWRL 方法。

表级锁

“表级锁”会锁定整张表。用户对表进行写操作前,需要先获得写锁,这会阻塞其他用户对该表的所有读写操作。只有没有写锁时,其他用户才能获得读锁,读锁之间不会相互阻塞。

表锁

表锁就是对数据表进行锁定,锁定粒度很大,同时发生锁冲突的概率也会较高,数据访问的并发度低。不过好处在于对锁的使用开销小,加锁会很快。

**表锁的语法是 lock tables … read/write**,示例如下:

1
2
3
4
5
// 为 xxx 表加 MDL 读锁
lock tables xxx read;

// 为 xxx 表加 MDL 写锁
lock tables xxx write;

与 FTWRL 类似,可以用 unlock tables 主动释放锁,也可以在客户端断开的时候自动释放。

表锁一般是在数据库引擎不支持行锁的时候才会被用到的。如果你发现应用程序里有 lock tables 这样的语句,需要追查一下,比较可能的情况是:

  • 要么是你的系统现在还在用 MyISAM 这类不支持事务的引擎,那要安排升级换引擎;
  • 要么是你的引擎升级了,但是代码还没升级。我见过这样的情况,最后业务开发就是把 lock tablesunlock tables 改成 begincommit,问题就解决了。

元数据锁(MDL)

元数据锁,英文为 metadata lock,缩写为 MDL。MySQL 5.5 版本中引入了 MDL。MDL 的作用是,保证读写的正确性。MDL 不需要显式使用,在访问一个表的时候会被自动加上

  • 对一个表做“增删改查”操作的时候,加 MDL 读锁。读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。
  • 对一个表做“结构变更”操作的时候,加 MDL 写锁。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性。因此,如果有两个线程要同时给一个表加字段,其中一个要等另一个执行完才能开始执行。

MDL 会直到事务提交才释放。在做表结构变更的时候,一定要小心不要导致锁住线上查询和更新。

如果数据库有一个长事务(所谓的长事务,就是开启了事务,但是一直还没提交),那么在对表结构做变更操作的时候,可能会发生意想不到的事情,比如下面这个顺序的场景:

  1. 首先,线程 A 先启用了事务(但是一直不提交),然后执行一条 SELECT 语句,此时就先对该表加上 MDL 读锁;
  2. 然后,线程 B 也执行了同样的 SELECT 语句,此时并不会阻塞,因为“读读”并不冲突;
  3. 接着,线程 C 修改了表字段,此时由于线程 A 的事务并没有提交,也就是 MDL 读锁还在占用着,这时线程 C 就无法申请到 MDL 写锁,就会被阻塞,

那么在线程 C 阻塞后,后续有对该表的 SELECT 语句,就都会被阻塞。如果此时有大量该表的 SELECT 语句的请求到来,就会有大量的线程被阻塞住,这时数据库的线程很快就会爆满了。

为什么线程 C 因为申请不到 MDL 写锁,而导致后续的申请读锁的查询操作也会被阻塞?

这是因为申请 MDL 锁的操作会形成一个队列,队列中写锁获取优先级高于读锁,一旦出现 MDL 写锁等待,会阻塞后续该表的所有 CRUD 操作。

所以为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。

意向锁(Intention Lock)

InnoDB 支持不同粒度的锁定,允许行锁和表锁共存。当存在表级锁和行级锁的情况下,必须先申请意向锁,再获取行级锁。意向锁是表级锁,表示事务稍后需要对表中的行使用哪种类型的锁(共享或独享)。意向锁是 InnoDB 自动添加的,不需要用户干预。

意向锁有两种类型:

  • 意向共享锁(IS - 表示事务有意向对表中的行设置共享锁(S)。

  • 意向独享锁(IX - 表示事务有意向对表中的行设置独享锁(X)。

比如 SELECT ... FOR SHARE 设置 IS 锁, SELECT ... FOR UPDATE 设置 IX 锁。

意向锁的规则如下:

  • 一个事务在获得某个数据行的共享锁(S)之前,必须先获得表的意向共享锁(IS)或者更强的锁;
  • 一个事务在获得某个数据行的独享锁(X)之前,必须先获得表的意向独享锁(IX)。

也就是,当执行插入、更新、删除操作,需要先对表加上 IX 锁,然后对该记录加 X 锁。而快照读(普通的 SELECT)是不会加行级锁的,快照读是利用 MVCC 实现一致性读,是无锁的。

不过,SELECT 也是可以对记录加共享锁和独享锁的,具体方式如下:

1
2
3
4
5
// 先在表上加上 IS 锁,然后对读取的记录加 S 锁
select ... lock in share mode;

// 当前读:先在表上加上 IX 锁,然后对读取的记录加 X 锁
select ... for update;

意向共享锁和意向独享锁是表级锁,不会和行级的共享锁和独享锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁(lock tables ... read)和独享表锁(lock tables ... write)发生冲突

如果申请的锁与现有锁兼容,则锁申请成功;反之,则锁申请失败。锁申请失败的情况下,申请锁的事务会一直等待,直到存在冲突的锁被释放。如果存在与申请的锁相冲突的锁,并且该锁迟迟得不到释放,就会导致死锁。

为什么要引入意向锁?

如果没有意向锁,那么加独享表锁时,就需要遍历表里所有记录,查看是否有记录存在独享锁,这样效率会很低。

有了意向锁,在对记录加独享锁前,会先加上表级别的意向独享锁。此时,如果需要加独享表锁,可以直接查该表是否有意向独享锁:如果有,就意味着表里已经有记录被加了独享锁。这样一来,就不用去遍历表里的记录了。

综上所述,意向锁的目的是为了快速判断表里是否有记录被加锁

自增锁(AUTO-INC)

表里的主键通常都会设置成自增的,这是通过对主键字段声明 AUTO_INCREMENT 属性实现的。之后可以在插入数据时,可以不指定主键的值,数据库会自动给主键赋值递增的值,这主要是通过 AUTO-INC 锁实现的。

AUTO-INC 锁是特殊的表级锁,锁不是在一个事务提交后才释放,而是在执行完插入语句后就会立即释放

在插入数据时,会加一个表级别的 AUTO-INC 锁,然后为被 AUTO_INCREMENT 修饰的字段赋值递增的值,等插入语句执行完成后,才会把 AUTO-INC 锁释放掉。

一个事务在持有 AUTO-INC 锁的过程中,其他事务的如果要向该表插入语句都会被阻塞,从而保证插入数据时,被 AUTO_INCREMENT 修饰的字段的值是连续递增的。但是,AUTO-INC 锁再对大量数据进行插入的时候,会影响插入性能,因为另一个事务中的插入会被阻塞。

因此, 在 MySQL 5.1.22 版本开始,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。一样也是在插入数据的时候,会为被 AUTO_INCREMENT 修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁

InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量,是用来控制选择用 AUTO-INC 锁,还是轻量级的锁。

  • innodb_autoinc_lock_mode = 0,就采用 AUTO-INC 锁,语句执行结束后才释放锁;
  • innodb_autoinc_lock_mode = 2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。
  • innodb_autoinc_lock_mode = 1
    • 普通 insert 语句,自增锁在申请之后就马上释放;
    • 类似 insert … select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;

以上模式中,innodb_autoinc_lock_mode = 2 是性能最高的方式,但是当搭配 binlog 的日志格式是 statement 一起使用的时候,在“主从复制的场景”中会发生数据不一致的问题

行锁

MySQL 的行锁是在引擎层由各个引擎自己实现的。但并不是所有的引擎都支持行锁,比如 MyISAM 引擎就不支持行锁。不支持行锁意味着并发控制只能使用表锁,对于这种引擎的表,同一张表上任何时刻只能有一个更新在执行,这就会影响到业务并发度。InnoDB 是支持行锁的,这也是 MyISAM 被 InnoDB 替代的重要原因之一。

在 InnoDB 引擎中,行锁是通过给索引上的索引项加锁来实现的如果没有索引,InnoDB 将会通过隐藏的聚簇索引来对记录加锁。此外,在 InnoDB 引擎中,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,而是要等到事务结束时才释放。这个就是两阶段锁协议。因此,如果事务中需要锁多个行,要把最可能造成锁冲突、最可能影响并发度的锁尽量往后放。

行锁的具体实现算法有三种:Record Lock、Gap Lock 以及 Next-Key Lock。

记录锁(Record Lock)

记录锁(Record Lock)锁定一个记录上的索引,而不是记录本身。例如,执行 SELECT value FROM t WHERE value BETWEEN 10 and 20 FOR UPDATE; 后,会禁止任何其他事务插入、更新或删除 t.value 值在 10 到 20 范围之内的数据,因为该范围内的所有现有值之间的间隙已被锁定。

记录锁始终锁定索引记录,即使表定义为没有索引。如果表没有设置索引,InnoDB 会自动创建一个隐藏的聚簇索引并使用该索引进行记录锁定。

Record Lock 是有 S 锁和 X 锁之分的:

  • 当一个事务对一条记录加了 S 型记录锁后,其他事务也可以继续对该记录加 S 型记录锁(S 型与 S 锁兼容),但是不可以对该记录加 X 型记录锁(S 型与 X 锁不兼容);
  • 当一个事务对一条记录加了 X 型记录锁后,其他事务既不可以对该记录加 S 型记录锁(S 型与 X 锁不兼容),也不可以对该记录加 X 型记录锁(X 型与 X 锁不兼容)。

【示例】记录锁示例

注:测试环境的事务隔离级别为可重复级别

初始化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 创建表
DROP TABLE IF EXISTS `t`;
CREATE TABLE `t` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`value` INT(10) DEFAULT 0,
PRIMARY KEY (`id`)
)
ENGINE = InnoDB
DEFAULT CHARSET = `utf8`;

-- 分别插入 id 为 1、10、20 的数据
INSERT INTO `t`(`id`, `value`) VALUES (1, 1);
INSERT INTO `t`(`id`, `value`) VALUES (10, 10);
INSERT INTO `t`(`id`, `value`) VALUES (20, 20);

事务一、添加 X 型记录锁

1
2
3
4
5
6
7
8
9
10
11
-- 开启事务
BEGIN;

-- 对 id 为 1 的记录添加 X 型记录锁
SELECT * FROM `t` WHERE `id` = 1 FOR UPDATE;

-- 延迟 20 秒执行后续语句,保持锁定状态
SELECT SLEEP(20);

-- 释放锁
COMMIT;

事务二、被锁定的行记录无法修改

1
2
3
4
5
-- 修改 id = 10 的行记录,正常执行
UPDATE `t` SET `value` = 0 WHERE `id` = 10;

-- 修改 id = 1 的行记录,由于 id = 1 被 X 型记录锁锁定,直到事务一释放锁,方能执行
UPDATE `t` SET `value` = 0 WHERE `id` = 1;

间隙锁(Gap Lock)

间隙锁(Gap Lock)锁定索引之间的间隙,但是不包含索引本身

间隙锁虽然存在 X 型间隙锁和 S 型间隙锁,但是并没有什么区别,它们彼此不冲突,不同事务可以在间隙上持有冲突锁,并不存在互斥关系。例如,事务 A 可以在某个间隙上持有 S 型间隙锁,而事务 B 在同一间隙上持有 X 型间隙锁。允许存在冲突间隙锁的原因是:如果从索引中清除记录,则必须合并不同事务在该记录上持有的间隙锁。

间隙锁只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。如果将事务隔离级别更改为 读已提交,则间隙锁定对搜索和索引扫描禁用,并且仅用于外键约束检查和重复键检查。

在 MySQL 中,间隙锁默认是开启的,即 innodb_locks_unsafe_for_binlog 参数值是 disable 的,且 MySQL 中默认的是 RR 事务隔离级别。

【示例】间隙锁示例

注:测试环境的事务隔离级别为可重复级别

初始化数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 创建表
DROP TABLE IF EXISTS `t`;
CREATE TABLE `t` (
`id` INT(10) NOT NULL AUTO_INCREMENT,
`value` INT(10) DEFAULT 0,
PRIMARY KEY (`id`)
)
ENGINE = InnoDB
DEFAULT CHARSET = `utf8`;

-- 分别插入 id 为 1、10、20 的数据
INSERT INTO `t`(`id`, `value`) VALUES (1, 1);
INSERT INTO `t`(`id`, `value`) VALUES (10, 10);
INSERT INTO `t`(`id`, `value`) VALUES (20, 20);

事务一、添加间隙锁

1
2
3
4
5
6
7
8
9
10
11
-- 开启事务
BEGIN;

-- 对 id 为 1 的记录添加间隙锁
SELECT * FROM `t` WHERE `id` BETWEEN 1 AND 10 FOR UPDATE;

-- 延迟 20 秒执行后续语句,保持锁定状态
SELECT SLEEP(20);

-- 释放锁
COMMIT;

事务二、被锁定范围内的行记录无法修改

1
2
3
4
5
6
7
8
9
10
11
12
-- 插入 id 为 1 到 10 范围之外的数据,正常执行
INSERT INTO `t`(`id`, `value`) VALUES (15, 15);

-- 更新 id 为 1 到 10 范围之外的数据,正常执行
UPDATE `t` SET `value` = 0 WHERE `id` = 20;

-- 插入 id 为 1 到 10 范围之内的数据,被阻塞
INSERT INTO `t`(`id`, `value`) VALUES (5, 5);

-- 更新 id 为 1 到 10 范围之内的数据,被阻塞
UPDATE `t` SET `value` = 0 WHERE `id` = 1;
UPDATE `t` SET `value` = 0 WHERE `id` = 10;

临键锁(Next-Key Lock)

临键锁(Next-Key Lock)是记录锁和间隙锁的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙(它锁定一个前开后闭区间)。

假设索引包含值 10、11、13 和 20,那么该索引可能的 Next-Key Lock 涵盖以下区间:

1
2
3
4
5
(-, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)

所以,Next-Key Lock 即能保护该记录,又能阻止其他事务将新纪录插入到被保护记录前面的间隙中。MVCC 不能解决幻读问题,Next-Key 锁就是为了解决幻读问题而提出的。在可重复读(REPEATABLE READ)隔离级别下,使用MVCC + Next-Key 锁可以解决幻读问题。

只有可重复读、串行化隔离级别下的特定操作才会取得间隙锁或 Next-Key Lock。在 SelectUpdateDelete 时,除了基于唯一索引的查询之外,其它索引查询时都会获取间隙锁或 Next-Key Lock,即锁住其扫描的范围。主键索引也属于唯一索引,所以主键索引是不会使用间隙锁或 Next-Key Lock。

索引分为主键索引和非主键索引两种,如果一条 SQL 语句操作了主键索引,MySQL 就会锁定这条主键索引;如果一条语句操作了非主键索引,MySQL 会先锁定该非主键索引,再锁定相关的主键索引。在 UPDATEDELETE 操作时,MySQL 不仅锁定 WHERE 条件扫描过的所有索引记录,而且会锁定相邻的键值,即所谓的 Next-Key Lock。

插入意向锁

插入意向锁不是意向锁,而是一种特殊的间隙锁。当一个事务试图插入一条记录时,需要判断插入位置是否已被其他事务加了间隙锁(临键锁(Next-Key Lock 也包含间隙锁)。如果有的话,插入操作就会发生阻塞,直到拥有间隙锁的那个事务提交为止(释放间隙锁的时刻);在此期间,会生成一个插入意向锁,表明有事务想在某个区间插入新记录,但是现在处于等待状态。

假设存在值为 4 和 7 的索引记录。分别尝试插入值 5 和 6 的单独事务在获得插入行上的排他锁之前,每个事务都使用插入意向锁锁定 4 和 7 之间的间隙,但不要互相阻塞,因为行不冲突。

【示例】获取插入意向锁

初始化数据

1
2
mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);

事务 A 对 id 大于 100 的索引记录设置独享锁。独享锁包括了 id=102 之前的间隙锁:

1
2
3
4
5
6
7
mysql> BEGIN;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id |
+-----+
| 102 |
+-----+

事务 B 将记录插入到间隙中。事务在等待获取独享锁时获取插入意向锁。

1
2
mysql> BEGIN;
mysql> INSERT INTO child (id) VALUES (101);

死锁

“死锁”是指两个或多个事务竞争同一资源,并请求锁定对方占用的资源,从而导致恶性循环的现象

产生死锁的场景:

  • 当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁。

  • 多个事务同时锁定同一个资源时,也会产生死锁。

死锁示例

(1)数据初始化

1
2
3
4
5
6
7
8
9
10
-- 创建表 test
CREATE TABLE `test` (
`id` INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT,
`value` INT(10) NOT NULL
);

-- 数据初始化
INSERT INTO `test` (`id`, `value`) VALUES (1, 1);
INSERT INTO `test` (`id`, `value`) VALUES (2, 2);
INSERT INTO `test` (`id`, `value`) VALUES (3, 3);

(2)两个事务严格按下表顺序执行,产生死锁

事务 A 事务 B
BEGIN; BEGIN;
– 查询 value = 4 的记录
SELECT * FROM test WHERE value = 4 FOR UPDATE;
– 结果为空
– 查询 value = 5 的记录
SELECT * FROM test WHERE value = 5 FOR UPDATE;
– 结果为空
INSERT INTO test (id, value) VALUES (4, 4);
– 锁等待中
INSERT INTO test (id, value) VALUES (5, 5);
– 锁等待中
– 由于死锁无法执行到此步骤
COMMIT;
– 由于死锁无法执行到此步骤
COMMIT;

死锁是如何产生的

行锁的具体实现算法有三种:Record Lock、Gap Lock 以及 Next-Key Lock。Record Lock 是专门对索引项加锁;Gap Lock 是对索引项之间的间隙加锁;Next-Key Lock 则是前面两种的组合,对索引项以其之间的间隙加锁。

只有在可重复读或以上隔离级别下的特定操作才会取得 Gap Lock 或 Next-Key Lock,在 Select、Update 和 Delete 时,除了基于唯一索引的查询之外,其它索引查询时都会获取 Gap Lock 或 Next-Key Lock,即锁住其扫描的范围。主键索引也属于唯一索引,所以主键索引是不会使用 Gap Lock 或 Next-Key Lock。

在 MySQL 中,Gap Lock 默认是开启的,即 innodb_locks_unsafe_for_binlog 参数值是 disable 的,且 MySQL 中默认的是可重复读事务隔离级别。

当我们执行以下查询 SQL 时,由于 value 列为非唯一索引,此时又是 RR 事务隔离级别,所以 SELECT 的加锁类型为 Gap Lock,这里的 gap 范围是 (4,+∞)。

1
SELECT * FROM test where value = 4 for update;

执行查询 SQL 语句获取的 Gap Lock 并不会导致阻塞,而当我们执行以下插入 SQL 时,会在插入间隙上再次获取插入意向锁。插入意向锁其实也是一种 gap 锁,它与 Gap Lock 是冲突的,所以当其它事务持有该间隙的 Gap Lock 时,需要等待其它事务释放 Gap Lock 之后,才能获取到插入意向锁。

以上事务 A 和事务 B 都持有间隙 (4,+∞)的 gap 锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的 gap 锁释放,于是就造成了循环等待,导致死锁。

1
INSERT INTO `test` (`id`, `value`) VALUES (5, 5);

img

另一个死锁场景

InnoDB 存储引擎的主键索引为聚簇索引,其它索引为辅助索引。如果使用辅助索引来更新数据库,就需要使用聚簇索引来更新数据库字段。如果两个更新事务使用了不同的辅助索引,或一个使用了辅助索引,一个使用了聚簇索引,就都有可能导致锁资源的循环等待。由于本身两个事务是互斥,也就构成了以上死锁的四个必要条件了。

img

出现死锁的步骤:

img

综上可知,在更新操作时,我们应该尽量使用主键来更新表字段,这样可以有效避免一些不必要的死锁发生。

避免死锁

死锁的四个必要条件:互斥、占有且等待、不可强占用、循环等待。只要系统发生死锁,这些条件必然成立,但是只要破坏任意一个条件就死锁就不会成立。由此可知,要想避免死锁,就要从这几个必要条件上去着手:

  • 更新表时,尽量使用主键更新,减少冲突;
  • 避免长事务,尽量将长事务拆解,可以降低与其它事务发生冲突的概率;
  • 设置合理的锁等待超时参数,我们可以通过 innodb_lock_wait_timeout 设置合理的等待超时阈值,特别是在一些高并发的业务中,我们可以尽量将该值设置得小一些,避免大量事务等待,占用系统资源,造成严重的性能开销。
  • 在编程中尽量按照固定的顺序来处理数据库记录,假设有两个更新操作,分别更新两条相同的记录,但更新顺序不一样,有可能导致死锁;
  • 在允许幻读和不可重复读的情况下,尽量使用读已提交事务隔离级别,可以避免 Gap Lock 导致的死锁问题;
  • 还可以使用其它的方式来代替数据库实现幂等性校验。例如,使用 Redis 以及 ZooKeeper 来实现,运行效率比数据库更佳。

解决死锁

当出现死锁以后,有两种策略:

  • 设置事务等待锁的超时时间。这个超时时间可以通过参数 innodb_lock_wait_timeout 来设置。
  • 开启死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect 设置为 on,表示开启这个逻辑。

在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。但是,我直接把这个时间设置成一个很小的值,比如 1s,也是不可取的。当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。

所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。为了解决死锁问题,不同数据库实现了各自的死锁检测和超时机制。InnoDB 的处理策略是:将持有最少行级排它锁的事务进行回滚。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担的:每当一个事务被锁的时候,就要看看它所依赖的线程有没有被别人锁住,如此循环,最后判断是否出现了循环等待,也就是死锁。因此,死锁检测可能会耗费大量的 CPU。

参考资料

MongoDB 应用指南

简介

MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。

MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。

MongoDB 发展

  • 1.x - 支持复制和分片
  • 2.x - 更丰富的数据库功能
  • 3.x - WiredTiger 和周边生态
  • 4.x - 支持分布式事务

MongoDB 和 RDBMS

特性 MongoDB RDBMS
数据模型 文档模型 关系型
CRUD 操作 MQL/SQL SQL
高可用 复制集 集群模式
扩展性 支持分片 数据分区
扩繁方式 垂直扩展+水平扩展 垂直扩展
索引类型 B 树、全文索引、地理位置索引、多键索引、TTL 索引 B 树
数据容量 没有理论上限 千万、亿

MongoDB 特性

  • 数据是 JSON 结构
    • 支持结构化、半结构化数据模型
    • 可以动态响应结构变化
  • 通过副本机制提供高可用
  • 通过分片提供扩容能力

MongoDB 概念

SQL 术语/概念 MongoDB 术语/概念 解释/说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表连接,MongoDB 不支持
primary key primary key 主键,MongoDB 自动将_id 字段设置为主键

数据库

一个 MongoDB 中可以建立多个数据库。

MongoDB 的默认数据库为”db”,该数据库存储在 data 目录中。

MongoDB 的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。

“show dbs” 命令可以显示所有数据的列表。

1
2
3
4
5
6
7
$ ./mongo
MongoDBshell version: 3.0.6
connecting to: test
> show dbs
local 0.078GB
test 0.078GB
>

执行 “db” 命令可以显示当前数据库对象或集合。

1
2
3
4
5
6
$ ./mongo
MongoDBshell version: 3.0.6
connecting to: test
> db
test
>

运行”use”命令,可以连接到一个指定的数据库。

1
2
3
4
5
> use local
switched to db local
> db
local
>

数据库也通过名字来标识。数据库名可以是满足以下条件的任意 UTF-8 字符串。

  • 不能是空字符串(””)。
  • 不得含有 ‘ ‘(空格)、.\$/\\0 (空字符)。
  • 应全部小写。
  • 最多 64 字节。

有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库。

  • admin:从权限的角度来看,这是”root”数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。
  • local:这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合
  • config:当 Mongo 用于分片设置时,config 数据库在内部使用,用于保存分片的相关信息。

文档

文档是一组键值(key-value)对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。

需要注意的是:

  • 文档中的键/值对是有序的。
  • 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。
  • MongoDB 区分类型和大小写。
  • MongoDB 的文档不能有重复的键。
  • 文档的键是字符串。除了少数例外情况,键可以使用任意 UTF-8 字符。

文档键命名规范:

  • 键不能含有 \0 (空字符)。这个字符用来表示键的结尾。
  • .$ 有特别的意义,只有在特定环境下才能使用。
  • 以下划线 _ 开头的键是保留的(不是严格要求的)。

集合

集合就是 MongoDB 文档组,类似于 RDBMS (关系数据库管理系统:Relational Database Management System)中的表格。

集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。

合法的集合名:

  • 集合名不能是空字符串””。
  • 集合名不能含有 \0 字符(空字符),这个字符表示集合名的结尾。
  • 集合名不能以”system.”开头,这是为系统集合保留的前缀。
  • 用户创建的集合名字不能含有保留字符。有些驱动程序的确支持在集合名里面包含,这是因为某些系统生成的集合中包含该字符。除非你要访问这种系统创建的集合,否则千万不要在名字里出现 $

元数据

数据库的信息是存储在集合中。它们使用了系统的命名空间:dbname.system.*

在 MongoDB 数据库中名字空间 <dbname>.system.* 是包含多种系统信息的特殊集合(Collection),如下:

集合命名空间 描述
dbname.system.namespaces 列出所有名字空间。
dbname.system.indexes 列出所有索引。
dbname.system.profile 包含数据库概要(profile)信息。
dbname.system.users 列出所有可访问数据库的用户。
dbname.local.sources 包含复制对端(slave)的服务器信息和状态。

对于修改系统集合中的对象有如下限制。

system.indexes 插入数据,可以创建索引。但除此之外该表信息是不可变的(特殊的 drop index 命令将自动更新相关信息)。system.users 是可修改的。system.profile 是可删除的。

MongoDB 数据类型

数据类型 描述
String 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。
Integer 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。
Boolean 布尔值。用于存储布尔值(真/假)。
Double 双精度浮点值。用于存储浮点值。
Min/Max keys 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。
Array 用于将数组或列表或多个值存储为一个键。
Timestamp 时间戳。记录文档修改或添加的具体时间。
Object 用于内嵌文档。
Null 用于创建空值。
Symbol 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。
Date 日期时间。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。
Object ID 对象 ID。用于创建文档的 ID。
Binary Data 二进制数据。用于存储二进制数据。
Code 代码类型。用于在文档中存储 JavaScript 代码。
Regular expression 正则表达式类型。用于存储正则表达式。

MongoDB CRUD

数据库操作

查看所有数据库

1
show dbs

创建数据库

1
use <database>

如果数据库不存在,则创建数据库,否则切换到指定数据库。

【示例】创建数据库,并插入一条数据

刚创建的数据库 test 并不在数据库的列表中, 要显示它,需要插入一些数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
> use test
switched to db test
>
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
> db.test.insert({"name":"mongodb"})
WriteResult({ "nInserted" : 1 })
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
test 0.000GB

删除数据库

删除当前数据库

1
db.dropDatabase()

集合操作

查看集合

1
show collections

创建集合

1
db.createCollection(name, options)

参数说明:

  • name: 要创建的集合名称
  • options: 可选参数, 指定有关内存大小及索引的选项

options 可以是如下参数:

字段 类型 描述
capped 布尔 (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 当该值为 true 时,必须指定 size 参数。
autoIndexId 布尔 3.2 之后不再支持该参数。(可选)如为 true,自动在 _id 字段创建索引。默认为 false。
size 数值 (可选)为固定集合指定一个最大值,即字节数。 如果 capped 为 true,也需要指定该字段。
max 数值 (可选)指定固定集合中包含文档的最大数量。

在插入文档时,MongoDB 首先检查固定集合的 size 字段,然后检查 max 字段。

1
2
3
4
> db.createCollection("collection")
{ "ok" : 1 }
> show collections
collection

删除集合

1
2
3
4
> db.collection.drop()
true
> show collections
>

插入文档操作

MongoDB 使用 insert() 方法完成插入操作。

语法格式

1
2
3
4
# 插入单条记录
db.<集合>.insertOne(<JSON>)
# 插入多条记录
db.<集合>.insertMany([<JSON 1>, <JSON 2>, ..., <JSON N>])

【示例】insertOne

1
2
3
4
5
> db.color.insertOne({name: "red"})
{
"acknowledged" : true,
"insertedId" : ObjectId("5f533ae4e8f16647950fdf43")
}

【示例】insertMany

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> db.color.insertMany([
{
"name": "yellow"
},
{
"name": "blue"
}
])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5f533bcae8f16647950fdf44"),
ObjectId("5f533bcae8f16647950fdf45")
]
}
>

查询文档操作

MongoDB 使用 find() 方法完成查询文档操作。

语法格式

1
db.<集合>.find(<JSON>)

查询条件也是 json 形式,如果不设置查询条件,即为全量查询。

查询条件

操作 格式 范例 RDBMS 中的类似语句
等于 {<key>:<value>} db.book.find({"pageCount": {$eq: 0}}) where pageCount = 0
不等于 {<key>:{$ne:<value>}} db.book.find({"pageCount": {$ne: 0}}) where likes != 50
大于 {<key>:{$gt:<value>}} db.book.find({"pageCount": {$gt: 0}}) where likes > 50
{<key>:{$gt:<value>}} db.book.find({"pageCount": {$gt: 0}}) where likes > 50 大于或等于
小于 {<key>:{$lt:<value>}} db.book.find({"pageCount": {$lt: 200}}) where likes < 50
小于或等于 {<key>:{$lte:<value>}} db.book.find({"pageCount": {$lte: 200}}) where likes <= 50

说明:

1
2
3
4
5
6
$eq  --------  equal  =
$ne ----------- not equal !=
$gt -------- greater than >
$gte --------- gt equal >=
$lt -------- less than <
$lte --------- lt equal <=

【示例】

1
2
3
4
5


# 统计匹配查询条件的记录数
> db.book.find({"status": "MEAP"}).count()
68

查询逻辑条件

(1)and 条件

MongoDB 的 find() 方法可以传入多个键(key),每个键(key)以逗号隔开,即常规 SQL 的 AND 条件。

语法格式如下:

1
> db.col.find({key1:value1, key2:value2}).pretty()

(2)or 条件

MongoDB OR 条件语句使用了关键字 $or,语法格式如下:

1
2
3
4
5
6
7
>db.col.find(
{
$or: [
{key1: value1}, {key2:value2}
]
}
).pretty()

模糊查询

查询 title 包含”教”字的文档:

1
db.col.find({ title: /教/ })

查询 title 字段以”教”字开头的文档:

1
db.col.find({ title: /^教/ })

查询 titl e 字段以”教”字结尾的文档:

1
db.col.find({ title: /教$/ })

Limit() 方法

如果你需要在 MongoDB 中读取指定数量的数据记录,可以使用 MongoDB 的 Limit 方法,limit()方法接受一个数字参数,该参数指定从 MongoDB 中读取的记录条数。

limit()方法基本语法如下所示:

1
>db.COLLECTION_NAME.find().limit(NUMBER)

Skip() 方法

我们除了可以使用 limit()方法来读取指定数量的数据外,还可以使用 skip()方法来跳过指定数量的数据,skip 方法同样接受一个数字参数作为跳过的记录条数。

skip() 方法脚本语法格式如下:

1
>db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER)

Sort() 方法

在 MongoDB 中使用 sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。

sort()方法基本语法如下所示:

1
>db.COLLECTION_NAME.find().sort({KEY:1})

注意:skip(), limilt(), sort()三个放在一起执行的时候,执行的顺序是先 sort(), 然后是 skip(),最后是显示的 limit()。

更新文档操作

update() 方法用于更新已存在的文档。语法格式如下:

1
2
3
4
5
6
7
8
9
db.collection.update(
<query>,
<update>,
{
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
}
)

参数说明:

  • query : update 的查询条件,类似 sql update 查询内 where 后面的。
  • update : update 的对象和一些更新的操作符(如$,$inc…)等,也可以理解为 sql update 查询内 set 后面的
  • upsert : 可选,这个参数的意思是,如果不存在 update 的记录,是否插入 objNew,true 为插入,默认是 false,不插入。
  • multi : 可选,mongodb 默认是 false,只更新找到的第一条记录,如果这个参数为 true,就把按条件查出来多条记录全部更新。
  • writeConcern :可选,抛出异常的级别。

【示例】更新文档

1
db.collection.update({ title: 'MongoDB 教程' }, { $set: { title: 'MongoDB' } })

【示例】更新多条相同文档

以上语句只会修改第一条发现的文档,如果你要修改多条相同的文档,则需要设置 multi 参数为 true。

1
2
3
4
5
db.collection.update(
{ title: 'MongoDB 教程' },
{ $set: { title: 'MongoDB' } },
{ multi: true }
)

【示例】更多实例

只更新第一条记录:

1
db.collection.update({ count: { $gt: 1 } }, { $set: { test2: 'OK' } })

全部更新:

1
2
3
4
5
6
db.collection.update(
{ count: { $gt: 3 } },
{ $set: { test2: 'OK' } },
false,
true
)

只添加第一条:

1
2
3
4
5
6
db.collection.update(
{ count: { $gt: 4 } },
{ $set: { test5: 'OK' } },
true,
false
)

全部添加进去:

1
2
3
4
5
6
db.collection.update(
{ count: { $gt: 4 } },
{ $set: { test5: 'OK' } },
true,
false
)

全部更新:

1
2
3
4
5
6
db.collection.update(
{ count: { $gt: 4 } },
{ $set: { test5: 'OK' } },
true,
false
)

只更新第一条记录:

1
2
3
4
5
6
db.collection.update(
{ count: { $gt: 4 } },
{ $set: { test5: 'OK' } },
true,
false
)

删除文档操作

官方推荐使用 deleteOne() 和 deleteMany() 方法删除数据。

删除 status 等于 A 的全部文档:

1
db.collection.deleteMany({ status: 'A' })

删除 status 等于 D 的一个文档:

1
db.collection.deleteOne({ status: 'D' })

索引操作

索引通常能够极大的提高查询的效率,如果没有索引,MongoDB 在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。

这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。

索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构。

MongoDB 使用 createIndex() 方法来创建索引。

createIndex()方法基本语法格式如下所示:

1
>db.collection.createIndex(keys, options)

语法中 Key 值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。

1
>db.col.createIndex({"title":1})

createIndex() 方法中你也可以设置使用多个字段创建索引(关系型数据库中称作复合索引)。

1
>db.col.createIndex({"title":1,"description":-1})

createIndex() 接收可选参数,可选参数列表如下:

Parameter Type Description
background Boolean 建索引过程会阻塞其它数据库操作,background 可指定以后台方式创建索引,即增加 “background” 可选参数。 “background” 默认值为false
unique Boolean 建立的索引是否唯一。指定为 true 创建唯一索引。默认值为false.
name string 索引的名称。如果未指定,MongoDB 的通过连接索引的字段名和排序顺序生成一个索引名称。
dropDups Boolean 3.0+版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false
sparse Boolean 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为 true 的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false.
expireAfterSeconds integer 指定一个以秒为单位的数值,完成 TTL 设定,设定集合的生存时间。
v index version 索引的版本号。默认的索引版本取决于 mongod 创建索引时运行的版本。
weights document 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。
default_language string 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语
language_override string 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的 language,默认值为 language.

MongoDB 聚合操作

MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似 sql 语句中的 count(*)。

管道

整个聚合运算过程称为管道,它是由多个步骤组成,每个管道

  • 接受一系列文档(原始数据);
  • 每个步骤对这些文档进行一系列运算;
  • 结果文档输出给下一个步骤;

聚合操作的基本格式

1
2
3
pipeline = [$stage1, $stage1, ..., $stageN];

db.<集合>.aggregate(pipeline, {options});

聚合步骤

步骤 作用 SQL 等价运算符
$match 过滤 WHERE
$project 投影 AS
$sort 排序 ORDER BY
$group 分组 GROUP BY
$skip / $limit 结果限制 SKIP / LIMIT
$lookup 左外连接 LEFT OUTER JOIN
$unwind 展开数组 N/A
$graphLookup 图搜索 N/A
$facet / $bucket 分面搜索 N/A

【示例】

1
2
3
4
5
> db.collection.insertMany([{"title":"MongoDB Overview","description":"MongoDB is no sql database","by_user":"collection","tagsr":["mongodb","database","NoSQL"],"likes":"100"},{"title":"NoSQL Overview","description":"No sql database is very fast","by_user":"collection","tagsr":["mongodb","database","NoSQL"],"likes":"10"},{"title":"Neo4j Overview","description":"Neo4j is no sql database","by_user":"Neo4j","tagsr":["neo4j","database","NoSQL"],"likes":"750"}])
> db.collection.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}])
{ "_id" : null, "num_tutorial" : 3 }
{ "_id" : "Neo4j", "num_tutorial" : 1 }
{ "_id" : "collection", "num_tutorial" : 2 }

下表展示了一些聚合的表达式:

表达式 描述 实例
$sum 计算总和。 db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}])
$avg 计算平均值 db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}])
$min 获取集合中所有文档对应值得最小值。 db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}])
$max 获取集合中所有文档对应值得最大值。 db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}])
$push 在结果文档中插入值到一个数组中。 db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}])
$addToSet 在结果文档中插入值到一个数组中,但不创建副本。 db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}])
$first 根据资源文档的排序获取第一个文档数据。 db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}])
$last 根据资源文档的排序获取最后一个文档数据 db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}])

参考资料

Spring 依赖查找

依赖查找是主动或手动的依赖查找方式,通常需要依赖容器或标准 API 实现

IoC 依赖查找大致可以分为以下几类:

  • 根据 Bean 名称查找
  • 根据 Bean 类型查找
  • 根据 Bean 名称 + 类型查找
  • 根据 Java 注解查找

此外,根据查找的 Bean 对象是单一或集合对象,是否需要延迟查找等特定常见,有相应不同的 API。

单一类型依赖查找

单一类型依赖查找接口- BeanFactory

  • 根据 Bean 名称查找
    • getBean(String)
    • Spring 2.5 覆盖默认参数:getBean(String,Object...)
  • 根据 Bean 类型查找
    • Bean 实时查找
      • Spring 3.0 getBean(Class)
      • Spring 4.1 覆盖默认参数:getBean(Class,Object...)
    • Spring 5.1 Bean 延迟查找
      • getBeanProvider(Class)
      • getBeanProvider(ResolvableType)
  • 根据 Bean 名称 + 类型查找:getBean(String,Class)

集合类型依赖查找

集合类型依赖查找接口- ListableBeanFactory

  • 根据 Bean 类型查找

    • 获取同类型 Bean 名称列表
      • getBeanNamesForType(Class)
      • Spring 4.2 getBeanNamesForType(ResolvableType)
    • 获取同类型 Bean 实例列表
      • getBeansOfType(Class) 以及重载方法
  • 通过注解类型查找

    • Spring 3.0 获取标注类型 Bean 名称列表

      • getBeanNamesForAnnotation(Class<? extends Annotation>)
    • Spring 3.0 获取标注类型 Bean 实例列表

      • getBeansWithAnnotation(Class<? extends Annotation>)
    • Spring 3.0 获取指定名称+ 标注类型 Bean 实例

      • findAnnotationOnBean(String,Class<? extends Annotation>)

层次性依赖查找

层次性依赖查找接口- HierarchicalBeanFactory

  • 双亲 BeanFactorygetParentBeanFactory()
  • 层次性查找
    • 根据 Bean 名称查找
      • 基于 containsLocalBean 方法实现
    • 根据 Bean 类型查找实例列表
      • 单一类型:BeanFactoryUtils#beanOfType
      • 集合类型:BeanFactoryUtils#beansOfTypeIncludingAncestors
    • 根据 Java 注解查找名称列表
      • BeanFactoryUtils#beanNamesForTypeIncludingAncestors

延迟依赖查找

Bean 延迟依赖查找接口

  • org.springframework.beans.factory.ObjectFactory
  • org.springframework.beans.factory.ObjectProvider(Spring 5 对 Java 8 特性扩展)
  • 函数式接口
    • getIfAvailable(Supplier)
    • ifAvailable(Consumer)
  • Stream 扩展- stream()

安全依赖查找

依赖查找类型 代表实现 是否安全
单一类型查找 BeanFactory#getBean
ObjectFactory#getObject
ObjectProvider#getIfAvailable
集合类型查找 ListableBeanFactory#getBeansOfType
ObjectProvider#stream

注意:层次性依赖查找的安全性取决于其扩展的单一或集合类型的 BeanFactory 接口

内建可查找的依赖

AbstractApplicationContext 内建可查找的依赖

Bean 名称 Bean 实例使用场景
environment Environment 对象 外部化配置以及 Profiles
systemProperties java.util.Properties 对象 Java 系统属性
systemEnvironment java.util.Map 对象 操作系统环境变量
messageSource MessageSource 对象 国际化文案
lifecycleProcessor LifecycleProcessor 对象 Lifecycle Bean 处理器
applicationEventMulticaster ApplicationEventMulticaster 对象 Spring 事件广播器

注解驱动 Spring 应用上下文内建可查找的依赖(部分)

Bean 名称 Bean 实例 使用场景
org.springframework.context.annotation.internalConfigurationAnnotationProcessor ConfigurationClassPostProcessor 对象 处理 Spring 配置类
org.springframework.context.annotation.internalAutowiredAnnotationProcessor AutowiredAnnotationBeanPostProcessor 对象 处理@Autowired 以及@Value 注解
org.springframework.context.annotation.internalCommonAnnotationProcessor CommonAnnotationBeanPostProcessor 对象 (条件激活)处理 JSR-250 注解,如@PostConstruct 等
org.springframework.context.event.internalEventListenerProcessor EventListenerMethodProcessor 对象 处理标注@EventListener 的 Spring 事件监听方法
org.springframework.context.event.internalEventListenerFactory DefaultEventListenerFactory 对象 @EventListener 事件监听方法适配为 ApplicationListener
org.springframework.context.annotation.internalPersistenceAnnotationProcessor PersistenceAnnotationBeanPostProcessor 对象 (条件激活)处理 JPA 注解场景

依赖查找中的经典异常

BeansException 子类型

异常类型 触发条件(举例) 场景举例
NoSuchBeanDefinitionException 当查找 Bean 不存在于 IoC 容器时 BeanFactory#getBeanObjectFactory#getObject
NoUniqueBeanDefinitionException 类型依赖查找时,IoC 容器存在多个 Bean 实例 BeanFactory#getBean(Class)
BeanInstantiationException 当 Bean 所对应的类型非具体类时 BeanFactory#getBean
BeanCreationException 当 Bean 初始化过程中 Bean 初始化方法执行异常时
BeanDefinitionStoreException BeanDefinition 配置元信息非法时 XML 配置资源无法打开时

参考资料

Spring IoC

IoC 简介

IoC 是什么

IoC控制反转(Inversion of Control,缩写为 IoC)。IoC 又称为依赖倒置原则(设计模式六大原则之一),它的要点在于:程序要依赖于抽象接口,不要依赖于具体实现。它的作用就是用于降低代码间的耦合度

IoC 的实现方式有两种:

  • 依赖注入(Dependency Injection,简称 DI):不通过 new() 的方式在类内部创建依赖类对象,而是将依赖的类对象在外部创建好之后,通过构造函数、函数参数等方式传递(或注入)给类使用。
  • 依赖查找(Dependency Lookup):容器中的受控对象通过容器的 API 来查找自己所依赖的资源和协作对象。

理解 Ioc 的关键是要明确两个要点:

  • 谁控制谁,控制什么:传统 Java SE 程序设计,我们直接在对象内部通过 new 进行创建对象,是程序主动去创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 Ioc 容器来控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。
  • 为何是反转,哪些方面反转了:有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

IoC 能做什么

IoC 不是一种技术,而是编程思想,一个重要的面向对象编程的法则,它能指导我们如何设计出松耦合、更优良的程序。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器进行注入组合对象,所以对象与对象之间是松散耦合,这样也方便测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

其实 IoC 对编程带来的最大改变不是从代码上,而是从思想上,发生了“主从换位”的变化。应用程序原本是老大,要获取什么资源都是主动出击,但是在 IoC/DI 思想中,应用程序就变成被动的了,被动的等待 IoC 容器来创建并注入它所需要的资源了。

IoC 很好的体现了面向对象设计法则之一—— 好莱坞法则:“别找我们,我们找你”;即由 IoC 容器帮对象找相应的依赖对象并注入,而不是由对象主动去找。

IoC 和 DI

其实它们是同一个概念的不同角度描述,由于控制反转概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护对象关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:“依赖注入”,相对 IoC 而言,“依赖注入”明确描述了“被注入对象依赖 IoC 容器配置依赖对象”。

注:如果想要更加深入的了解 IoC 和 DI,请参考大师级人物 Martin Fowler 的一篇经典文章 Inversion of Control Containers and the Dependency Injection pattern

IoC 容器

IoC 容器就是具有依赖注入功能的容器。IoC 容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。应用程序无需直接在代码中 new 相关的对象,应用程序由 IoC 容器进行组装。在 Spring 中 BeanFactory 是 IoC 容器的实际代表者。

Spring IoC 容器如何知道哪些是它管理的对象呢?这就需要配置文件,Spring IoC 容器通过读取配置文件中的配置元数据,通过元数据对应用中的各个对象进行实例化及装配。一般使用基于 xml 配置文件进行配置元数据,而且 Spring 与配置文件完全解耦的,可以使用其他任何可能的方式进行配置元数据,比如注解、基于 java 文件的、基于属性文件的配置都可以。

Bean

JavaBean 是一种 JAVA 语言写成的可重用组件。为写成 JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 对外部通过提供 getter / setter 方法来访问其成员。

由 IoC 容器管理的那些组成你应用程序的对象我们就叫它 Bean。Bean 就是由 Spring 容器初始化、装配及管理的对象,除此之外,bean 就与应用程序中的其他对象没有什么区别了。那 IoC 怎样确定如何实例化 Bean、管理 Bean 之间的依赖关系以及管理 Bean 呢?这就需要配置元数据,在 Spring 中由 BeanDefinition 代表,后边会详细介绍,配置元数据指定如何实例化 Bean、如何组装 Bean 等。

Spring IoC

Spring IoC 容器中的对象仅通过构造函数参数、工厂方法的参数或在对象实例被构造或从工厂方法返回后设置的属性来定义它们的依赖关系(即与它们一起工作的其他对象)。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身通过使用类的直接构造或诸如服务定位器模式之类的机制来控制其依赖关系的实例化或位置的逆过程(因此称为控制反转)。

org.springframework.beansorg.springframework.context 是 IoC 容器的基础。

IoC 容器

在 Spring 中,有两种 IoC 容器:BeanFactoryApplicationContext

  • BeanFactory:**BeanFactory 是 Spring 基础 IoC 容器**。BeanFactory 提供了 Spring 容器的配置框架和基本功能。
  • ApplicationContext:**ApplicationContext 是具备应用特性的 BeanFactory 的子接口**。它还扩展了其他一些接口,以支持更丰富的功能,如:国际化、访问资源、事件机制、更方便的支持 AOP、在 web 应用中指定应用层上下文等。

实际开发中,更推荐使用 ApplicationContext 作为 IoC 容器,因为它的功能远多于 BeanFactory

org.springframework.context.ApplicationContext 接口代表 Spring IoC 容器,负责实例化、配置和组装 bean。容器通过读取配置元数据来获取关于要实例化、配置和组装哪些对象的指令。配置元数据以 XML、Java 注释或 Java 代码表示。它允许您表达组成应用程序的对象以及这些对象之间丰富的相互依赖关系。

Spring 提供了 ApplicationContext 接口的几个实现,例如:

1
BeanFactory beanFactory = new ClassPathXmlApplicationContext("classpath.xml");
1
BeanFactory beanFactory = new FileSystemXmlApplicationContext("fileSystemConfig.xml");

在大多数应用场景中,不需要显式通过用户代码来实例化 Spring IoC 容器的一个或多个实例。

下图显示了 Spring IoC 容器的工作步骤

img

使用 IoC 容器可分为三步骤:

  1. 配置元数据:需要配置一些元数据来告诉 Spring,你希望容器如何工作,具体来说,就是如何去初始化、配置、管理 JavaBean 对象。
  2. 实例化容器:由 IoC 容器解析配置的元数据。IoC 容器的 Bean Reader 读取并解析配置文件,根据定义生成 BeanDefinition 配置元数据对象,IoC 容器根据 BeanDefinition 进行实例化、配置及组装 Bean。
  3. 使用容器:由客户端实例化容器,获取需要的 Bean。

配置元数据

元数据(Metadata)又称中介数据、中继数据,为描述数据的数据(data about data),主要是描述数据属性(property)的信息。

配置元数据的方式:

  • 基于 xml 配置:Spring 的传统配置方式。通常是在顶级元素 <beans> 中通过 <bean>元素配置元数据。这种方式的缺点是:如果 JavaBean 过多,则产生的配置文件足以让你眼花缭乱。
  • **基于注解配置**:Spring 2.5 引入了对基于注解的配置元数据的支持。可以大大简化你的配置。
  • **基于 Java 配置**:从 Spring 3.0 开始,Spring 支持使用 Java 代码来配置元数据。通常是在 @Configuration 修饰的类中通过 @Bean 指定实例化 Bean 的方法。更多详情,可以参阅 @Configuration@Bean@Import@DependsOn 注释。

这些 bean 定义对应于构成应用程序的实际对象。例如:定义服务层对象、数据访问对象 (DAO)、表示对象(如 Struts Action 实例)、基础设施对象(如 Hibernate SessionFactories、JMS 队列等)。通常,不会在容器中配置细粒度的域对象,因为创建和加载域对象通常是 DAO 和业务逻辑的责任。但是,可以使用 Spring 与 AspectJ 的集成来配置在 IoC 容器控制之外创建的对象。

以下示例显示了基于 XML 的配置元数据的基本结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!--id 属性用于唯一标识单个 bean 定义-->
<!--class 属性用于指明 bean 类型的完全限定名-->
<bean id="..." class="...">
<!-- 这里配置 Bean 的属性 -->
</bean>

<bean id="..." class="...">
<!-- 这里配置 Bean 的属性 -->
</bean>

<!-- 更多的 Bean 定义 -->

</beans>

实例化容器

可以通过为 ApplicationContext 的构造函数指定外部资源路径,来加载配置元数据。

1
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

以下示例显示了服务层对象 (services.xml) 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- services -->

<bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="itemDao" ref="itemDao"/>
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<!-- more bean definitions for services go here -->

</beans>

以下示例显示了数据访问对象 (daos.xml) 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
<!-- additional collaborators and configuration for this bean go here -->
</bean>

<!-- more bean definitions for data access objects go here -->

</beans>

上面的示例中,服务层由 PetStoreServiceImpl 类和类型为 JpaAccountDaoJpaItemDao 的两个数据访问对象(基于 JPA 对象关系映射标准)组成。 property name 元素指的是 JavaBean 属性的名称,ref 元素指的是另一个 bean 定义的名称。 idref 元素之间的这种联系表达了协作对象之间的依赖关系。

Spring 支持通过多个 xml 文件来定义 Bean,每个单独的 XML 配置文件都代表架构中的一个逻辑层或模块。可以使用 ApplicationContext 构造函数从所有这些 XML 片段加载 bean 定义。或者,使用 <import/> 元素从另一个或多个文件加载 bean 定义。如下所示:

1
2
3
4
5
6
7
8
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>

<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>

在上面的示例中,外部 bean 定义从三个文件加载:services.xmlmessageSource.xmlthemeSource.xmlservices.xml 文件必须和当前 xml 文件位于同一目录或类路径位置;而 messageSource.xmlthemeSource.xml 必须位于当前文件所在目录的子目录 resources 下。/resources/ 会被忽略。但是,鉴于这些路径是相对的,最好不要使用 /。根据 Spring Schema,被导入文件的内容,包括顶级 <beans/> 元素,必须是有效的 XML bean 定义。

注意:

可以,但不推荐使用相对 “../” 路径来引用父目录中的文件。这样做会创建对当前应用程序之外的文件的依赖。特别是,不建议将此引用用于 classpath:URL(例如, classpath:../services.xml),其中运行时解析过程会选择“最近的”类路径根,然后查看其父目录。类路径配置更改可能会导致选择不同的、不正确的目录。

可以使用完全限定的资源位置而不是相对路径:例如,file:C:/config/services.xmlclasspath:/config/services.xml。建议为此类绝对路径保留一定的间接性  —  例如,通过 “${...}” 占位符来引用运行时指定 的 JVM 参数。

命名空间本身提供了导入指令功能。 Spring 提供的一系列 XML 命名空间中提供了除了普通 bean 定义之外的更多配置特性  —  例如,contextutil 命名空间。

使用容器

ApplicationContext 能够维护不同 bean 及其依赖项的注册表。通过使用方法 T getBean(String name, Class T requiredType),可以检索并获取 bean 的实例

ApplicationContext 允许读取 bean 定义并访问它们,如以下示例所示:

1
2
3
4
5
6
7
8
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// use configured instance
List<String> userList = service.getUsernameList();

最灵活的变体是 GenericApplicationContext 结合阅读器委托  —  例如,结合 XML 文件的 XmlBeanDefinitionReader,如下例所示:

1
2
3
GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();

可以在同一个 ApplicationContext 上混合和匹配此类读取器委托,从不同的配置源读取 bean 定义。

然后,可以使用 getBean 检索 bean 的实例。 ApplicationContext 接口还有一些其他方法用于检索 bean,但理想情况下,应用程序代码不应该使用它们。实际上,应用程序代码根本不应该调用 getBean() 方法,因此根本不依赖 Spring API。例如,Spring 与 Web 框架的集成为各种 Web 框架组件(例如控制器和 JSF 管理的 bean)提供了依赖注入,让您可以通过元数据(例如自动装配注释)声明对特定 bean 的依赖。

IoC 依赖来源

自定义 Bean

容器内建 Bean 对象

容器内建依赖

IoC 配置元数据

IoC 容器的配置有三种方式:

  • 基于 xml 配置
  • 基于 properties 配置
  • 基于注解配置
  • 基于 Java 配置

作为 Spring 传统的配置方式,xml 配置方式一般为大家所熟知。

如果厌倦了 xml 配置,Spring 也提供了注解配置方式或 Java 配置方式来简化配置。

本文,将对 Java 配置 IoC 容器做详细的介绍。

Xml 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<import resource="resource1.xml" />
<bean id="bean1" class=""></bean>
<bean id="bean2" class=""></bean>
<bean name="bean2" class=""></bean>

<alias alias="bean3" name="bean2"/>
<import resource="resource2.xml" />
</beans>

标签说明:

  • <beans> 是 Spring 配置文件的根节点。
  • <bean> 用来定义一个 JavaBean。id 属性是它的标识,在文件中必须唯一;class 属性是它关联的类。
  • <alias> 用来定义 Bean 的别名。
  • <import> 用来导入其他配置文件的 Bean 定义。这是为了加载多个配置文件,当然也可以把这些配置文件构造为一个数组(new String[] {“config1.xml”, config2.xml})传给 ApplicationContext 实现类进行加载多个配置文件,那一个更适合由用户决定;这两种方式都是通过调用 Bean Definition Reader 读取 Bean 定义,内部实现没有任何区别。<import> 标签可以放在 <beans> 下的任何位置,没有顺序关系。

实例化容器

实例化容器的过程:
定位资源(XML 配置文件)
读取配置信息(Resource)
转化为 Spring 可识别的数据形式(BeanDefinition)

1
2
ApplicationContext context =
new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});

组合 xml 配置文件
配置的 Bean 功能各不相同,都放在一个 xml 文件中,不便管理。
Java 设计模式讲究职责单一原则。配置其实也是如此,功能不同的 JavaBean 应该被组织在不同的 xml 文件中。然后使用 import 标签把它们统一导入。

1
2
<import resource="classpath:spring/applicationContext.xml"/>
<import resource="/WEB-INF/spring/service.xml"/>

使用容器

使用容器的方式就是通过getBean获取 IoC 容器中的 JavaBean。
Spring 也有其他方法去获得 JavaBean,但是 Spring 并不推荐其他方式。

1
2
3
4
5
6
7
// create and configure beans
ApplicationContext context =
new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});
// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// use configured instance
List<String> userList = service.getUsernameList();

注解配置

Spring2.5 引入了注解。
于是,一个问题产生了:使用注解方式注入 JavaBean 是不是一定完爆 xml 方式?
未必。正所谓,仁者见仁智者见智。任何事物都有其优缺点,看你如何取舍。来看看注解的优缺点:
优点:大大减少了配置,并且可以使配置更加精细——类,方法,字段都可以用注解去标记。
缺点:使用注解,不可避免产生了侵入式编程,也产生了一些问题。

  • 你需要将注解加入你的源码并编译它;

  • 注解往往比较分散,不易管控。

注:spring 中,先进行注解注入,然后才是 xml 注入,因此如果注入的目标相同,后者会覆盖前者。

启动注解

Spring 默认是不启用注解的。如果想使用注解,需要先在 xml 中启动注解。
启动方式:在 xml 中加入一个标签,很简单吧。

1
<context:annotation-config/>

注:<context:annotation-config/> 只会检索定义它的上下文。什么意思呢?就是说,如果你
为 DispatcherServlet 指定了一个WebApplicationContext,那么它只在 controller 中查找@Autowired注解,而不会检查其它的路径。

@Required

@Required 注解只能用于修饰 bean 属性的 setter 方法。受影响的 bean 属性必须在配置时被填充在 xml 配置文件中,否则容器将抛出BeanInitializationException

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
public class AnnotationRequired {
private String name;
private String sex;

public String getName() {
return name;
}

/**
* @Required 注解用于bean属性的setter方法并且它指示,受影响的bean属性必须在配置时被填充在xml配置文件中,
* 否则容器将抛出BeanInitializationException。
*/
@Required
public void setName(String name) {
this.name = name;
}

public String getSex() {
return sex;
}

public void setSex(String sex) {
this.sex = sex;
}
}

@Autowired

@Autowired注解可用于修饰属性、setter 方法、构造方法。

@Autowired 注入过程

  • 元信息解析
  • 依赖查找
  • 依赖注入(字段、方法)

注:@Autowired注解也可用于修饰构造方法,但如果类中只有默认构造方法,则没有必要。如果有多个构造器,至少应该修饰一个,来告诉容器哪一个必须使用。

可以使用 JSR330 的注解@Inject来替代@Autowired

范例

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
public class AnnotationAutowired {
private static final Logger log = LoggerFactory.getLogger(AnnotationRequired.class);

@Autowired
private Apple fieldA;

private Banana fieldB;

private Orange fieldC;

public Apple getFieldA() {
return fieldA;
}

public void setFieldA(Apple fieldA) {
this.fieldA = fieldA;
}

public Banana getFieldB() {
return fieldB;
}

@Autowired
public void setFieldB(Banana fieldB) {
this.fieldB = fieldB;
}

public Orange getFieldC() {
return fieldC;
}

public void setFieldC(Orange fieldC) {
this.fieldC = fieldC;
}

public AnnotationAutowired() {}

@Autowired
public AnnotationAutowired(Orange fieldC) {
this.fieldC = fieldC;
}

public static void main(String[] args) throws Exception {
AbstractApplicationContext ctx =
new ClassPathXmlApplicationContext("spring/spring-annotation.xml");

AnnotationAutowired annotationAutowired =
(AnnotationAutowired) ctx.getBean("annotationAutowired");
log.debug("fieldA: {}, fieldB:{}, fieldC:{}", annotationAutowired.getFieldA().getName(),
annotationAutowired.getFieldB().getName(),
annotationAutowired.getFieldC().getName());
ctx.close();
}
}

xml 中的配置

1
2
3
4
5
<!-- 测试@Autowired -->
<bean id="apple" class="org.zp.notes.spring.beans.annotation.sample.Apple"/>
<bean id="potato" class="org.zp.notes.spring.beans.annotation.sample.Banana"/>
<bean id="tomato" class="org.zp.notes.spring.beans.annotation.sample.Orange"/>
<bean id="annotationAutowired" class="org.zp.notes.spring.beans.annotation.sample.AnnotationAutowired"/>

@Qualifier

@Autowired注解中,提到了如果发现有多个候选的 bean 都符合修饰类型,Spring 就会抓瞎了。

那么,如何解决这个问题。

可以通过@Qualifier指定 bean 名称来锁定真正需要的那个 bean。

范例

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
public class AnnotationQualifier {
private static final Logger log = LoggerFactory.getLogger(AnnotationQualifier.class);

@Autowired
@Qualifier("dog") /** 去除这行,会报异常 */
Animal dog;

Animal cat;

public Animal getDog() {
return dog;
}

public void setDog(Animal dog) {
this.dog = dog;
}

public Animal getCat() {
return cat;
}

@Autowired
public void setCat(@Qualifier("cat") Animal cat) {
this.cat = cat;
}

public static void main(String[] args) throws Exception {
AbstractApplicationContext ctx =
new ClassPathXmlApplicationContext("spring/spring-annotation.xml");

AnnotationQualifier annotationQualifier =
(AnnotationQualifier) ctx.getBean("annotationQualifier");

log.debug("Dog name: {}", annotationQualifier.getDog().getName());
log.debug("Cat name: {}", annotationQualifier.getCat().getName());
ctx.close();
}
}

abstract class Animal {
public String getName() {
return null;
}
}

class Dog extends Animal {
public String getName() {
return "狗";
}
}

class Cat extends Animal {
public String getName() {
return "猫";
}
}

xml 中的配置

1
2
3
4
<!-- 测试@Qualifier -->
<bean id="dog" class="org.zp.notes.spring.beans.annotation.sample.Dog"/>
<bean id="cat" class="org.zp.notes.spring.beans.annotation.sample.Cat"/>
<bean id="annotationQualifier" class="org.zp.notes.spring.beans.annotation.sample.AnnotationQualifier"/>

@Resource

Spring 支持 JSP250 规定的注解@Resource。这个注解根据指定的名称来注入 bean。

如果没有为@Resource指定名称,它会像@Autowired一样按照类型去寻找匹配。

在 Spring 中,由CommonAnnotationBeanPostProcessor来处理@Resource注解。

范例

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
public class AnnotationResource {
private static final Logger log = LoggerFactory.getLogger(AnnotationResource.class);

@Resource(name = "flower")
Plant flower;

@Resource(name = "tree")
Plant tree;

public Plant getFlower() {
return flower;
}

public void setFlower(Plant flower) {
this.flower = flower;
}

public Plant getTree() {
return tree;
}

public void setTree(Plant tree) {
this.tree = tree;
}

public static void main(String[] args) throws Exception {
AbstractApplicationContext ctx =
new ClassPathXmlApplicationContext("spring/spring-annotation.xml");

AnnotationResource annotationResource =
(AnnotationResource) ctx.getBean("annotationResource");
log.debug("type: {}, name: {}", annotationResource.getFlower().getClass(), annotationResource.getFlower().getName());
log.debug("type: {}, name: {}", annotationResource.getTree().getClass(), annotationResource.getTree().getName());
ctx.close();
}
}

xml 的配置

1
2
3
4
<!-- 测试@Resource -->
<bean id="flower" class="org.zp.notes.spring.beans.annotation.sample.Flower"/>
<bean id="tree" class="org.zp.notes.spring.beans.annotation.sample.Tree"/>
<bean id="annotationResource" class="org.zp.notes.spring.beans.annotation.sample.AnnotationResource"/>

@PostConstruct@PreDestroy

@PostConstruct@PreDestroy 是 JSR 250 规定的用于生命周期的注解。

从其名号就可以看出,一个是在构造之后调用的方法,一个是销毁之前调用的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class AnnotationPostConstructAndPreDestroy {
private static final Logger log = LoggerFactory.getLogger(AnnotationPostConstructAndPreDestroy.class);

@PostConstruct
public void init() {
log.debug("call @PostConstruct method");
}

@PreDestroy
public void destroy() {
log.debug("call @PreDestroy method");
}
}

@Inject

从 Spring3.0 开始,Spring 支持 JSR 330 标准注解(依赖注入)。

注:如果要使用 JSR 330 注解,需要使用外部 jar 包。

若你使用 maven 管理 jar 包,只需要添加依赖到 pom.xml 即可:

1
2
3
4
5
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>

@Inject@Autowired 一样,可以修饰属性、setter 方法、构造方法。

范例

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
public class AnnotationInject {
private static final Logger log = LoggerFactory.getLogger(AnnotationInject.class);
@Inject
Apple fieldA;

Banana fieldB;

Orange fieldC;

public Apple getFieldA() {
return fieldA;
}

public void setFieldA(Apple fieldA) {
this.fieldA = fieldA;
}

public Banana getFieldB() {
return fieldB;
}

@Inject
public void setFieldB(Banana fieldB) {
this.fieldB = fieldB;
}

public Orange getFieldC() {
return fieldC;
}

public AnnotationInject() {}

@Inject
public AnnotationInject(Orange fieldC) {
this.fieldC = fieldC;
}

public static void main(String[] args) throws Exception {
AbstractApplicationContext ctx =
new ClassPathXmlApplicationContext("spring/spring-annotation.xml");
AnnotationInject annotationInject = (AnnotationInject) ctx.getBean("annotationInject");

log.debug("type: {}, name: {}", annotationInject.getFieldA().getClass(),
annotationInject.getFieldA().getName());

log.debug("type: {}, name: {}", annotationInject.getFieldB().getClass(),
annotationInject.getFieldB().getName());

log.debug("type: {}, name: {}", annotationInject.getFieldC().getClass(),
annotationInject.getFieldC().getName());

ctx.close();
}
}

Java 配置

基于 Java 配置 Spring IoC 容器,实际上是Spring 允许用户定义一个类,在这个类中去管理 IoC 容器的配置

为了让 Spring 识别这个定义类为一个 Spring 配置类,需要用到两个注解:@Configuration@Bean

如果你熟悉 Spring 的 xml 配置方式,你可以将@Configuration等价于<beans>标签;将@Bean等价于<bean>标签。

@Bean

@Bean 的修饰目标只能是方法或注解。

@Bean 只能定义在 @Configuration@Component 注解修饰的类中。

声明一个 bean

此外,@Configuration 类允许在同一个类中通过@Bean 定义内部 bean 依赖。

声明一个 bean,只需要在 bean 属性的 set 方法上标注@Bean 即可。

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
@Configuration
public class AnnotationConfiguration {
private static final Logger log = LoggerFactory.getLogger(JavaComponentScan.class);

@Bean
public Job getPolice() {
return new Police();
}

public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AnnotationConfiguration.class);
ctx.scan("org.zp.notes.spring.beans");
ctx.refresh();
Job job = (Job) ctx.getBean("police");
log.debug("job: {}, work: {}", job.getClass(), job.work());
}
}

public interface Job {
String work();
}

@Component("police")
public class Police implements Job {
@Override
public String work() {
return "抓罪犯";
}
}

这等价于配置

1
2
3
<beans>
<bean id="police" class="org.zp.notes.spring.ioc.sample.job.Police"/>
</beans>

@Bean 注解用来表明一个方法实例化、配置合初始化一个被 Spring IoC 容器管理的新对象。

如果你熟悉 Spring 的 xml 配置,你可以将@Bean 视为等价于<beans>标签。

@Bean 注解可以用于任何的 Spring @Component bean,然而,通常被用于@Configuration bean。

@Configuration

@Configuration 是一个类级别的注解,用来标记被修饰类的对象是一个BeanDefinition

@Configuration 声明 bean 是通过被 @Bean 修饰的公共方法。此外,@Configuration 允许在同一个类中通过 @Bean 定义内部 bean 依赖。

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}

这等价于配置

1
2
3
<beans>
<bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

AnnotationConfigApplicationContext 实例化 IoC 容器。

依赖解决过程

容器执行 bean 依赖解析如下:

  • ApplicationContext 使用配置元数据创建和初始化 Bean。配置元数据可以由 XML、Java 代码或注解指定。
  • 对于每个 bean,其依赖关系以属性、构造函数参数或静态工厂方法的参数的形式表示。这些依赖项在实际创建 bean 时提供给 bean。
  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个 bean 的引用。
  • 作为值的每个属性或构造函数参数都从其指定格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring 可以将以字符串格式提供的值转换为所有内置类型,例如 int、long、String、boolean 等。

Spring 容器在创建容器时验证每个 bean 的配置。但是,在实际创建 bean 之前,不会设置 bean 属性本身。在创建容器时会创建 singleton 型的实例并设置为默认的 Bean。否则,只有在请求时才会创建 bean。

需注意:构造器注入,可能会导致无法解决循环依赖问题。

例如:A 类通过构造器注入需要 B 类的实例,B 类通过构造器注入需要 A 类的实例。Spring IoC 容器会在运行时检测到此循环引用,并抛出 BeanCurrentlyInCreationException

一种解决方案是使用 setter 方法注入替代构造器注入。

另一种解决方案是:bean A 和 bean B 之间的循环依赖关系,强制其中一个 bean 在完全初始化之前注入另一个 bean(典型的先有鸡还是先有蛋的场景)。

Spring 会在容器加载时检测配置问题,例如引用不存在的 bean 或循环依赖。在实际创建 bean 时,Spring 会尽可能晚地设置属性并解析依赖关系。这意味着,如果在创建该对象或其依赖项之一时出现问题,则正确加载的 Spring 容器稍后可以在您请求对象时生成异常  —  例如,bean 由于丢失或无效而引发异常。某些配置问题的这种潜在的延迟可见性是默认情况下 ApplicationContext 实现预实例化单例 bean 的原因。以在实际需要之前创建这些 bean 的一些前期时间和内存为代价,您会在创建 ApplicationContext 时发现配置问题,而不是稍后。您仍然可以覆盖此默认行为,以便单例 bean 延迟初始化,而不是急切地预先实例化。

最佳实践

singleton 的 Bean 如何注入 prototype 的 Bean

Spring 创建的 Bean 默认是单例的,但当 Bean 遇到继承的时候,可能会忽略这一点。

假设有一个 SayService 抽象类,其中维护了一个类型是 ArrayList 的字段 data,用于保存方法处理的中间数据。每次调用 say 方法都会往 data 加入新数据,可以认为 SayService 是有状态,如果 SayService 是单例的话必然会 OOM。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* SayService 是有状态,如果 SayService 是单例的话必然会 OOM
*/
@Slf4j
public abstract class SayService {

List<String> data = new ArrayList<>();

public void say() {
data.add(IntStream.rangeClosed(1, 1000000)
.mapToObj(__ -> "a")
.collect(Collectors.joining("")) + UUID.randomUUID().toString());
log.info("I'm {} size:{}", this, data.size());
}

}

但实际开发的时候,开发同学没有过多思考就把 SayHello 和 SayBye 类加上了 @Service 注解,让它们成为了 Bean,也没有考虑到父类是有状态的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@Slf4j
public class SayBye extends SayService {

@Override
public void say() {
super.say();
log.info("bye");
}

}

@Service
@Slf4j
public class SayHello extends SayService {

@Override
public void say() {
super.say();
log.info("hello");
}

}

在为类标记上 @Service 注解把类型交由容器管理前,首先评估一下类是否有状态,然后为 Bean 设置合适的 Scope。

调用代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Slf4j
@RestController
@RequestMapping("beansingletonandorder")
public class BeanSingletonAndOrderController {

@Autowired
List<SayService> sayServiceList;
@Autowired
private ApplicationContext applicationContext;

@GetMapping("test")
public void test() {
log.info("====================");
sayServiceList.forEach(SayService::say);
}

}

可能有人认为,为 SayHello 和 SayBye 两个类都标记了 @Scope 注解,设置了 PROTOTYPE 的生命周期就可以解决上面的问题。

1
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)

但实际上还是有问题。因为@RestController 注解 =@Controller 注解 +@ResponseBody 注解,又因为 @Controller 标记了 @Component 元注解,所以 @RestController 注解其实也是一个 Spring Bean。

Bean 默认是单例的,所以单例的 Controller 注入的 Service 也是一次性创建的,即使 Service 本身标识了 prototype 的范围也没用。

修复方式是,让 Service 以代理方式注入。这样虽然 Controller 本身是单例的,但每次都能从代理获取 Service。这样一来,prototype 范围的配置才能真正生效。

1
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProx)

参考资料

Spring 依赖注入

DI,是 Dependency Injection 的缩写,即依赖注入。依赖注入是 IoC 的最常见形式。依赖注入是手动或自动绑定的方式,无需依赖特定的容器或 API。

依赖注入 (Dependency Injection,简称 DI) 是一个过程,其中对象仅通过构造函数参数、工厂方法的参数或对象实例在构造或从工厂方法返回。然后容器在创建 bean 时注入这些依赖项。这个过程基本上是 bean 本身的逆过程(因此得名,控制反转),它通过使用类的直接构造或服务定位器模式自行控制其依赖项的实例化或位置。

使用 DI,代码更干净,当对象具有依赖关系时,解耦更有效。对象不查找其依赖项,也不知道依赖项的位置或类别。结果,您的类变得更容易测试,特别是当依赖关系在接口或抽象基类上时,它们允许在单元测试中使用存根或模拟实现。

容器全权负责组件的装配,它会把符合依赖关系的对象通过 JavaBean 属性或者构造函数传递给需要的对象

DI 是组件之间依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

理解 DI 的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:

  • 谁依赖于谁:当然是应用程序依赖于 IoC 容器;
  • 为什么需要依赖:应用程序需要 IoC 容器来提供对象需要的外部资源;
  • 谁注入谁:很明显是 IoC 容器注入应用程序某个对象,应用程序依赖的对象;
  • 注入了什么:就是注入某个对象所需要的外部资源(包括对象、资源、常量数据)。

IoC 依赖注入 API

  • 根据 Bean 名称注入
  • 根据 Bean 类型注入
  • 注入容器内建 Bean 对象
  • 注入非 Bean 对象
  • 注入类型
    • 实时注入
    • 延迟注入

依赖注入模式

依赖注入模式可以分为手动注入模式和自动注入模式。

手动注入模式

手动注入模式:配置或者编程的方式,提前安排注入规则

  • XML 资源配置元信息
  • Java 注解配置元信息
  • API 配置元信息

自动注入模式

自动注入模式即自动装配。自动装配(Autowiring)是指 Spring 容器可以自动装配 Bean 之间的关系。Spring 可以通过检查 ApplicationContext 的内容,自动解析合作者(其他 Bean)。

  • 自动装配可以显著减少属性或构造函数参数的配置。
  • 随着对象的发展,自动装配可以更新配置。

注:由于自动装配存在一些限制和不足,官方不推荐使用。

自动装配策略

当使用基于 XML 的配置元数据时,可以使用 <bean/> 元素的 autowire 属性为 Bean 指定自动装配模式。自动装配模式有以下类型:

模式 说明
no 默认值,未激活 Autowiring,需要手动指定依赖注入对象。
byName 根据被注入属性的名称作为 Bean 名称进行依赖查找,并将对象设置到该属性。
byType 根据被注入属性的类型作为依赖类型进行查找,并将对象设置到该属性。
constructor 特殊 byType 类型,用于构造器参数。

org.springframework.beans.factory.config.AutowireCapableBeanFactoryBeanFactory 的子接口,它是 Spring 中用于实现自动装配的容器。

@Autowired 注入过程

  • 元信息解析
  • 依赖查找
  • 依赖注入(字段、方法)

自动装配的限制和不足

自动装配有以下限制和不足:

  • 属性和构造函数参数设置中的显式依赖项会覆盖自动装配。您不能自动装配简单属性,例如基础数据类型、字符串和类(以及此类简单属性的数组)。
  • 自动装配不如显式装配精准。Spring 会尽量避免猜测可能存在歧义的结果。
  • Spring 容器生成文档的工具可能无法解析自动装配信息。
  • 如果同一类型存在多个 Bean 时,自动装配时会存在歧义。容器内的多个 Bean 定义可能与要自动装配的 Setter 方法或构造函数参数指定的类型匹配。对于数组、集合或 Map 实例,这不一定是问题。但是,对于期望单值的依赖项,如果没有唯一的 Bean 定义可用,则会引发异常。

自动装配的限制和不足,详情可以参考官方文档:Limitations and Disadvantages of Autowiring 小节

依赖注入方式

依赖注入有如下方式:

依赖注入方式 配置元数据举例
Setter 方法注入 <proeprty name="user" ref="userBean"/>
构造器注入 <constructor-arg name="user" ref="userBean" />
字段注入 @Autowired User user;
方法注入 @Autowired public void user(User user) { ... }
接口回调注入 class MyBean implements BeanFactoryAware { ... }

构造器注入

  • 手动模式
    • xml 配置元信息
    • 注解配置元信息
    • Java 配置元信息
  • 自动模式
    • constructor

构造器注入是通过容器调用具有多个参数的构造函数来完成的,每个参数代表一个依赖项。调用带有特定参数的静态工厂方法来构造 bean 几乎是等价的,并且本次讨论对构造函数和静态工厂方法的参数进行了类似的处理。

下面是一个构造器注入示例:

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {

// the SimpleMovieLister has a dependency on a MovieFinder
private final MovieFinder movieFinder;

// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// business logic that actually uses the injected MovieFinder is omitted...
}

构造函数参数解析匹配通过使用参数的类型进行。如果 bean 定义的构造函数参数中不存在潜在的歧义,则在 bean 定义中定义构造函数参数的顺序是在实例化 bean 时将这些参数提供给适当构造函数的顺序。

1
2
3
4
5
6
7
8
package x.y;

public class ThingOne {

public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
// ...
}
}

假设 ThingTwo 和 ThingThree 类没有继承关系,则不存在潜在的歧义。因此,以下配置工作正常,您无需在 <constructor-arg/> 元素中显式指定构造函数参数索引或类型。

1
2
3
4
5
6
7
8
9
10
<beans>
<bean id="beanOne" class="x.y.ThingOne">
<constructor-arg ref="beanTwo"/>
<constructor-arg ref="beanThree"/>
</bean>

<bean id="beanTwo" class="x.y.ThingTwo"/>

<bean id="beanThree" class="x.y.ThingThree"/>
</beans>

当引用另一个 bean 时,类型是已知的,并且可以发生匹配(就像前面的示例一样)。当使用简单类型时,例如 <value>true</value> ,Spring 无法确定 value 的类型,因此无法在没有帮助的情况下按类型匹配。考虑以下类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package examples;

public class ExampleBean {

// Number of years to calculate the Ultimate Answer
private final int years;

// The Answer to Life, the Universe, and Everything
private final String ultimateAnswer;

public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}

构造函数参数类型匹配

在上述场景中,如果您使用 type 属性显式指定构造函数参数的类型,则容器可以使用简单类型的类型匹配,如以下示例所示:

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg type="int" value="7500000"/>
<constructor-arg type="java.lang.String" value="42"/>
</bean>

构造函数参数索引匹配

可以使用 index 属性显式指定构造函数参数的索引,如以下示例所示

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg index="0" value="7500000"/>
<constructor-arg index="1" value="42"/>
</bean>

构造函数参数名称匹配

1
2
3
4
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
</bean>

可以使用 @ConstructorProperties 显式命名构造函数参数。

1
2
3
4
5
6
7
8
9
10
11
12
package examples;

public class ExampleBean {

// Fields omitted

@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}

Setter 方法注入

  • 手动模式
    • xml 配置元信息
    • 注解配置元信息
    • Java 配置元信息
  • 自动模式
    • byName
    • byType

Setter 方法注入是通过容器在调用无参数构造函数或无参数静态工厂方法来实例化 bean 后调用 bean 上的 setter 方法来完成的。

以下示例显示了一个只能通过使用纯 setter 注入进行依赖注入的类。

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {

// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;

// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// business logic that actually uses the injected MovieFinder is omitted...
}

在 Spring 中,可以混合使用构造器注入和 setter 方法注入。建议将构造器注入用于强制依赖项;并将 setter 方法注入或配置方法用于可选依赖项。需要注意的是,在 setter 方法上使用 @Required 注解可用于使属性成为必需的依赖项;然而,更建议使用构造器注入来完成这项工作。

字段注入

手动模式(Java 注解配置元信息)

  • @Autowired
  • @Resource
  • @Inject(可选)

方法注入

手动模式(Java 注解配置元信息)

  • @Autowired
  • @Resource
  • @Inject(可选)
  • @Bean

接口回调注入

Aware 系列接口回调

內建接口 说明
BeanFactoryAware 获取 IoC 容器- BeanFactory
ApplicationContextAware 获取 Spring 应用上下文- ApplicationContext 对象
EnvironmentAware 获取 Environment 对象
ResourceLoaderAware 获取资源加载器对象- ResourceLoader
BeanClassLoaderAware 获取加载当前 Bean Class 的 ClassLoader
BeanNameAware 获取当前 Bean 的名称
MessageSourceAware 获取 MessageSource 对象,用于 Spring 国际化
ApplicationEventPublisherAware 获取 ApplicationEventPublishAware 对象,用于 Spring 事件
EmbeddedValueResolverAware 获取 StringValueResolver 对象,用于占位符处理

依赖注入选型

  • 低依赖:构造器注入
  • 多依赖:Setter 方法注入
  • 便利性:字段注入
  • 声明类:方法注入

限定注入和延迟注入

限定注入

  • 使用 @Qualifier 注解限定
    • 通过 Bean 名称限定
    • 通过分组限定
  • 通过 @Qualifier 注解扩展限定
    • 自定义注解:如 Spring Cloud 的 @LoadBalanced

延迟注入

  • 使用 ObjectFactory
  • 使用 ObjectProvider(推荐)

依赖注入数据类型

基础类型

  • 基础数据类型:booleanbytecharshortintfloatlongdouble
  • 标量类型:NumberCharacterBooleanEnumLocaleCharsetCurrencyPropertiesUUID
  • 常规类型:ObjectStringTimeZoneCalendarOptional
  • Spring 类型:ResourceInputSourceFormatter 等。

集合类型

数组类型:基础数据类型、标量类型、常规类型、String 类型的数组

集合类型:

  • CollectionListSet
  • MapProperties

依赖处理过程

入口:DefaultListableBeanFactory#resolveDependency

依赖描述符:DependencyDescriptor

自定义绑定候选对象处理器:AutowireCandidateResolver

@Autowired@Value@javax.inject.Inject 处理器:AutowiredAnnotationBeanPostProcessor

通用注解处理器:CommonAnnotationBeanPostProcessor

  • 注入注解
    • javax.xml.ws.WebServiceRef
    • javax.ejb.EJB
    • javax.annotation.Resources
  • 生命周期注解
    • javax.annotation.PostConstruct
    • javax.annotation.PreDestroy

自定义依赖注入注解:

  • 生命周期处理
    • InstantiationAwareBeanPostProcessor
    • MergedBeanDefinitionPostProcessor
  • 元数据
    • InjectionMetadata
    • InjectionMetadata.InjectedElement

依赖查找 VS. 依赖注入

类型 依赖处理 实现复杂度 代码侵入性 API 依赖性 可读性
依赖查找 主动 相对繁琐 侵入业务逻辑 依赖容器 API 良好
依赖注入 被动 相对便利 低侵入性 不依赖容器 API 一般

参考资料

JavaWeb 之 Servlet 指南

JavaWeb 简介

Web 应用程序

Web,在英语中 web 即表示网页的意思,它用于表示 Internet 主机上供外界访问的资源。

Web 应用程序是一种可以通过 Web 访问的应用程序,程序的最大好处是用户很容易访问应用程序,用户只需要有浏览器即可,不需要再安装其他软件。

Internet 上供外界访问的 Web 资源分为:

  • 静态 web 资源:指 web 页面中供人们浏览的数据始终是不变。常见静态资源文件:html、css、各种图片类型(jpg、png)
  • 动态 web 资源:指 web 页面中供人们浏览的数据是由程序产生的,不同时间点访问 web 页面看到的内容各不相同。常见动态资源技术:JSP/Servlet、ASP、PHP

常见 Web 服务器

Servlet 简介

什么是 Servlet

Servlet(Server Applet),即小服务程序或服务连接器。Servlet 是 Java 编写的服务器端程序,具有独立于平台和协议的特性,主要功能在于交互式地浏览和生成数据,生成动态 Web 内容。

  • 狭义的 Servlet 是指 Java 实现的一个接口。
  • 广义的 Servlet 是指任何实现了这个 Servlet 接口的类。

Servlet 运行于支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下 Servlet 只用来扩展基于 HTTP 协议的 Web 服务器。

Servlet 和 CGI 的区别

Servlet 技术出现之前,Web 主要使用 CGI 技术。它们的区别如下:

  • Servlet 是基于 Java 编写的,处于服务器进程中,他能够通过多线程方式运行 service() 方法,一个实例可以服务于多个请求,而且一般不会销毁;
  • CGI(Common Gateway Interface),即通用网关接口。它会为每个请求产生新的进程,服务完成后销毁,所以效率上低于 Servlet。

Servlet 版本以及主要特性

版本 日期 JAVA EE/JDK 版本 特性
Servlet 4.0 2017 年 10 月 JavaEE 8 HTTP2
Servlet 3.1 2013 年 5 月 JavaEE 7 非阻塞 I/O,HTTP 协议升级机制
Servlet 3.0 2009 年 12 月 JavaEE 6, JavaSE 6 可插拔性,易于开发,异步 Servlet,安全性,文件上传
Servlet 2.5 2005 年 10 月 JavaEE 5, JavaSE 5 依赖 JavaSE 5,支持注解
Servlet 2.4 2003 年 11 月 J2EE 1.4, J2SE 1.3 web.xml 使用 XML Schema
Servlet 2.3 2001 年 8 月 J2EE 1.3, J2SE 1.2 Filter
Servlet 2.2 1999 年 8 月 J2EE 1.2, J2SE 1.2 成为 J2EE 标准
Servlet 2.1 1998 年 11 月 未指定 First official specification, added RequestDispatcher, ServletContext
Servlet 2.0 JDK 1.1 Part of Java Servlet Development Kit 2.0
Servlet 1.0 1997 年 6 月

Servlet 任务

Servlet 执行以下主要任务:

  • 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 applet 或自定义的 HTTP 客户端程序的表单。
  • 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 cookies、媒体类型和浏览器能理解的压缩格式等等。
  • 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,调用 Web 服务,或者直接计算得出对应的响应。
  • 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样的,包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。
  • 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返回的文档类型(例如 HTML),设置 cookies 和缓存参数,以及其他类似的任务。

Servlet 生命周期

img

Servlet 生命周期如下:

  1. 加载 - 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。容器通过类加载器使用 Servlet 类对应的文件加载 servlet;
  2. 初始化 - Servlet 通过调用 init () 方法进行初始化。
  3. 服务 - Servlet 调用 service() 方法来处理客户端的请求。
  4. 销毁 - Servlet 通过调用 destroy() 方法终止(结束)。
  5. 卸载 - Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

Servlet API

Servlet 包

Java Servlet 是运行在带有支持 Java Servlet 规范的解释器的 web 服务器上的 Java 类。

Servlet 可以使用 javax.servletjavax.servlet.http 包创建,它是 Java 企业版的标准组成部分,Java 企业版是支持大型开发项目的 Java 类库的扩展版本。

Java Servlet 就像任何其他的 Java 类一样已经被创建和编译。在您安装 Servlet 包并把它们添加到您的计算机上的 Classpath 类路径中之后,您就可以通过 JDK 的 Java 编译器或任何其他编译器来编译 Servlet。

Servlet 接口

Servlet 接口定义了下面五个方法:

1
2
3
4
5
6
7
8
9
10
11
public interface Servlet {
void init(ServletConfig var1) throws ServletException;

ServletConfig getServletConfig();

void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;

String getServletInfo();

void destroy();
}

init() 方法

init 方法被设计成只调用一次。它在第一次创建 Servlet 时被调用,在后续每次用户请求时不再调用。因此,它是用于一次性初始化,就像 Applet 的 init 方法一样。

Servlet 创建于用户第一次调用对应于该 Servlet 的 URL 时,但是您也可以指定 Servlet 在服务器第一次启动时被加载。

当用户调用一个 Servlet 时,就会创建一个 Servlet 实例,每一个用户请求都会产生一个新的线程,适当的时候移交给 doGet 或 doPost 方法。init() 方法简单地创建或加载一些数据,这些数据将被用于 Servlet 的整个生命周期。

init 方法的定义如下:

1
2
3
public void init() throws ServletException {
// 初始化代码...
}

service() 方法

service() 方法是执行实际任务的核心方法。Servlet 容器(即 Web 服务器)调用 service() 方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。

service() 方法有两个参数:ServletRequestServletResponseServletRequest 用来封装请求信息,ServletResponse 用来封装响应信息,因此本质上这两个类是对通信协议的封装。

每次服务器接收到一个 Servlet 请求时,服务器会产生一个新的线程并调用服务。service() 方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGetdoPostdoPutdoDelete 等方法。

下面是该方法的特征:

1
2
3
4
public void service(ServletRequest request,
ServletResponse response)
throws ServletException, IOException{
}

service() 方法由容器调用,service 方法在适当的时候调用 doGet、doPost、doPut、doDelete 等方法。所以,您不用对 service() 方法做任何动作,您只需要根据来自客户端的请求类型来重写 doGet() 或 doPost() 即可。

doGet() 和 doPost() 方法是每次服务请求中最常用的方法。下面是这两种方法的特征。

doGet() 方法

GET 请求来自于一个 URL 的正常请求,或者来自于一个未指定 METHOD 的 HTML 表单,它由 doGet() 方法处理。

1
2
3
4
5
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Servlet 代码
}

doPost() 方法

POST 请求来自于一个特别指定了 METHOD 为 POST 的 HTML 表单,它由 doPost() 方法处理。

1
2
3
4
5
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// Servlet 代码
}

destroy() 方法

destroy() 方法只会被调用一次,在 Servlet 生命周期结束时被调用。destroy() 方法可以让您的 Servlet 关闭数据库连接、停止后台线程、把 Cookie 列表或点击计数器写入到磁盘,并执行其他类似的清理活动。

在调用 destroy() 方法之后,servlet 对象被标记为垃圾回收。destroy 方法定义如下所示:

1
2
3
public void destroy() {
// 终止化代码...
}

Servlet 和 HTTP 状态码

title: JavaEE Servlet HTTP 状态码
date: 2017-11-08
categories:

  • javaee
    tags:
  • javaee
  • servlet
  • http

HTTP 状态码

HTTP 请求和 HTTP 响应消息的格式是类似的,结构如下:

  • 初始状态行 + 回车换行符(回车+换行)
  • 零个或多个标题行+回车换行符
  • 一个空白行,即回车换行符
  • 一个可选的消息主体,比如文件、查询数据或查询输出

例如,服务器的响应头如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
HTTP/1.1 200 OK
Content-Type: text/html
Header2: ...
...
HeaderN: ...
(Blank Line)
<!doctype ...>
<html>
<head>...</head>
<body>
...
</body>
</html>

状态行包括 HTTP 版本(在本例中为 HTTP/1.1)、一个状态码(在本例中为 200)和一个对应于状态码的短消息(在本例中为 OK)。

以下是可能从 Web 服务器返回的 HTTP 状态码和相关的信息列表:

  • 1**:信息性状态码
  • 2**:成功状态码
    • 200:请求正常成功
    • 204:指示请求成功但没有返回新信息
    • 206:指示服务器已完成对资源的部分 GET 请求
  • 3**:重定向状态码
    • 301:永久性重定向
    • 302:临时性重定向
    • 304:服务器端允许请求访问资源,但未满足条件
  • 4**:客户端错误状态码
    • 400:请求报文中存在语法错误
    • 401:发送的请求需要有通过 HTTP 认证的认证信息
    • 403:对请求资源的访问被服务器拒绝了
    • 404:服务器上无法找到请求的资源
  • 5**:服务器错误状态码
    • 500:服务器端在执行请求时发生了错误
    • 503:服务器暂时处于超负载或正在进行停机维护,现在无法处理请求

设置 HTTP 状态码的方法

下面的方法可用于在 Servlet 程序中设置 HTTP 状态码。这些方法通过 HttpServletResponse 对象可用。

序号 方法 & 描述
1 **public void setStatus ( int statusCode )**该方法设置一个任意的状态码。setStatus 方法接受一个 int(状态码)作为参数。如果您的反应包含了一个特殊的状态码和文档,请确保在使用 PrintWriter 实际返回任何内容之前调用 setStatus。
2 **public void sendRedirect(String url)**该方法生成一个 302 响应,连同一个带有新文档 URL 的 Location 头。
3 **public void sendError(int code, String message)**该方法发送一个状态码(通常为 404),连同一个在 HTML 文档内部自动格式化并发送到客户端的短消息。

HTTP 状态码实例

下面的例子把 407 错误代码发送到客户端浏览器,浏览器会显示 “Need authentication!!!” 消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 导入必需的 java 库
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;

// 扩展 HttpServlet 类
public class showError extends HttpServlet {

// 处理 GET 方法请求的方法
public void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException
{
// 设置错误代码和原因
response.sendError(407, "Need authentication!!!" );
}
// 处理 POST 方法请求的方法
public void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}

现在,调用上面的 Servlet 将显示以下结果:

1
2
3
4
5
HTTP Status 407 - Need authentication!!!
type Status report
message Need authentication!!!
description The client must first authenticate itself with the proxy (Need authentication!!!).
Apache Tomcat/5.5.29

参考资料

JavaWeb 之 Filter 和 Listener

引入了 Servlet 规范后,你不需要关心 Socket 网络通信、不需要关心 HTTP 协议,也不需要关心你的业务类是如何被实例化和调用的,因为这些都被 Servlet 规范标准化了,你只要关心怎么实现的你的业务逻辑。这对于程序员来说是件好事,但也有不方便的一面。所谓规范就是说大家都要遵守,就会千篇一律,但是如果这个规范不能满足你的业务的个性化需求,就有问题了,因此设计一个规范或者一个中间件,要充分考虑到可扩展性。Servlet 规范提供了两种扩展机制:FilterListener

Filter

Filter 是过滤器,这个接口允许你对请求和响应做一些统一的定制化处理

Filter 提供了过滤链(Filter Chain)的概念,一个过滤链包括多个 Filter。客户端请求 request 在抵达 Servlet 之前会经过过滤链的所有 Filter,服务器响应 response 从 Servlet 抵达客户端浏览器之前也会经过过滤链的所有 FIlter。

img

过滤器方法

Filter 接口有三个方法:

  • init:初始化 Filter
  • destroy:销毁 Filter
  • doFilter:将请求传给下个 FilterServlet

initdestroy 方法只会被调用一次;doFilter 每次有客户端请求都会被调用一次。

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 interface Filter {

/**
* web 程序启动时调用此方法, 用于初始化该 Filter
* @param config
* 可以从该参数中获取初始化参数以及ServletContext信息等
* @throws ServletException
*/
public void init(FilterConfig config) throws ServletException;

/**
* 客户请求服务器时会经过
*
* @param request
* 客户请求
* @param response
* 服务器响应
* @param chain
* 过滤链, 通过 chain.doFilter(request, response) 将请求传给下个 Filter 或
* Servlet
* @throws ServletException
* @throws IOException
*/
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws ServletException, IOException;

/**
* web 程序关闭时调用此方法, 用于销毁一些资源
*/
public void destroy();

}

过滤器配置

Filter 需要配置在 web.xml 中才能生效。一个 Filter 需要配置 <filter><filter-mapping> 标签。

  • <filter> 配置 Filter 名称,实现类以及初始化参数。
  • <filter-mapping> 配置什么规则下使用该 Filter。
  • <filter> 的 filterName 与 <filter-mapping> 的 filterName 必须匹配。
  • <url-pattern> 配置 URL 的规则,可以配置多个,可以使用通配符(*)。
  • <dispatcher> 配置到达 Servlet 的方式,有 4 种取值:REQUEST、FORWARD、INCLUDE、ERROR。可以同时配置多个 <dispatcher>。如果没有配置任何 <dispatcher>,默认为 REQUEST。
    • REQUEST - 表示仅当直接请求 Servlet 时才生效。
    • FORWARD - 表示仅当某 Servlet 通过 FORWARD 到该 Servlet 时才生效。
    • INCLUDE - JSP 中可以通过 <jsp:include> 请求某 Servlet。仅在这种情况表有效。
    • ERROR - JSP 中可以通过 <%@ page errorPage="error.jsp" %> 指定错误处理页面。仅在这种情况表有效。

Listener

监听器(Listener)用于监听 web 应用程序中的ServletContext, HttpSessionServletRequest等域对象的创建与销毁事件,以及监听这些域对象中的属性发生修改的事件。

使用 Listener 不需要关注该类事件时怎样触发或者怎么调用相应的 Listener,只要记住该类事件触发时一定会调用相应的 Listener,遵循 Servlet 规范的服务器会自动完成相应工作。

监听器的分类

在 Servlet 规范中定义了多种类型的监听器,它们用于监听的事件源分别为ServletContextHttpSessionServletRequest这三个域对象
Servlet 规范针对这三个对象上的操作,又把多种类型的监听器划分为三种类型:

  1. 监听域对象自身的创建和销毁的事件监听器。
  2. 监听域对象中的属性的增加和删除的事件监听器。
  3. 监听绑定到 HttpSession 域中的某个对象的状态的事件监听器。

监听对象的创建和销毁

HttpSessionListener

HttpSessionListener 接口用于监听 HttpSession 对象的创建和销毁。

  • 创建一个 Session 时,激发 sessionCreated (HttpSessionEvent se) 方法
  • 销毁一个 Session 时,激发 sessionDestroyed (HttpSessionEvent se) 方法。

ServletContextListener

ServletContextListener 接口用于监听 ServletContext 对象的创建和销毁事件。

实现了 ServletContextListener 接口的类都可以对 ServletContext 对象的创建和销毁进行监听。

  • ServletContext 对象被创建时,激发 contextInitialized (ServletContextEvent sce) 方法。
  • ServletContext 对象被销毁时,激发 contextDestroyed(ServletContextEvent sce) 方法。

ServletContext 域对象创建和销毁时机:

  • 创建:服务器启动针对每一个 Web 应用创建 ServletContext
  • 销毁:服务器关闭前先关闭代表每一个 web 应用的 ServletContext

ServletRequestListener

ServletRequestListener 接口用于监听 ServletRequest 对象的创建和销毁。

  • Request 对象被创建时,监听器的 requestInitialized(ServletRequestEvent sre) 方法将会被调用
  • Request 对象被销毁时,监听器的 requestDestroyed(ServletRequestEvent sre) 方法将会被调用

ServletRequest 域对象创建和销毁时机:

  • 创建:用户每一次访问都会创建 request 对象
  • 销毁:当前访问结束,request 对象就会销毁

监听对象的属性变化

域对象中属性的变更的事件监听器就是用来监听 ServletContextHttpSessionHttpServletRequest 这三个对象中的属性变更信息事件的监听器。
这三个监听器接口分别是 ServletContextAttributeListenerHttpSessionAttributeListener 和 ServletRequestAttributeListener,这三个接口中都定义了三个方法来处理被监听对象中的属性的增加,删除和替换的事件,同一个事件在这三个接口中对应的方法名称完全相同,只是接受的参数类型不同。

attributeAdded 方法

当向被监听对象中增加一个属性时,web 容器就调用事件监听器的 attributeAdded 方法进行响应,这个方法接收一个事件类型的参数,监听器可以通过这个参数来获得正在增加属性的域对象和被保存到域中的属性对象
各个域属性监听器中的完整语法定义为:

1
2
3
public void attributeAdded(ServletContextAttributeEvent scae)
public void attributeReplaced(HttpSessionBindingEvent hsbe)
public void attributeRmoved(ServletRequestAttributeEvent srae)

attributeRemoved 方法

当删除被监听对象中的一个属性时,web 容器调用事件监听器的 attributeRemoved 方法进行响应
各个域属性监听器中的完整语法定义为:

1
2
3
public void attributeRemoved(ServletContextAttributeEvent scae)
public void attributeRemoved(HttpSessionBindingEvent hsbe)
public void attributeRemoved(ServletRequestAttributeEvent srae)

attributeReplaced 方法

当监听器的域对象中的某个属性被替换时,web 容器调用事件监听器的 attributeReplaced 方法进行响应
各个域属性监听器中的完整语法定义为:

1
2
3
public void attributeReplaced(ServletContextAttributeEvent scae)
public void attributeReplaced(HttpSessionBindingEvent hsbe)
public void attributeReplaced(ServletRequestAttributeEvent srae)

监听 Session 内的对象

保存在 Session 域中的对象可以有多种状态:

  • 绑定(session.setAttribute("bean",Object))到 Session 中;
  • Session 域中解除绑定(session.removeAttribute("bean"));
  • Session 对象持久化到一个存储设备中;
  • Session 对象从一个存储设备中恢复。

Servlet 规范中定义了两个特殊的监听器接口 HttpSessionBindingListenerHttpSessionActivationListener 来帮助 JavaBean 对象了解自己在 Session 域中的这些状态。

实现这两个接口的类不需要 web.xml 文件中进行注册。

HttpSessionBindingListener

HttpSessionBindingListener 接口的 JavaBean 对象可以感知自己被绑定或解绑定到 Session 中的事件。

  • 当对象被绑定到 HttpSession 对象中时,web 服务器调用该对象的 valueBound(HttpSessionBindingEvent event) 方法。
  • 当对象从 HttpSession 对象中解除绑定时,web 服务器调用该对象的 valueUnbound(HttpSessionBindingEvent event) 方法。

HttpSessionActivationListener

实现了 HttpSessionActivationListener 接口的 JavaBean 对象可以感知自己被活化(反序列化)和钝化(序列化)的事件。

  • 当绑定到 HttpSession 对象中的 JavaBean 对象将要随 HttpSession 对象被序列化之前,web 服务器调用该 JavaBean 对象的 sessionWillPassivate(HttpSessionEvent event) 方法。这样 JavaBean 对象就可以知道自己将要和 HttpSession 对象一起被序列化到硬盘中.
  • 当绑定到 HttpSession 对象中的 JavaBean 对象将要随 HttpSession 对象被反序列化之后,web 服务器调用该 JavaBean 对象的 sessionDidActive(HttpSessionEvent event) 方法。这样 JavaBean 对象就可以知道自己将要和 HttpSession 对象一起被反序列化回到内存中

Filter 和 Listener

Filter 和 Listener 的本质区别:

  • Filter 是干预过程的,它是过程的一部分,是基于过程行为的。
  • Listener 是基于状态的,任何行为改变同一个状态,触发的事件是一致的。

示例代码

  • Filter 的示例源码:源码
  • Listener 的示例源码:源码

参考资料