Dunwu Blog

大道至简,知易行难

HBase Java API 基础特性

HBase Client API

HBase Java API 示例

引入依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.hbase</groupId>
<artifactId>hbase-client</artifactId>
<version>2.1.4</version>
</dependency>

示例

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
public class HBaseUtils {

private static Connection connection;

static {
Configuration configuration = HBaseConfiguration.create();
configuration.set("hbase.zookeeper.property.clientPort", "2181");
// 如果是集群 则主机名用逗号分隔
configuration.set("hbase.zookeeper.quorum", "hadoop001");
try {
connection = ConnectionFactory.createConnection(configuration);
} catch (IOException e) {
e.printStackTrace();
}
}

/**
* 创建 HBase 表
*
* @param tableName 表名
* @param columnFamilies 列族的数组
*/
public static boolean createTable(String tableName, List<String> columnFamilies) {
try {
HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
if (admin.tableExists(TableName.valueOf(tableName))) {
return false;
}
TableDescriptorBuilder tableDescriptor = TableDescriptorBuilder.newBuilder(TableName.valueOf(tableName));
columnFamilies.forEach(columnFamily -> {
ColumnFamilyDescriptorBuilder cfDescriptorBuilder = ColumnFamilyDescriptorBuilder.newBuilder(Bytes.toBytes(columnFamily));
cfDescriptorBuilder.setMaxVersions(1);
ColumnFamilyDescriptor familyDescriptor = cfDescriptorBuilder.build();
tableDescriptor.setColumnFamily(familyDescriptor);
});
admin.createTable(tableDescriptor.build());
} catch (IOException e) {
e.printStackTrace();
}
return true;
}


/**
* 删除 hBase 表
*
* @param tableName 表名
*/
public static boolean deleteTable(String tableName) {
try {
HBaseAdmin admin = (HBaseAdmin) connection.getAdmin();
// 删除表前需要先禁用表
admin.disableTable(TableName.valueOf(tableName));
admin.deleteTable(TableName.valueOf(tableName));
} catch (Exception e) {
e.printStackTrace();
}
return true;
}

/**
* 插入数据
*
* @param tableName 表名
* @param rowKey 唯一标识
* @param columnFamilyName 列族名
* @param qualifier 列标识
* @param value 数据
*/
public static boolean putRow(String tableName, String rowKey, String columnFamilyName, String qualifier,
String value) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Put put = new Put(Bytes.toBytes(rowKey));
put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(qualifier), Bytes.toBytes(value));
table.put(put);
table.close();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}


/**
* 插入数据
*
* @param tableName 表名
* @param rowKey 唯一标识
* @param columnFamilyName 列族名
* @param pairList 列标识和值的集合
*/
public static boolean putRow(String tableName, String rowKey, String columnFamilyName, List<Pair<String, String>> pairList) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Put put = new Put(Bytes.toBytes(rowKey));
pairList.forEach(pair -> put.addColumn(Bytes.toBytes(columnFamilyName), Bytes.toBytes(pair.getKey()), Bytes.toBytes(pair.getValue())));
table.put(put);
table.close();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}


/**
* 根据 rowKey 获取指定行的数据
*
* @param tableName 表名
* @param rowKey 唯一标识
*/
public static Result getRow(String tableName, String rowKey) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowKey));
return table.get(get);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}


/**
* 获取指定行指定列 (cell) 的最新版本的数据
*
* @param tableName 表名
* @param rowKey 唯一标识
* @param columnFamily 列族
* @param qualifier 列标识
*/
public static String getCell(String tableName, String rowKey, String columnFamily, String qualifier) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Get get = new Get(Bytes.toBytes(rowKey));
if (!get.isCheckExistenceOnly()) {
get.addColumn(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));
Result result = table.get(get);
byte[] resultValue = result.getValue(Bytes.toBytes(columnFamily), Bytes.toBytes(qualifier));
return Bytes.toString(resultValue);
} else {
return null;
}

} catch (IOException e) {
e.printStackTrace();
}
return null;
}


/**
* 检索全表
*
* @param tableName 表名
*/
public static ResultScanner getScanner(String tableName) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}


/**
* 检索表中指定数据
*
* @param tableName 表名
* @param filterList 过滤器
*/

public static ResultScanner getScanner(String tableName, FilterList filterList) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
scan.setFilter(filterList);
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

/**
* 检索表中指定数据
*
* @param tableName 表名
* @param startRowKey 起始 RowKey
* @param endRowKey 终止 RowKey
* @param filterList 过滤器
*/

public static ResultScanner getScanner(String tableName, String startRowKey, String endRowKey,
FilterList filterList) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Scan scan = new Scan();
scan.withStartRow(Bytes.toBytes(startRowKey));
scan.withStopRow(Bytes.toBytes(endRowKey));
scan.setFilter(filterList);
return table.getScanner(scan);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}

/**
* 删除指定行记录
*
* @param tableName 表名
* @param rowKey 唯一标识
*/
public static boolean deleteRow(String tableName, String rowKey) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Delete delete = new Delete(Bytes.toBytes(rowKey));
table.delete(delete);
} catch (IOException e) {
e.printStackTrace();
}
return true;
}


/**
* 删除指定行指定列
*
* @param tableName 表名
* @param rowKey 唯一标识
* @param familyName 列族
* @param qualifier 列标识
*/
public static boolean deleteColumn(String tableName, String rowKey, String familyName,
String qualifier) {
try {
Table table = connection.getTable(TableName.valueOf(tableName));
Delete delete = new Delete(Bytes.toBytes(rowKey));
delete.addColumn(Bytes.toBytes(familyName), Bytes.toBytes(qualifier));
table.delete(delete);
table.close();
} catch (IOException e) {
e.printStackTrace();
}
return true;
}

}

数据库连接

在上面的代码中,在类加载时就初始化了 Connection 连接,并且之后的方法都是复用这个 Connection,这时我们可能会考虑是否可以使用自定义连接池来获取更好的性能表现?实际上这是没有必要的。

首先官方对于 Connection 的使用说明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Connection Pooling For applications which require high-end multithreaded
access (e.g., web-servers or application servers that may serve many
application threads in a single JVM), you can pre-create a Connection,
as shown in the following example:

对于高并发多线程访问的应用程序(例如,在单个 JVM 中存在的为多个线程服务的 Web 服务器或应用程序服务器),
您只需要预先创建一个 Connection。例子如下:

// Create a connection to the cluster.
Configuration conf = HBaseConfiguration.create();
try (Connection connection = ConnectionFactory.createConnection(conf);
Table table = connection.getTable(TableName.valueOf(tablename))) {
// use table as needed, the table returned is lightweight
}

之所以能这样使用,这是因为 Connection 并不是一个简单的 socket 连接,接口文档 中对 Connection 的表述是:

1
2
3
4
5
6
7
A cluster connection encapsulating lower level individual connections to actual servers and a
connection to zookeeper. Connections are instantiated through the ConnectionFactory class.
The lifecycle of the connection is managed by the caller, who has to close() the connection
to release the resources.

Connection 是一个集群连接,封装了与多台服务器(Matser/Region Server)的底层连接以及与 zookeeper 的连接。
连接通过 ConnectionFactory 类实例化。连接的生命周期由调用者管理,调用者必须使用 close() 关闭连接以释放资源。

之所以封装这些连接,是因为 HBase 客户端需要连接三个不同的服务角色:

  • Zookeeper :主要用于获取 meta 表的位置信息,Master 的信息;
  • HBase Master :主要用于执行 HBaseAdmin 接口的一些操作,例如建表等;
  • HBase RegionServer :用于读、写数据。

Connection 对象和实际的 Socket 连接之间的对应关系如下图:

在 HBase 客户端代码中,真正对应 Socket 连接的是 RpcConnection 对象。HBase 使用 PoolMap 这种数据结构来存储客户端到 HBase 服务器之间的连接。PoolMap 的内部有一个 ConcurrentHashMap 实例,其 key 是 ConnectionId(封装了服务器地址和用户 ticket),value 是一个 RpcConnection 对象的资源池。当 HBase 需要连接一个服务器时,首先会根据 ConnectionId 找到对应的连接池,然后从连接池中取出一个连接对象。

1
2
3
4
5
6
7
8
9
10
11
12
@InterfaceAudience.Private
public class PoolMap<K, V> implements Map<K, V> {
private PoolType poolType;

private int poolMaxSize;

private Map<K, Pool<V>> pools = new ConcurrentHashMap<>();

public PoolMap(PoolType poolType) {
this.poolType = poolType;
}
.....

HBase 中提供了三种资源池的实现,分别是 ReusableRoundRobinThreadLocal。具体实现可以通 hbase.client.ipc.pool.type 配置项指定,默认为 Reusable。连接池的大小也可以通过 hbase.client.ipc.pool.size 配置项指定,默认为 1,即每个 Server 1 个连接。也可以通过修改配置实现:

1
2
3
config.set("hbase.client.ipc.pool.type",...);
config.set("hbase.client.ipc.pool.size",...);
connection = ConnectionFactory.createConnection(config);

由此可以看出 HBase 中 Connection 类已经实现了对连接的管理功能,所以我们不必在 Connection 上在做额外的管理。

另外,Connection 是线程安全的,但 Table 和 Admin 却不是线程安全的,因此正确的做法是一个进程共用一个 Connection 对象,而在不同的线程中使用单独的 Table 和 Admin 对象。Table 和 Admin 的获取操作 getTable()getAdmin() 都是轻量级,所以不必担心性能的消耗,同时建议在使用完成后显示的调用 close() 方法来关闭它们。

概述

HBase 的主要客户端操作是由 org.apache.hadoop.hbase.client.HTable 提供的。创建 HTable 实例非常耗时,所以,建议每个线程只创建一次 HTable 实例。

HBase 所有修改数据的操作都保证了行级别的原子性。要么读到最新的修改,要么等待系统允许写入改行修改

用户要尽量使用批处理(batch)更新来减少单独操作同一行数据的次数

写操作中设计的列的数目并不会影响该行数据的原子性,行原子性会同时保护到所有列

创建 HTable 实例(指的是在 java 中新建该类),每个实例都要扫描.META. 表,以检查该表是否存在,推荐用户只创建一次 HTable 实例,而且是每个线程创建一个

如果用户需要多个 HTable 实例,建议使用 HTablePool 类(类似连接池)

CRUD 操作

put

Table 接口提供了两个 put 方法

1
2
3
4
// 写入单行 put
void put(Put put) throws IOException;
// 批量写入 put
void put(List<Put> puts) throws IOException;

Put 类提供了多种构造器方法用来初始化实例。

Put 类还提供了一系列有用的方法:

多个 add 方法:用于添加指定的列数据。

has 方法:用于检查是否存在特定的单元格,而不需要遍历整个集合

getFamilyMap 方法:可以遍历 Put 实例中每一个可用的 KeyValue 实例

getRow 方法:用于获取 rowkey
Put.heapSize() 可以计算当前 Put 实例所需的堆大小,既包含其中的数据,也包含内部数据结构所需的空间

KeyValue 类

特定单元格的数据以及坐标,坐标包括行键、列族名、列限定符以及时间戳
KeyValue(byte[] row, int roffset, int rlength, byte[] family, int foffoset, int flength, byte[] qualifier, int qoffset, int qlength, long timestamp, Type type, byte[] value, int voffset, int vlength)
每一个字节数组都有一个 offset 参数和一个 length 参数,允许用户提交一个已经存在的字节数组进行字节级别操作。
行目前来说指的是行键,即 Put 构造器里的 row 参数。

客户端的写缓冲区

每一个 put 操作实际上都是一个 RPC 操作,它将客户端数据传送到服务器然后返回。

HBase 的 API 配备了一个客户端的写缓冲区,缓冲区负责收集 put 操作,然后调用 RPC 操作一次性将 put 送往服务器。

1
2
void setAutoFlush(boolean autoFlush)
boolean isAutoFlush()

默认情况下,客户端缓冲区是禁用的。可以通过 table.setAutoFlush(false) 来激活缓冲区。

Put 列表

批量提交 put 列表:

1
void put(List<Put> puts) throws IOException

注意:批量提交可能会有部分修改失败。

原子性操作 compare-and-set

checkAndPut 方法提供了 CAS 机制来保证 put 操作的原子性。

get

1
Result get(Get get) throws IOException
1
2
3
4
Get(byte[] row)
Get(byte[] row, RowLock rowLock)
Get addColumn(byte[] family, byte[] qualifier)
Get addFamily(byte[] family)

Result 类

当用户使用 get() 方法获取数据,HBase 返回的结果包含所有匹配的单元格数据,这些数据被封装在一个 Result 实例中返回给用户。

Result 类提供的方法如下:

1
2
3
4
5
6
7
byte[] getValue(byte[] family, byte[] qualifier)
byte[] value()
byte[] getRow()
int size()
boolean isEmpty()
KeyValue[] raw()
List<KeyValue> list()

delete

1
void delete(Delete delete) throws IOException
1
2
Delte(byte[] row)
Delete(byte[] row, long timestamp, RowLock rowLock)
1
2
3
4
Delete deleteFamily(byte[] family)
Delete deleteFamily(byte[] family, long timestamp)
Delete deleteColumns(byte[] family, byte[] qualifier)
Delete deleteColumn(byte[] family, byte[] qualifier) // 只删除最新版本

批处理操作

Row 是 Put、Get、Delete 的父类。

1
2
void batch(List<Row> actions, Object[] results) throws IOException, InterruptedException
Object batch(List<Row> actions) throws IOException, InterruptedException

行锁

region 服务器提供了行锁特性,这个特性保证了只有一个客户端能获取一行数据相应的锁,同时对该行进行修改。

如果不显示指定锁,服务器会隐式加锁。

扫描

scan,类似数据库系统中的 cursor,利用了 HBase 提供的底层顺序存储的数据结构。

调用 HTable 的 getScanner 就可以返回扫描器

1
2
ResultScanner getScanner(Scan scan) throws IOException
ResultScanner getScanner(byte[] family) throws IOException

Scan 类构造器可以有 startRow,区间一般为 [startRow, stopRow)

1
2
Scan(byte[] startRow, Filter filter)
Scan(byte[] startRow)

ResultScanner

以行为单位进行返回

1
2
3
Result next() throws IOException
Result[] next(int nbRows) throws IOException
void close()

缓存与批量处理

每一个 next()调用都会为每行数据生成一个单独的 RPC 请求

可以设置扫描器缓存

1
2
void setScannerCaching(itn scannerCaching)
int getScannerCaching()

缓存是面向行一级操作,批量是面向列一级操作

1
2
void setBatch(int batch)
int getBatch

RPC 请求的次数=(行数*每行列数)/Min(每行的列数,批量大小)/扫描器缓存

各种特性

Bytes 类提供了一系列将原生 Java 类型和字节数组互转的方法。

参考资料

《极客时间教程 - 大规模数据处理实战》笔记

00 丨开篇词丨从这里开始,带你走上硅谷一线系统架构师之路

01 丨为什么 MapReduce 会被硅谷一线公司淘汰?

高昂的维护成本

时间性能“达不到”用户的期待

02 | MapReduce 后谁主沉浮:怎样设计下一代数据处理技术?

03 | 大规模数据处理初体验:怎样实现大型电商热销榜?

不同量级 TOP K 算法的解决方案不同:

小规模:Hash 即可

大规模:由于单机的处理量不足以处理全量数据,势必分而治之:分片统计,然后聚合(即先 map 后 reduce)

04 丨分布式系统(上):学会用服务等级协议 SLA 来评估你的系统

SLA(Service-Level Agreement),也就是服务等级协议,指的是系统服务提供者(Provider)对客户(Customer)的一个服务承诺。

可用性:大厂一般要求可用性至少达到四个 9(即 99.99%)

准确性:准确率= 正确的有效请求数 / 有效的总请求数

系统容量:通常通过 QPS (Queries Per Second)来衡量

延迟:请求和响应的时间间隔

05 丨分布式系统(下):架构师不得不知的三大指标

  • 可扩展性(Scalability)
    • 水平扩展(Horizontal Scaling)
    • 垂直扩展(Vertical Scaling)
  • 一致性(Consistency)
    • 强一致性(Strong Consistency):系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新
      后的值。所以在任意时刻,同一系统所有节点中的数据是一样的。
    • 弱一致性(Weak Consistency):系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,
      也可能是更改前的值。但经过“不一致时间窗口”这段时间后,后续对该数据的读取都是更新后的值。
    • 最终一致性(Eventual Consistency):是弱一致性的特殊形式。存储系统保证,在没有新的更新的条件下,最终所有的访问都是最后更新的值。
  • 持久性(Data Durability):意味着数据一旦被成功存储就可以一直继续使用,即使系统中的节点下线、宕机或数据损坏也是如。

06 | 如何区分批处理还是流处理?

  • 无边界数据(Unbounded Data):是一种不断增长,可以说是无限的数据集。
  • 有边界数据(Bounded Data):是一种有限的数据集。
  • 事件时间(Event Time):指的是一个数据实际产生的时间点。
  • 处理时间(Precessing Time):指的是处理数据的系统架构实际接收到这个数据的时间点。
  • 批处理:绝大部分情况下,批处理的输入数据都是有边界数据,同样的,输出结果也一样是有边界数据。所以在批处理中,我们所关心的更多会是数据的事件时间。
    • 应用场景:
      • 日志分析:日志系统是在一定时间段(日,周或年)内收集的,而日志的数据处理分析是在不同的时间内执行,以得出有关系统的一些关键性能指标。
      • 计费应用程序:计费应用程序会计算出一段时间内一项服务的使用程度,并生成计费信息,例如银行在每个月末生成的信用卡还款单。
      • 数据仓库:数据仓库的主要目标是根据收集好的数据事件时间,将数据信息合并为静态快照 (static snapshot),并将它们聚合为每周、每月、每季度的报告等。
  • 流处理:流处理的输入数据基本上都是无边界数据。
    • 应用场景
      • 实时监控:捕获和分析各种来源发布的数据,如传感器,新闻源,点击网页等。
      • 实时商业智能:智能汽车,智能家居,智能病人护理等。
      • 销售终端(POS)系统:像是股票价格的更新,允许用户实时完成付款的系统等。

07 | Workflow 设计模式:让你在大规模数据世界中君临天下

08 | 发布/订阅模式:流处理架构中的瑞士军刀

09 丨 CAP 定理:三选二,架构师必须学会的取舍

10 丨 Lambda 架构:Twitter 亿级实时数据分析架构背后的倚天剑

Lambda 架构总共由三层系统组成:批处理层(Batch Layer),速度处理层(Speed Layer),以及用于响应查询的服务层(Serving Layer)。

11 丨 Kappa 架构:利用 Kafka 锻造的屠龙刀

12 | 我们为什么需要 Spark?

MapReduce 的缺点:

  • 高昂的维护成本
  • 时间性能“达不到”用户的期待
  • MapReduce 模型的抽象层次低
  • 只提供 Map 和 Reduce 两个操作
  • 在 Hadoop 中,每一个 Job 的计算结果都会存储在 HDFS 文件存储系统中,所以每一步计算都要进行硬盘的读取和写入,大大增加了系统的延迟。
  • 只支持批处理

Spark 的优点

  • 性能比 MapReduce 高很多
  • Spark 提供了很多对 RDD 的操作,如 Map、Filter、flatMap、groupByKey 和 Union 等等,极大地提升了对各种复杂场景的支持

13 丨弹性分布式数据集:Spark 大厦的地基(上)

Spark 最基本的数据抽象是弹性分布式数据集(Resilient Distributed Dataset)

RDD 表示已被分区、不可变的,并能够被并行操作的数据集合。

14 丨弹性分布式数据集:Spark 大厦的地基(下)

15 丨 SparkSQL:Spark 数据查询的利器

16 | Spark Streaming:Spark 的实时流计算 API

Spark Streaming 用时间片拆分了无限的数据流,然后对每一个数据片用类似于批处理的方法进行处理,输出的数据也是一块一块的

17 | Structured Streaming:如何用 DataFrame API 进行实时数据分析?

18 丨 WordCount:从零开始运行你的第一个 Spark 应用

19 丨综合案例实战:处理加州房屋信息,构建线性回归模型

20 丨流处理案例实战:分析纽约市出租车载客信息


读到此处,感觉收获甚少,暂时搁置阅读。

参考资料

《极客时间教程 - 从 0 开始学大数据》笔记

预习模块

01 丨预习 01 丨大数据技术发展史:大数据的前世今生

大数据技术,起源于 Google 在 2004 年前后发表的三篇论文:

Doug Cutting 根据 Google 论文开发了 Hadoop。

大数据处理的主要应用场景包括数据分析、数据挖掘与机器学习。

数据分析主要使用 Hive、Spark SQL 等 SQL 引擎完成;

数据挖掘与机器学习则有专门的机器学习框架 TensorFlow、Mahout 以及 MLlib 等,内置了主要的机器学习和数据挖掘算法。

大数据要存入分布式文件系统(HDFS),要有序调度 MapReduce 和 Spark 作业执行,并能把执行结果写入到各个应用系统的数据库中,还需要有一个大数据平台整合所有这些大数据组件和企业应用系统。

02 丨预习 02 丨大数据应用发展史:从搜索引擎到人工智能

大数据的应用领域:

  • 搜索引擎:GFS 和 MapReduce 开启了超大规模的分布式存储和分布式计算应用。
  • 数据仓库:Hive 实现了用更低廉的价格获得比以往多得多的数据存储与计算能力。
  • 数据挖掘:基于海量数据进行关联分析。应用有:关联推荐、用户画像、关系图谱
  • 机器学习:有了大数据,可以把全部的历史数据都收集起来,统计其规律,进而预测正在发生的事情。

03 丨预习 03 丨大数据应用领域:数据驱动一切

大数据的行业应用:

  • 医疗健康领域
    • 医学影像智能识别
    • 病历大数据智能诊疗
  • 教育领域
    • AI 外语老师
    • 智能解题
  • 社交媒体领域:舆情监控与分析
  • 金融领域:大数据风控
  • 新零售领域:全链路管理
  • 交通领域
    • 实时采集监控数据
    • 判断道路拥堵状态
    • 无人驾驶技术

模块一、Hadoop 大数据原理与架构

04 | 移动计算比移动数据更划算

传统计算模型:输入 -> 计算 -> 输出,面对海量数据(TB 级甚至 PB 级),无法应对。

移动计算将程序分发到数据所在的地方进行计算,也就是所谓的移动计算比移动数据更划算。

移动计算步骤:

  1. 将待处理的大规模数据存储在服务器集群的所有服务器上,主要使用 HDFS 分布式文件存储系统,将文件分成很多块(Block),以块为单位存储在集群的服务器上。
  2. 大数据引擎根据集群里不同服务器的计算能力,在每台服务器上启动若干分布式任务执行进程,这些进程会等待给它们分配执行任务。
  3. 使用大数据计算框架支持的编程模型进行编程,比如 Hadoop 的 MapReduce 编程模型,或者 Spark 的 RDD 编程模型。应用程序编写好以后,将其打包,MapReduce 和 Spark 都是在 JVM 环境中运行,所以打包出来的是一个 Java 的 JAR 包。
  4. 用 Hadoop 或者 Spark 的启动命令执行这个应用程序的 JAR 包,首先执行引擎会解析程序要处理的数据输入路径,根据输入数据量的大小,将数据分成若干片(Split),每一个数据片都分配给一个任务执行进程去处理。
  5. 任务执行进程收到分配的任务后,检查自己是否有任务对应的程序包,如果没有就去下载程序包,下载以后通过反射的方式加载程序。走到这里,最重要的一步,也就是移动计算就完成了。
  6. 加载程序后,任务执行进程根据分配的数据片的文件地址和数据在文件内的偏移量读取数据,并把数据输入给应用程序相应的方法

05 | 从 RAID 看垂直伸缩到水平伸缩的演化

海量数据存储核心问题

  • 数据存储容量的问题。既然大数据要解决的是数以 PB 计的数据计算问题,而一般的服务器磁盘容量通常 1 ~ 2TB,那么如何存储这么大规模的数据呢?
  • 数据读写速度的问题。一般磁盘的连续读写速度为几十 MB,以这样的速度,几十 PB 的数据恐怕要读写到天荒地老。
  • 数据可靠性的问题。磁盘大约是计算机设备中最易损坏的硬件了,通常情况一块磁盘使用寿命大概是一年,如果磁盘损坏了,数据怎么办?

解决方式是 RAID 技术:

  • 数据存储容量的问题。RAID 使用了 N 块磁盘构成一个存储阵列,如果使用 RAID 5,数据就可以存储在 N-1 块磁盘上,这样将存储空间扩大了 N-1 倍。
  • 数据读写速度的问题。RAID 根据可以使用的磁盘数量,将待写入的数据分成多片,并发同时向多块磁盘进行写入,显然写入的速度可以得到明显提高;同理,读取速度也可以得到明显提高。不过,需要注意的是,由于传统机械磁盘的访问延迟主要来自于寻址时间,数据真正进行读写的时间可能只占据整个数据访问时间的一小部分,所以数据分片后对 N 块磁盘进行并发读写操作并不能将访问速度提高 N 倍。
  • 数据可靠性的问题。使用 RAID 10、RAID 5 或者 RAID 6 方案的时候,由于数据有冗余存储,或者存储校验信息,所以当某块磁盘损坏的时候,可以通过其他磁盘上的数据和校验数据将丢失磁盘上的数据还原。

实现更强的计算能力和更大规模的数据存储有两种思路

  • 垂直伸缩(scaling up),即硬件升级
  • 水平伸缩(scaling out),即分布式系统

注:RAID 技术就是采用了垂直伸缩的方式。

06 | 新技术层出不穷,HDFS 依然是存储的王者

img

HDFS 有两个关键组件:DataNode 和 NameNode:

  • DataNode 负责文件数据的存储和读写操作,HDFS 将文件数据分割成若干数据块(Block),每个 DataNode 存储一部分数据块,这样文件就分布存储在整个 HDFS 服务器集群中。应用程序客户端(Client)可以并行对这些数据块进行访问,从而使得 HDFS 可以在服务器集群规模上实现数据并行访问,极大地提高了访问速度。
  • NameNode 负责整个分布式文件系统的元数据(MetaData)管理,也就是文件路径名、数据块的 ID 以及存储位置等信息,相当于操作系统中文件分配表(FAT)的角色。

为保证高可用,HDFS 会,会将一个数据块复制为多份(默认为 3 份),并将多份相同的数据块存储在不同的服务器上,甚至不同的机架上。

img

HDFS 故障容错:

  • 数据存储故障容错
    • 对于 DataNode 上的数据块进行计算并存储校验和(CheckSum)
    • 读取数据的时候,重新计算读取出来的数据的校验和,校验和不正确,则抛出异常
    • 发现异常后,从其他 DataNode 读取备份
  • 磁盘故障容错
    • 如果 DataNode 监测到本机磁盘损坏,将该磁盘的所有数据块 ID 报告给 NameNode
    • NameNode 检查这些数据块在哪些 DataNode 上有备份,复制一份到其他 DataNode 上
  • DataNode 故障容错
    • DataNode 会通过心跳和 NameNode 保持通信
    • 如果 DataNode 超时未发送心跳,NameNode 视其为宕机
    • NameNode 查找这个 DataNode 存储的所有数据块,复制一份到其他 DataNode 上
  • NameNode 故障容错
    • 基于 ZooKeeper 实现主从备份
    • 争夺 znode 锁

07 | 为什么说 MapReduce 既是编程模型又是计算框架?

MapReduce 既是编程模型,又是计算框架

MapReduce 编程模型只包含 Map 和 Reduce 两个过程,map 的主要输入是一对 <Key, Value> 值,经过 map 计算后输出一对 <Key, Value> 值;然后将相同 Key 合并,形成 <Key, Value 集合>;再将这个 <Key, Value 集合> 输入 reduce,经过计算输出零个或多个 <Key, Value> 对。

08 | MapReduce 如何让数据完成一次旅行?

MapReduce 作业启动和运行机制

大数据应用进程。这类进程是启动 MapReduce 程序的主入口,主要是指定 Map 和 Reduce 类、输入输出文件路径等,并提交作业给 Hadoop 集群,也就是下面提到的 JobTracker 进程。这是由用户启动的 MapReduce 程序进程,比如我们上期提到的 WordCount 程序。

JobTracker 进程。这类进程根据要处理的输入数据量,命令下面提到的 TaskTracker 进程启动相应数量的 Map 和 Reduce 进程任务,并管理整个作业生命周期的任务调度和监控。这是 Hadoop 集群的常驻进程,需要注意的是,JobTracker 进程在整个 Hadoop 集群全局唯一。

TaskTracker 进程。这个进程负责启动和管理 Map 进程以及 Reduce 进程。因为需要每个数据块都有对应的 map 函数,TaskTracker 进程通常和 HDFS 的 DataNode 进程启动在同一个服务器。也就是说,Hadoop 集群中绝大多数服务器同时运行 DataNode 进程和 TaskTracker 进程。

MapReduce 数据合并与连接机制

在 map 输出与 reduce 输入之间,MapReduce 计算框架处理数据合并与连接操作,这个操作有个专门的词汇叫 shuffle。分布式计算需要将不同服务器上的相关数据合并到一起进行下一步计算,这就是 shuffle。

09 | 为什么我们管 Yarn 叫作资源调度框架?

服务器集群资源调度管理和 MapReduce 执行过程耦合在一起,如果想在当前集群中运行其他计算任务,比如 Spark 或者 Storm,就无法统一使用集群中的资源了。

Yarn 包括两个部分:

ResourceManager 进程负责整个集群的资源调度管理,通常部署在独立的服务器上;

NodeManager 进程负责具体服务器上的资源和任务管理,在集群的每一台计算服务器上都会启动,基本上跟 HDFS 的 DataNode 进程一起出现。

Yarn 的工作流程

  1. 我们向 Yarn 提交应用程序,包括 MapReduce ApplicationMaster、我们的 MapReduce 程序,以及 MapReduce Application 启动命令。
  2. ResourceManager 进程和 NodeManager 进程通信,根据集群资源,为用户程序分配第一个容器,并将 MapReduce ApplicationMaster 分发到这个容器上面,并在容器里面启动 MapReduce ApplicationMaster。
  3. MapReduce ApplicationMaster 启动后立即向 ResourceManager 进程注册,并为自己的应用程序申请容器资源。
  4. MapReduce ApplicationMaster 申请到需要的容器后,立即和相应的 NodeManager 进程通信,将用户 MapReduce 程序分发到 NodeManager 进程所在服务器,并在容器中运行,运行的就是 Map 或者 Reduce 任务。
  5. Map 或者 Reduce 任务在运行期和 MapReduce ApplicationMaster 通信,汇报自己的运行状态,如果运行结束,MapReduce ApplicationMaster 向 ResourceManager 进程注销并释放所有的容器资源。

10 | 模块答疑:我们能从 Hadoop 学到什么?

Hadoop 几个主要产品的架构设计,就会发现它们都有相似性,都是一主多从的架构方案。

  • HDFS,一个 NameNode,多个 DataNode;
  • MapReduce,一个 JobTracker,多个 TaskTracker;
  • Yarn,一个 ResourceManager,多个 NodeManager。

事实上,很多大数据产品都是这样的架构方案:

Storm,一个 Nimbus,多个 Supervisor;

Spark,一个 Master,多个 Slave。

大数据因为要对数据和计算任务进行统一管理,所以和互联网在线应用不同,需要一个全局管理者。一言以蔽之:集中管理,分布存储与计算

模块二、大数据生态体系主要产品原理与架构

11 | Hive 是如何让 MapReduce 实现 SQL 操作的?

Hive 能够直接处理我们输入的 SQL 语句(Hive 的 SQL 语法和数据库标准 SQL 略有不同),调用 MapReduce 计算框架完成数据分析操作。

我们通过 Hive 的 Client(Hive 的命令行工具,JDBC 等)向 Hive 提交 SQL 命令。如果是创建数据表的 DDL(数据定义语言),Hive 就会通过执行引擎 Driver 将数据表的信息记录在 Metastore 元数据组件中,这个组件通常用一个关系数据库实现,记录表名、字段名、字段类型、关联 HDFS 文件路径等这些数据库的 Meta 信息(元信息)。

如果我们提交的是查询分析数据的 DQL(数据查询语句),Driver 就会将该语句提交给自己的编译器 Compiler 进行语法分析、语法解析、语法优化等一系列操作,最后生成一个 MapReduce 执行计划。然后根据执行计划生成一个 MapReduce 的作业,提交给 Hadoop MapReduce 计算框架处理。

12 | 我们并没有觉得 MapReduce 速度慢,直到 Spark 出现

RDD 是 Spark 的核心概念,是弹性数据集(Resilient Distributed Datasets)的缩写。RDD 既是 Spark 面向开发者的编程模型,又是 Spark 自身架构的核心元素。

Spark 上编写 WordCount 程序,主要代码只需要三行

1
2
3
4
5
val textFile = sc.textFile("hdfs://...")
val counts = textFile.flatMap(line => line.split(" "))
.map(word => (word, 1))
.reduceByKey(_ + _)
counts.saveAsTextFile("hdfs://...")

MapReduce 针对输入数据,将计算过程分为两个阶段,一个 Map 阶段,一个 Reduce 阶段,可以理解成是面向过程的大数据计算。

而 Spark 则直接针对数据进行编程,将大规模数据集合抽象成一个 RDD 对象,然后在这个 RDD 上进行各种计算处理,得到一个新的 RDD,继续计算处理,直到得到最后的结果数据。所以 Spark 可以理解成是面向对象的大数据计算。

RDD 上定义的函数分两种,一种是转换(transformation)函数,这种函数的返回值还是 RDD;另一种是执行(action)函数,这种函数不再返回 RDD。

RDD 定义了很多转换操作函数,比如有计算 map(func)、过滤 filter(func)、合并数据集 union(otherDataset)、根据 Key 聚合 reduceByKey(func, [numPartitions])、连接数据集 join(otherDataset, [numPartitions])、分组 groupByKey([numPartitions]) 等十几个函数。

13 | 同样的本质,为何 Spark 可以更高效?

Spark 有三个主要特性:RDD 的编程模型更简单,DAG 切分的多阶段计算过程更快速,使用内存存储中间计算结果更高效。这三个特性使得 Spark 相对 Hadoop MapReduce 可以有更快的执行速度,以及更简单的编程实现。

14 | BigTable 的开源实现:HBase

HBase 可伸缩架构

HBase 的伸缩性主要依赖其可分裂的 HRegion 及可伸缩的分布式文件系统 HDFS 实现。

HRegion 是 HBase 负责数据存储的主要进程,应用程序对数据的读写操作都是通过和 HRegion 通信完成。上面是 HBase 架构图,我们可以看到在 HBase 中,数据以 HRegion 为单位进行管理,也就是说应用程序如果想要访问一个数据,必须先找到 HRegion,然后将数据读写操作提交给 HRegion,由 HRegion 完成存储层面的数据操作。

HRegionServer 是物理服务器,每个 HRegionServer 上可以启动多个 HRegion 实例。当一个 HRegion 中写入的数据太多,达到配置的阈值时,一个 HRegion 会分裂成两个 HRegion,并将 HRegion 在整个集群中进行迁移,以使 HRegionServer 的负载均衡。

每个 HRegion 中存储一段 Key 值区间 [key1, key2) 的数据,所有 HRegion 的信息,包括存储的 Key 值区间、所在 HRegionServer 地址、访问端口号等,都记录在 HMaster 服务器上。为了保证 HMaster 的高可用,HBase 会启动多个 HMaster,并通过 ZooKeeper 选举出一个主服务器。

应用程序通过 ZooKeeper 获得主 HMaster 的地址,输入 Key 值获得这个 Key 所在的 HRegionServer 地址,然后请求 HRegionServer 上的 HRegion,获得所需要的数据。

HRegion 会把数据存储在若干个 HFile 格式的文件中,这些文件使用 HDFS 分布式文件系统存储,在整个集群内分布并高可用。当一个 HRegion 中数据量太多时,这个 HRegion 连同 HFile 会分裂成两个 HRegion,并根据集群中服务器负载进行迁移。如果集群中有新加入的服务器,也就是说有了新的 HRegionServer,由于其负载较低,也会把 HRegion 迁移过去并记录到 HMaster,从而实现 HBase 的线性伸缩。

HBase 可扩展数据模型

支持列族结构的 NoSQL 数据库,在创建表的时候,只需要指定列族的名字,无需指定字段(Column)。那什么时候指定字段呢?可以在数据写入时再指定。通过这种方式, 数据表可以包含数百万的字段,这样就可以随意扩展应用程序的数据结构了。并且这种数据库在查询时也很方便,可以通过指定任意字段名称和值进行查询。

HBase 这种列族的数据结构设计,实际上是把字段的名称和字段的值,以 Key-Value 的方式一起存储在 HBase 中。实际写入的时候,可以随意指定字段名称,即使有几百万个字段也能轻松应对。

HBase 的高性能存储

HBase 使用了一种叫作 LSM 树(Log 结构合并树)的数据结构进行数据存储。数据写入的时候以 Log 方式连续写入,然后异步对磁盘上的多个 LSM 树进行合并。

LSM 树可以看作是一个 N 阶合并树。数据写操作(包括插入、修改、删除)都在内存中进行,并且都会创建一个新记录(修改会记录新的数据值,而删除会记录一个删除标志)。这些数据在内存中仍然还是一棵排序树,当数据量超过设定的内存阈值后,会将这棵排序树和磁盘上最新的排序树合并。当这棵排序树的数据量也超过设定阈值后,会和磁盘上下一级的排序树合并。合并过程中,会用最新更新的数据覆盖旧的数据(或者记录为不同版本)。

15 | 流式计算的代表:Storm、Flink、Spark Streaming

16 | ZooKeeper 是如何保证数据一致性的?

分布式系统中的“脑裂”是指一个系统中的节点被分隔成两个或多个独立的部分,这些部分无法互相通信,导致系统出现不一致性和数据丢失的问题。通常情况下,“脑裂”是由于网络故障、硬件故障或者软件故障等因素导致的。

包括 HDFS 在内的很多大数据技术都选择了使用 ZooKeeper 来解决多台服务器的状态一致性问题。

ZooKeeper 使用了一种叫 ZAB 算法来解决一致性问题。ZAB 可视为 Paxos 算法的一种简化方案。

17 丨模块答疑:这么多技术,到底都能用在什么场景里?

大数据技术在实际部署的时候,通常会部署在同一个集群中,也就是说,在由很多台服务器组成的服务器集群中,某台服务器可能运行着 HDFS 的 DataNode 进程,负责 HDFS 的数据存储;同时也运行着 Yarn 的 NodeManager,负责计算资源的调度管理;而 MapReduce、Spark、Storm、Flink 这些批处理或者流处理大数据计算引擎则通过 Yarn 的调度,运行在 NodeManager 的容器(container)里面。至于 Hive、Spark SQL 这些运行在 MapReduce 或者 Spark 基础上的大数据仓库引擎,在经过自身的执行引擎将 SQL 语句解析成 MapReduce 或者 Spark 的执行计划以后,一样提交给 Yarn 去调度执行。

模块三、大数据开发实践

18 | 如何自己开发一个大数据 SQL 引擎?

19 | Spark 的性能优化案例分析(上)

性能指标:

  • 响应时间:完成一次任务(请求)花费的时间。
  • 并发数:同时处理的任务数(请求数)。
  • 吞吐量:单位时间完成的任务数(请求数、事务数、查询数……)。
  • 性能计数器:System Load,线程数,进程数,CPU、内存、磁盘、网络使用率等。

Spark 性能优化可以分解为下面几步。

  1. 性能测试,观察 Spark 性能特性和资源(CPU、Memory、Disk、Net)利用情况。
  2. 分析、寻找资源瓶颈。
  3. 分析系统架构、代码,发现资源利用关键所在,思考优化策略。
  4. 代码、架构、基础设施调优,优化、平衡资源利用。
  5. 性能测试,观察系统性能特性,是否达到优化目的,以及寻找下一个瓶颈点。

20 | Spark 的性能优化案例分析(下)

21 | 从阿里内部产品看海量数据处理系统的设计(上):Doris 的立项

22 | 从阿里内部产品看海量数据处理系统的设计(下):架构与创新

23 | 大数据基准测试可以带来什么好处?

大数据基准测试工具:

HiBench

24 丨从大数据性能测试工具 Dew 看如何快速开发大数据系统

25 | 模块答疑:我能从大厂的大数据开发实践中学到什么?

学习层次

  1. 练习
  2. 应用
  3. 开发

模块四、大数据平台与系统集成

26 | 互联网产品 + 大数据产品 = 大数据平台

  • 数据采集:数据库同步通常用 Sqoop,日志同步可以选择 Flume,打点采集的数据经过格式化转换后通过 Kafka 等消息队列进行传递
  • 数据处理:离线计算:MapReduce、Hive、Spark;实时计算:Storm、Spark Streaming、Flink
  • 数据展示:Lambda 架构

27 | 大数据从哪里来?

大数据平台的数据来源主要有数据库、日志、前端程序埋点、爬虫系统。

  • 数据库导入
    • Sqoop:Sqoop 是一个数据库批量导入导出工具,可以将关系数据库的数据批量导入到 Hadoop,也可以将 Hadoop 的数据导出到关系数据库。
    • Canal:Canal 是阿里巴巴开源的一个 MySQL binlog 获取工具,binlog 是 MySQL 的事务日志,可用于 MySQL 数据库主从复制,Canal 将自己伪装成 MySQL 从库,从 MySQL 获取 binlog。
  • 日志文件导入
    • Flume:Flume 是大数据日志收集常用的工具。
  • 前端程序埋点
    • 手动埋点
    • 自动埋点
  • 爬虫

28 | 知名大厂如何搭建大数据平台?

淘宝大数据平台

美团大数据平台

滴滴大数据平台

29 | 盘点可供中小企业参考的商业大数据平台

大数据解决方案提供商

CDH、TDH

CDH 是一个大数据集成平台,将主流大数据产品都集成到这个平台中,企业可以使用 CDH 一站式部署整个大数据技术栈。从架构分层角度,CDH 可以分为 4 层:系统集成,大数据存储,统一服务,过程、分析与计算。

  1. 系统集成:数据库导入导出用 Sqoop,日志导入导出用 Flume,其他实时数据导入导出用 Kafka。
  2. 大数据存储:文件系统用 HDFS,结构化数据用 Kudu,NoSQL 存储用 HBase,其他还有对象存储。
  3. 统一服务:资源管理用 Yarn,安全管理用 Sentry 和 RecordService 细粒度地管理不同用户数据的访问权限。
  4. 过程、分析与计算:批处理计算用 MapReduce、Spark、Hive、Pig,流计算用 SparkStreaming,快速 SQL 分析用 Impala,搜索服务用 Solr。

大数据云计算服务商

阿里云、亚马逊

大数据 SaaS 服务商

友盟、神策、百度统计

大数据开放平台

30 | 当大数据遇上物联网

  1. 智能网关通过消息队列将数据上传到物联网大数据平台,Storm 等流式计算引擎从消息队列获取数据,对数据的处理分三个方面。数据进行清理转换后写入到大数据存储系统。调用规则和机器学习模型,对上传数据进行计算,如果触发了某种执行规则,就将控制信息通过设备管理服务器下发给智能网关,并进一步控制终端智能设备。
  2. Spark 等离线计算引擎定时对写入存储系统的数据进行批量计算处理,进行全量统计分析和机器学习,并更新机器学习模型。
  3. 应用程序也可以通过设备管理服务器直接发送控制指令给智能网关,控制终端智能设备。

31 | 模块答疑:为什么大数据平台至关重要?

模块五、大数据分析与运营

32 | 互联网运营数据指标与可视化监控

运营常用数据指标

  • 新增用户数
  • 用户留存率
  • 活跃用户数
  • PV(Page View)
  • GMV(Gross Merchandise Volume),即成交总金额
  • 转化率 = 付费用户数 / 总用户数

33 丨一个电商网站订单下降的数据分析案例

34 丨 A-B 测试与灰度发布必知必会

A/B 测试的过程

A/B 测试的系统架构

A/B 测试系统最重要的是能够根据用户 ID(或者设备 ID)将实验配置参数分发给应用程序,应用程序根据配置参数决定给用户展示的界面和执行的业务逻辑

灰度发布

35 丨如何利用大数据成为“增长黑客”?

AARRR 用户增长模型:它描述了用户增长的 5 个关键环节,分别是:获取用户(Acquisition)、提高活跃度(Activation)、提高留存率(Retention)、获取收入(Revenue)和自传播(Refer)。

  • 获取用户:通过各种推广手段,使产品触达用户并吸引用户,让用户访问我们的产品。
  • 提高活跃度:用户访问我们的产品后,如果发现没意思、体验差,就很难再次打开,产品的价值也就无法实现。因此需要结合产品内容、运营活动各种手段吸引用户,提升产品的活跃度。
  • 提高留存率:留住一个老用户的成本远低于获取一个新用户,而真正为产品带来营收利润的通常是老用户,因此需要提高留存率。提高留存率的常用手段有:针对老用户推出各种优惠和活动;建立会员等级体系,注册时间越长等级越高;对于一段时间没有访问的疑似流失用户进行消息短信推送以实现用户挽回等。
  • 获取收入:做企业不是做慈善,开发、运营互联网产品的最终目的还是为了赚钱,即获取收入。互联网产品收入主要有用户付费和广告收入,有些互联网产品看起来是用户付费,但其实主要营收是广告收入,比如淘宝。
  • 自传播:让用户利用利用自己的社交网络进行产品推广就是自传播,几乎所有的互联网产品都有“分享到”这样一个功能按钮,促进用户社交传播。有些产品还会利用“帮我砍价”“帮我抢票”等产品功能推动用户进行分享,实现产品的裂变式传播、病毒式营销。

增长用户的手段主要有:

利用用户画像定位用户群体

  • 通过用户分析挽回用户
  • A/B 测试决定产品功能
  • 大数据反欺诈、反羊毛
  • 用户生命周期管理

36 丨模块答疑:为什么说数据驱动运营?

模块六、大数据算法

37 丨如何对数据进行分类和预测?

KNN 算法:KNN 算法,即 K 近邻(K Nearest Neighbour)算法,是一种基本的分类算法。其主要原理是:对于一个需要分类的数据,将其和一组已经分类标注好的样本集合进行比较,得到距离最近的 K 个样本,K 个样本最多归属的类别,就是这个需要分类数据的类别。

数据的距离:

  • 欧氏距离:计算空间距离
  • 余弦相似度:计算向量的夹角。更关注数据的相似性

文本的特征值:

  • TF-IDF 算法:TF 与 IDF 的乘积就是 TF-IDF。
    • TF 是词频(Term Frequency),表示某个单词在文档中出现的频率,一个单词在一个文档中出现的越频繁,TF 值越高。
    • IDF 是逆文档频率(Inverse Document Frequency),表示这个单词在所有文档中的稀缺程度,越少文档出现这个词,IDF 值越高。

贝叶斯分类:贝叶斯公式是一种基于条件概率的分类算法,如果我们已经知道 A 和 B 的发生概率,并且知道了 B 发生情况下 A 发生的概率,可以用贝叶斯公式计算 A 发生的情况下 B 发生的概率。

38 丨如何发掘数据之间的关系?

搜索排序:Google PageRank 算法

关联分析:

  • Apriori 算法:Apriori 算法极大地降低了需要计算的商品组合数目,这个算法的原理是,如果一个商品组合不满足最小支持度,那么所有包含这个商品组合的其他商品组合也不满足最小支持度。所以从最小商品组合,也就是一件商品开始计算最小支持度,逐渐迭代,进而筛选出所有满足最小支持度的频繁模式。其步骤如下:
    1. 设置最小支持度阈值。
    2. 寻找满足最小支持度的单件商品,也就是单件商品出现在所有订单中的概率不低于最小支持度。
    3. 从第 2 步找到的所有满足最小支持度的单件商品中,进行两两组合,寻找满足最小支持度的两件商品组合,也就是两件商品出现在同一个订单中概率不低于最小支持度。
    4. 从第 3 步找到的所有满足最小支持度的两件商品,以及第 2 步找到的满足最小支持度的单件商品进行组合,寻找满足最小支持度的三件商品组合。
    5. 以此类推,找到所有满足最小支持度的商品组合。

聚类:聚类就是对一批数据进行自动归类。

K-means 算法

39 丨如何预测用户的喜好?

基于人口统计的推荐:基于人口统计的推荐是相对比较简单的一种推荐算法,根据用户的基本信息进行分类,然后将商品推荐给同类用户。

基于产品属性的推荐:基于用户的属性进行分类,然后根据同类用户的行为进行推荐。而基于商品属性的推荐则是将商品的属性进行分类,然后根据用户的历史行为进行推荐。

基于用户的协同过滤推荐:基于用户的协同过滤推荐是根据用户的喜好进行用户分类,常用的就是 KNN 算法,寻找和当前用户喜好最相近的 K 个用户,然后根据这些用户的喜好为当前用户进行推荐。

基于商品的协同过滤推荐:根据用户的喜好对商品进行分类,如果两个商品,喜欢它们的用户具有较高的重叠性,就认为它们的距离相近,划分为同类商品,然后进行推荐

40 丨机器学习的数学原理是什么?

样本:样本就是通常我们常说的“训练数据”,包括输入和结果两部分。

模型:模型就是映射样本输入与样本结果的函数,可能是一个条件概率分布,也可能是一个决策函数。

算法:算法就是要从模型的假设空间中寻找一个最优的函数,使得样本空间的输入 X 经过该函数的映射得到的 f(X),和真实的 Y 值之间的距离最小。这个最优的函数通常没办法直接计算得到,即没有解析解,需要用数值计算的方法不断迭代求解。因此如何寻找到 f 函数的全局最优解,以及使寻找过程尽量高效,就构成了机器学习的算法。

41 丨从感知机到神经网络算法

42 丨模块答疑:软件工程师如何进入人工智能领域?

斯坦福大学的机器学习公开课

参考资料

Spring MVC 之视图技术

Spring MVC 中视图技术的使用是可插拔的。无论决定使用 Thymeleaf、Groovy 等模板引擎、JSP 还是其他技术,都可以通过配置来更改。

Spring MVC 的视图位于该应用程序的内部信任边界内。 视图可以访问应用程序上下文的所有 bean。 因此,不建议在模板可由外部源编辑的应用程序中使用 Spring MVC 的模板支持,因为这可能会产生安全隐患。

Thymeleaf

Thymeleaf 是一个现代服务器端 Java 模板引擎,它强调自然的 HTML 模板,可以通过双击在浏览器中预览,而无需运行服务器,这对于 UI 模板的独立工作(例如,由设计师)非常有帮助。

Thymeleaf 与 Spring MVC 的集成由 Thymeleaf 项目管理。 配置涉及一些 bean 声明,例如 ServletContextTemplateResolverSpringTemplateEngineThymeleafViewResolver。 有关详细信息,请参阅 Thymeleaf+Spring

FreeMarker

Apache FreeMarker 是一个模板引擎,用于生成从 HTML 到电子邮件等任何类型的文本内容。 Spring 框架内置了 Spring MVC 与 FreeMarker 模板结合使用的集成。

视图配置

以下示例显示了如何将 FreeMarker 配置为视图技术:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}

// Configure FreeMarker...

@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
return configurer;
}
}

以下示例显示了如何在 XML 中配置相同的内容:

1
2
3
4
5
6
7
8
9
10
<mvc:annotation-driven/>

<mvc:view-resolvers>
<mvc:freemarker/>
</mvc:view-resolvers>

<!-- Configure FreeMarker... -->
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/WEB-INF/freemarker"/>
</mvc:freemarker-configurer>

或者,您也可以声明 FreeMarkerConfigurer 以完全控制所有属性,如以下示例所示:

1
2
3
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
</bean>

您的模板需要存储在前面示例中所示的 FreeMarkerConfigurer 指定的目录中。鉴于前面的配置,如果您的控制器返回视图名称 welcome,解析器将查找 /WEB-INF/freemarker/welcome.ftl 模板。

FreeMarker 配置

可以通过在 FreeMarkerConfigurer 上设置适当的 bean 属性,将 FreeMarker ‘Settings’ 和 ‘SharedVariables’ 直接传递给 FreeMarker Configuration 对象(由 Spring 管理)。 freemarkerSettings 属性需要一个 java.util.Properties 对象,freemarkerVariables 属性需要一个 java.util.Map。 以下示例显示了如何使用 FreeMarkerConfigurer

1
2
3
4
5
6
7
8
9
10
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="freemarkerVariables">
<map>
<entry key="xml_escape" value-ref="fmXmlEscape"/>
</map>
</property>
</bean>

<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>

有关应用于 Configuration 对象的设置和变量的详细信息,请参阅 FreeMarker 文档。

表单处理

Spring 提供了一个用于 JSP 的标记库,其中包含一个 <spring:bind/> 元素。 此元素主要让表单显示来自表单支持对象的值,并显示来自 Web 或业务层中的“验证器”的验证失败的结果。 Spring 还支持 FreeMarker 中的相同功能,以及用于生成表单输入元素的额外便利宏。

绑定宏

在 FreeMarker 的 spring-webmvc.jar 文件中维护了一组标准宏,因此它们始终可用于适当配置的应用程序。

Spring 模板库中定义的一些宏被认为是内部的(私有的),但宏定义中不存在这样的范围,这使得所有宏对调用代码和用户模板都是可见的。以下部分仅关注您需要从模板中直接调用的宏。如果您想直接查看宏代码,该文件名为 spring.ftl ,位于 org.springframework.web.servlet.view.freemarker 包中。

简单绑定

在基于充当 Spring MVC 控制器表单视图的 FreeMarker 模板的 HTML 表单中,您可以使用类似于下一个示例的代码来绑定到字段值,并以类似于 JSP 等价物的方式为每个输入字段显示错误消息。以下示例显示了一个 personForm 视图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- FreeMarker macros have to be imported into a namespace.
We strongly recommend sticking to 'spring'. -->
<#import "/spring.ftl" as spring/>
<html>
...
<form action="" method="POST">
Name:
<@spring.bind "personForm.name"/>
<input type="text"
name="${spring.status.expression}"
value="${spring.status.value?html}"/><br />
<#list spring.status.errorMessages as error> <b>${error}</b> <br /> </#list>
<br />
...
<input type="submit" value="submit"/>
</form>
...
</html>

<@spring.bind> 需要一个 ‘path’ 参数,它由命令对象的名称(它是 ‘command’,除非您在控制器配置中更改它)组成,在您希望绑定的命令对象后跟一个句点和字段名称。 您还可以使用嵌套字段,例如 command.address.streetbind 宏采用 web.xml 中的 ServletContext 参数 defaultHtmlEscape 指定的默认 HTML 转义行为。

称为 <@spring.bindEscaped> 的宏的另一种形式采用第二个参数,该参数明确指定是否应在状态错误消息或值中使用 HTML 转义。 您可以根据需要将其设置为 truefalse 。 附加的表单处理宏简化了 HTML 转义的使用,您应该尽可能使用这些宏。

输入宏

FreeMarker 的附加便利宏简化了绑定和表单生成(包括验证错误显示)。 永远不需要使用这些宏来生成表单输入字段,您可以将它们与简单的 HTML 混合搭配,或者直接调用我们之前强调的 Spring 绑定宏。

下表中的可用宏显示了 FreeMarker 模板 (FTL) 定义和每个采用的参数列表:

macro FTL definition
message (output a string from a resource bundle based on the code parameter) <@spring.message code/>
messageText (output a string from a resource bundle based on the code parameter, falling back to the value of the default parameter) <@spring.messageText code, text/>
url (prefix a relative URL with the application’s context root) <@spring.url relativeUrl/>
formInput (standard input field for gathering user input) <@spring.formInput path, attributes, fieldType/>
formHiddenInput (hidden input field for submitting non-user input) <@spring.formHiddenInput path, attributes/>
formPasswordInput (standard input field for gathering passwords. Note that no value is ever populated in fields of this type.) <@spring.formPasswordInput path, attributes/>
formTextarea (large text field for gathering long, freeform text input) <@spring.formTextarea path, attributes/>
formSingleSelect (drop down box of options that let a single required value be selected) <@spring.formSingleSelect path, options, attributes/>
formMultiSelect (a list box of options that let the user select 0 or more values) <@spring.formMultiSelect path, options, attributes/>
formRadioButtons (a set of radio buttons that let a single selection be made from the available choices) <@spring.formRadioButtons path, options separator, attributes/>
formCheckboxes (a set of checkboxes that let 0 or more values be selected) <@spring.formCheckboxes path, options, separator, attributes/>
formCheckbox (a single checkbox) <@spring.formCheckbox path, attributes/>
showErrors (simplify display of validation errors for the bound field) <@spring.showErrors separator, classOrStyle/>

上述任何宏的参数具有一致的含义:

  • path: 要绑定到的字段的名称(例如,“command.name”)
  • options: 可在输入字段中选择的所有可用值的 Map。映射的键表示从表单回传并绑定到命令对象的值。针对键存储的 map 对象是在表单上显示给用户的标签,可能与表单回传的相应值不同。通常,这样的地图由控制器提供作为参考数据。您可以使用任何 Map 实现,具体取决于所需的行为。对于严格排序的映射,您可以使用带有合适的“比较器”的 SortedMap(例如 TreeMap),对于应按插入顺序返回值的任意映射,使用“LinkedHashMap”或“LinkedMap” 公共收藏
  • separator: 在多个选项可用作离散元素(单选按钮或复选框)的情况下,用于分隔列表中每个选项的字符序列(例如 <br>)。
  • attributes: 要包含在 HTML 标记本身中的任意标记或文本的附加字符串。该字符串按字面意思由宏回显。例如,在 textarea 字段中,您可以提供属性(例如“rows=”5” cols=”60”‘),或者您可以传递样式信息,例如 ‘style=”border:1px solid silver”‘。
  • classOrStyle: 对于 showErrors 宏,包装每个错误的 span 元素使用的 CSS 类的名称。如果未提供任何信息(或值为空),错误将包含在 <b></b> 标签中。

以下部分概述了宏的示例。

输入字段

formInput 宏采用 path 参数 (command.name) 和一个额外的 attributes 参数(在接下来的示例中为空)。该宏与所有其他表单生成宏一起对路径参数执行隐式 Spring 绑定。绑定在新绑定发生之前一直有效,因此 showErrors 宏不需要再次传递路径参数——它对上次创建绑定的字段进行操作。

showErrors 宏接受一个分隔符参数(用于分隔给定字段上的多个错误的字符),还接受第二个参数——这次是类名或样式属性。请注意,FreeMarker 可以为 attributes 参数指定默认值。以下示例显示了如何使用 formInputshowErrors 宏:

1
2
<@spring.formInput "command.name"/>
<@spring.showErrors "<br>"/>

下一个示例显示表单片段的输出,生成名称字段并在表单提交后显示验证错误,该字段中没有任何值。验证通过 Spring 的验证框架进行。

生成的 HTML 类似于以下示例:

1
2
3
4
5
6
Name:
<input type="text" name="name" value="">
<br>
<b>required</b>
<br>
<br>

formTextarea 宏的工作方式与 formInput 宏相同,并且接受相同的参数列表。通常,第二个参数 (attributes) 用于传递样式信息或 textarearowscols 属性。

选中字段

您可以使用四个选择字段宏在 HTML 表单中生成常见的 UI 值选择输入:

  • formSingleSelect
  • formMultiSelect
  • formRadioButtons
  • formCheckboxes

四个宏中的每一个都接受一个“Map”选项,其中包含表单字段的值和与该值对应的标签。值和标签可以相同。

下一个例子是 FTL 中的单选按钮。表单支持对象为此字段指定默认值“伦敦”,因此无需验证。渲染表单时,整个可供选择的城市列表作为参考数据提供在模型中,名称为 cityMap。以下清单显示了示例:

1
2
3
...
Town:
<@spring.formRadioButtons "command.address.town", cityMap, ""/><br><br>

前面的清单呈现一行单选按钮,一个用于 cityMap 中的每个值,并使用分隔符 ""。没有提供额外的属性(缺少宏的最后一个参数)。 cityMap 对地图中的每个键值对使用相同的 String。地图的键是表单实际作为 POST 请求参数提交的内容。地图值是用户看到的标签。在前面的示例中,给定三个知名城市的列表和表单支持对象中的默认值,HTML 类似于以下内容:

1
2
3
4
Town:
<input type="radio" name="address.town" value="London">London</input>
<input type="radio" name="address.town" value="Paris" checked="checked">Paris</input>
<input type="radio" name="address.town" value="New York">New York</input>

如果您的应用程序希望通过内部代码处理城市(例如),您可以使用合适的键创建代码映射,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
protected Map<String, ?> referenceData(HttpServletRequest request) throws Exception {
Map<String, String> cityMap = new LinkedHashMap<>();
cityMap.put("LDN", "London");
cityMap.put("PRS", "Paris");
cityMap.put("NYC", "New York");

Map<String, Object> model = new HashMap<>();
model.put("cityMap", cityMap);
return model;
}

代码现在生成输出,其中无线电值是相关代码,但用户仍然看到更用户友好的城市名称,如下所示:

1
2
3
4
Town:
<input type="radio" name="address.town" value="LDN">London</input>
<input type="radio" name="address.town" value="PRS" checked="checked">Paris</input>
<input type="radio" name="address.town" value="NYC">New York</input>

HTML 转义

前面描述的表单宏的默认使用导致 HTML 元素符合 HTML 4.01,并且使用 web.xml 文件中定义的 HTML 转义的默认值,如 Spring 的绑定支持所使用的那样。 要使元素符合 XHTML 或覆盖默认的 HTML 转义值,您可以在模板中指定两个变量(或在模型中,它们对模板可见)。 在模板中指定它们的好处是它们可以在稍后的模板处理中更改为不同的值,以便为表单中的不同字段提供不同的行为。

要为您的标签切换到 XHTML 合规性,请为名为 xhtmlCompliant 的模型或上下文变量指定 true 值,如以下示例所示:

1
2
<#-- for FreeMarker -->
<#assign xhtmlCompliant = true>

处理此指令后,Spring 宏生成的任何元素现在都符合 XHTML。

以类似的方式,您可以为每个字段指定 HTML 转义,如以下示例所示:

1
2
3
4
5
6
7
8
<#-- until this point, default HTML escaping is used -->

<#assign htmlEscape = true>
<#-- next field will use HTML escaping -->
<@spring.formInput "command.name"/>

<#assign htmlEscape = false in spring>
<#-- all future fields will be bound with HTML escaping off -->

Groovy

Groovy 标记模板引擎 主要用于生成类似 XML 的标记(XML、XHTML、HTML5 等),但可以使用它来生成任何基于文本的内容。 Spring Framework 具有将 Spring MVC 与 Groovy 标记结合使用的内置集成。

配置

以下示例显示如何配置 Groovy 标记模板引擎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}

// Configure the Groovy Markup Template Engine...

@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}

以下示例显示了如何在 XML 中配置相同的内容:

1
2
3
4
5
6
7
8
<mvc:annotation-driven/>

<mvc:view-resolvers>
<mvc:groovy/>
</mvc:view-resolvers>

<!-- Configure the Groovy Markup Template Engine... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>

示例

与传统的模板引擎不同,Groovy 标记依赖于使用构建器语法的 DSL。以下示例显示了 HTML 页面的示例模板:

1
2
3
4
5
6
7
8
9
10
yieldUnescaped '<!DOCTYPE html>'
html(lang:'en') {
head {
meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"')
title('My page')
}
body {
p('This is an example of HTML contents')
}
}

脚本视图

Spring 有一个内置的集成,可以将 Spring MVC 与任何可以在 JSR-223 之上运行的模板库一起使用 Java 脚本引擎。 我们在不同的脚本引擎上测试了以下模板库:

脚本库 脚本引擎
Handlebars Nashorn
Mustache Nashorn
React Nashorn
EJS Nashorn
ERB JRuby
String templates Jython
Kotlin Script templating Kotlin

要求

需要在类路径中包含脚本引擎,具体细节因脚本引擎而异:

  • The Nashorn Java 8+ 提供了 JavaScript 引擎。强烈建议使用可用的最新更新版本。
  • JRuby 应该作为 Ruby 支持的依赖项添加。
  • Jython 应该作为 Python 支持的依赖项添加。
  • org.jetbrains.kotlin:kotlin-script-util 依赖项和包含 org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory 行的 META-INF/services/javax.script.ScriptEngineFactory 文件应该被添加 Kotlin 脚本支持。 有关详细信息,请参阅此示例

您需要有脚本模板库。 为 JavaScript 做到这一点的一种方法是通过 WebJars

脚本模板

可以声明一个 ScriptTemplateConfigurer 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来渲染模板等等。 以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}

@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}

以下示例显示了 XML 中的相同配置:

1
2
3
4
5
6
7
8
9
<mvc:annotation-driven/>

<mvc:view-resolvers>
<mvc:script-template/>
</mvc:view-resolvers>

<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
<mvc:script location="mustache.js"/>
</mvc:script-template-configurer>

对于 Java 和 XML 配置,controller 看起来没有什么不同,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
@Controller
public class SampleController {

@GetMapping("/sample")
public String test(Model model) {
model.addAttribute("title", "Sample title");
model.addAttribute("body", "Sample body");
return "template";
}
}

以下示例显示了 Mustache 模板:

1
2
3
4
5
6
7
8
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>

使用以下参数调用渲染函数:

  • String template: 模板内容
  • 地图模型:视图模型
  • RenderingContext renderingContextRenderingContext 允许访问应用上下文、语言环境、模板加载器和 URL(自 5.0 起)

如果您的模板技术需要一些自定义,您可以提供一个实现自定义渲染功能的脚本。 例如,Handlerbars 需要在使用之前编译模板,并且需要一个 polyfill 来模拟一些浏览器工具,但在服务器端脚本引擎中不可用。

以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}

@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}

polyfill.js 只定义了 Handlebars 正常运行所需的 window 对象,如下:

1
var window = {}

这个基本的 render.js 实现在使用之前编译模板。 生产就绪的实现还应该存储任何重复使用的缓存模板或预编译模板。 您可以在脚本端这样做(并处理您需要的任何定制——管理模板引擎配置,例如)。 以下示例显示了如何执行此操作:

1
2
3
4
function render(template, model) {
var compiledTemplate = Handlebars.compile(template)
return compiledTemplate(model)
}

查看 Spring Framework 单元测试,Java资源,以获取更多配置示例。

JSP 和 JSTL

Spring Framework 具有将 Spring MVC 与 JSP 和 JSTL 结合使用的内置集成。

更多内容详见:Spring 官方文档之 JSP and JSTL

RSS and Atom

AbstractAtomFeedViewAbstractRssFeedView 都继承自 AbstractFeedView 基类,分别用于提供 Atom 和 RSS Feed 视图。 它们基于 ROME 项目,位于 org.springframework.web.servlet.view.feed 包中。

AbstractAtomFeedView 要求您实现 buildFeedEntries() 方法并可选择覆盖 buildFeedMetadata() 方法(默认实现为空)。 以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SampleContentAtomView extends AbstractAtomFeedView {

@Override
protected void buildFeedMetadata(Map<String, Object> model,
Feed feed, HttpServletRequest request) {
// implementation omitted
}

@Override
protected List<Entry> buildFeedEntries(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}

类似的要求适用于实现 AbstractRssFeedView,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SampleContentRssView extends AbstractRssFeedView {

@Override
protected void buildFeedMetadata(Map<String, Object> model,
Channel feed, HttpServletRequest request) {
// implementation omitted
}

@Override
protected List<Item> buildFeedItems(Map<String, Object> model,
HttpServletRequest request, HttpServletResponse response) throws Exception {
// implementation omitted
}
}

buildFeedItems()buildFeedEntries() 方法传入 HTTP 请求,以防您需要访问 Locale。 传入 HTTP 响应仅用于设置 cookie 或其他 HTTP 标头。 方法返回后,提要会自动写入响应对象。

有关创建 Atom 视图的示例,请参阅 Alef Arendsen 的 Spring Team 博客 entry

PDF and Excel

Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。

文档视图简介

HTML 页面并不总是用户查看模型输出的最佳方式,Spring 使从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。 该文档是视图,从服务器流出正确的内容类型,(希望)使客户端 PC 能够运行他们的电子表格或 PDF 查看器应用程序作为响应。

为了使用 Excel 视图,您需要将 Apache POI 库添加到类路径中。 对于 PDF 生成,您需要添加(最好)OpenPDF 库。

PDF 视图

单词列表的简单 PDF 视图可以扩展 org.springframework.web.servlet.view.document.AbstractPdfView 并实现 buildPdfDocument() 方法,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
public class PdfWordList extends AbstractPdfView {

protected void buildPdfDocument(Map<String, Object> model, Document doc, PdfWriter writer,
HttpServletRequest request, HttpServletResponse response) throws Exception {

List<String> words = (List<String>) model.get("wordList");
for (String word : words) {
doc.add(new Paragraph(word));
}
}
}

控制器可以从外部视图定义(按名称引用它)或作为处理程序方法的 View 实例返回此类视图。

Excel 视图

从 Spring Framework 4.2 开始,org.springframework.web.servlet.view.document.AbstractXlsView 作为 Excel 视图的基类提供。 它基于 Apache POI,具有专门的子类(AbstractXlsxViewAbstractXlsxStreamingView),取代了过时的 AbstractExcelView 类。

编程模型类似于 AbstractPdfView,以 buildExcelDocument() 作为核心模板方法,控制器能够从外部定义(按名称)或作为处理程序方法的 View 实例返回此类视图。

Jackson

Spring 提供对 Jackson JSON 库的支持。

基于 Jackson 的 JSON MVC 视图

MappingJackson2JsonView 使用 Jackson 库的 ObjectMapper 将响应内容渲染为 JSON。 默认情况下,模型映射的全部内容(特定于框架的类除外)都编码为 JSON。 对于需要过滤 map 内容的情况,您可以使用 modelKeys 属性指定一组特定的模型属性进行编码。 您还可以使用 extractValueFromSingleKeyModel 属性直接提取和序列化单键模型中的值,而不是作为模型属性的映射。

您可以根据需要使用 Jackson 提供的注释自定义 JSON 映射。 当您需要进一步控制时,您可以通过 ObjectMapper 属性注入自定义 ObjectMapper,适用于需要为特定类型提供自定义 JSON 序列化器和反序列化器的情况。

基于 Jackson 的 XML 视图

MappingJackson2XmlView 使用 Jackson XML 扩展 XmlMapper 将响应内容渲染为 XML。 如果模型包含多个条目,您应该使用 modelKey bean 属性显式设置要序列化的对象。 如果模型包含单个条目,它会自动序列化。

您可以根据需要使用 JAXB 或 Jackson 提供的注释自定义 XML 映射。当您需要进一步控制时,您可以通过 ObjectMapper 属性注入自定义 XmlMapper,对于需要为特定类型提供序列化器和反序列化器的自定义 XML 的情况

XML

MarshallingView 使用 XML Marshaller(在 org.springframework.oxm 包中定义)将响应内容渲染为 XML。 您可以使用 MarshallingView 实例的 modelKey 属性显式设置要编组的对象。 或者,视图遍历所有模型属性并编组 Marshaller 支持的第一个类型。 有关 org.springframework.oxm 包中功能的更多信息,请参阅 Marshalling XML using O/X Mappers

XSLT

XSLT 是 XML 的一种转换语言,作为 Web 应用程序中的一种视图技术很受欢迎。 如果您的应用程序自然地处理 XML,或者如果您的模型可以很容易地转换为 XML,那么 XSLT 作为一种视图技术是一个不错的选择。 以下部分展示了如何生成 XML 文档作为模型数据,并在 Spring Web MVC 应用程序中使用 XSLT 对其进行转换。

此示例是一个简单的 Spring 应用程序,它在 Controller 中创建关键字列表并将它们添加到模型映射中。 返回映射以及我们的 XSLT 视图的视图名称。 有关 Spring Web MVC 的 Controller 接口的详细信息,请参阅 Annotated Controllers。 XSLT 控制器将单词列表转换为准备转换的简单 XML 文档。

Beans

配置是一个简单的 Spring Web 应用程序的标准配置:MVC 配置必须定义一个 XsltViewResolver 和常规 MVC 注释配置。以下示例显示了如何执行此操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {

@Bean
public XsltViewResolver xsltViewResolver() {
XsltViewResolver viewResolver = new XsltViewResolver();
viewResolver.setPrefix("/WEB-INF/xsl/");
viewResolver.setSuffix(".xslt");
return viewResolver;
}
}

Controller

我们还需要一个控制器来封装我们的单词生成逻辑。

控制器逻辑封装在一个 @Controller 类中,处理方法定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller
public class XsltController {

@RequestMapping("/")
public String home(Model model) throws Exception {
Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument();
Element root = document.createElement("wordList");

List<String> words = Arrays.asList("Hello", "Spring", "Framework");
for (String word : words) {
Element wordNode = document.createElement("word");
Text textNode = document.createTextNode(word);
wordNode.appendChild(textNode);
root.appendChild(wordNode);
}

model.addAttribute("wordList", root);
return "home";
}
}

到目前为止,我们只创建了一个 DOM 文档并将其添加到模型映射中。请注意,您还可以将 XML 文件作为 Resource 加载并使用它来代替自定义 DOM 文档。

有可用的软件包可以自动 ‘domify’ 一个对象图,但是在 Spring 中,您可以完全灵活地以您选择的任何方式从您的模型创建 DOM。这可以防止 XML 的转换在模型数据的结构中发挥太大作用,这在使用工具管理 DOMification 过程时是一种危险。

Transformation

最后,XsltViewResolver 解析 “home” XSLT 模板文件并将 DOM 文档合并到其中以生成我们的视图。如 XsltViewResolver 配置所示,XSLT 模板位于 WEB-INF/xsl 目录下的 war 文件中,并以 xslt 文件扩展名结尾。

以下示例显示了 XSLT 转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:output method="html" omit-xml-declaration="yes"/>

<xsl:template match="/">
<html>
<head><title>Hello!</title></head>
<body>
<h1>My First Words</h1>
<ul>
<xsl:apply-templates/>
</ul>
</body>
</html>
</xsl:template>

<xsl:template match="word">
<li><xsl:value-of select="."/></li>
</xsl:template>

</xsl:stylesheet>

前面的转换渲染为以下 HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Hello!</title>
</head>
<body>
<h1>My First Words</h1>
<ul>
<li>Hello</li>
<li>Spring</li>
<li>Framework</li>
</ul>
</body>
</html>

参考资料

Spring MVC 之跨域

Spring MVC 支持跨域处理(CORS)。

简介

出于安全原因,浏览器禁止对当前源之外的资源进行 AJAX 调用。例如,可以在一个选项卡中使用您的银行帐户,而在另一个选项卡中使用 evil.com。来自 evil.com 的脚本不应该能够使用您的凭据向您的银行 API 发出 AJAX 请求——例如从您的账户中取款!

跨域(CORS)是由 大多数浏览器 实施的 W3C 规范,可让您指定哪种跨域请求是授权,而不是使用基于 IFRAME 或 JSONP 的不太安全和不太强大的解决方法。

处理

CORS 规范分为预检请求、简单请求和实际请求。要了解 CORS 的工作原理,可以阅读 Cross-Origin Resource Sharing (CORS) 等,或者查看规范了解更多详细信息。

Spring MVC HandlerMapping 实现提供了对 CORS 的内置支持。成功将请求映射到处理程序后,HandlerMapping 实现检查给定请求和处理程序的 CORS 配置并采取进一步的操作。预检请求被直接处理,而简单和实际的 CORS 请求被拦截、验证,并设置了所需的 CORS 响应标头。

为了启用跨源请求(即存在 Origin 标头并且与请求的主机不同),您需要有一些明确声明的 CORS 配置。如果未找到匹配的 CORS 配置,预检请求将被拒绝。没有 CORS 标头添加到简单和实际 CORS 请求的响应中,因此浏览器会拒绝它们。

每个 HandlerMapping 都可以[配置](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations- java.util.Map-) 单独使用基于 URL 模式的 CorsConfiguration 映射。 在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明此类映射,这会导致将单个全局映射传递给所有 HandlerMapping 实例。

可以将 HandlerMapping 级别的全局 CORS 配置与更细粒度的处理程序级别的 CORS 配置相结合。 例如,带注释的控制器可以使用类级或方法级的 @CrossOrigin 注释(其他处理程序可以实现 CorsConfigurationSource)。

The rules for combining global and local configuration are generally additive — for example, all global and all local origins. For those attributes where only a single value can be accepted, e.g. allowCredentials and maxAge, the local overrides the global value.

结合全局和局部配置的规则通常是附加的⟩——例如,所有全局和所有局部起源。 对于那些只能接受单个值的属性,例如 allowCredentialsmaxAge,局部覆盖全局值。

@CrossOrigin

@CrossOrigin 注解在带注解的 Controller 方法上启用跨源请求,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/account")
public class AccountController {

@CrossOrigin
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

默认,@CrossOrigin 允许访问:

  • 所以 origin
  • 所以 header
  • 所以 Controller 方法映射到的 HTTP 方法

allowCredentials 默认情况下不启用,因为它建立了一个信任级别,可以公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌),并且只应在适当的情况下使用。启用时,必须将 allowOrigins 设置为一个或多个特定域(但不是特殊值 "*"),或者 allowOriginPatterns 属性可用于匹配一组动态来源。

maxAge 单位为分钟

@CrossOrigin 也支持类级别,并且被所有方继承,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@CrossOrigin(origins = "https://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

可以同时在类级别和方法级别上使用 @CrossOrigin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

@CrossOrigin("https://domain2.com")
@GetMapping("/{id}")
public Account retrieve(@PathVariable Long id) {
// ...
}

@DeleteMapping("/{id}")
public void remove(@PathVariable Long id) {
// ...
}
}

全局配置

除了细粒度的控制器方法级别配置之外,您可能还想定义一些全局 CORS 配置。您可以在任何 HandlerMapping 上单独设置基于 URL 的 CorsConfiguration 映射。但是,大多数应用程序使用 MVC Java 配置或 MVC XML 命名空间来执行此操作。

默认情况下,全局配置启用以下功能:

  • 所以 origin
  • 所以 header
  • GETHEADPOST 方法

allowCredentials 默认情况下不启用,因为它建立了一个信任级别,可以公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌),并且只应在适当的情况下使用。启用时,必须将 allowOrigins 设置为一个或多个特定域(但不是特殊值 "*"),或者 allowOriginPatterns 属性可用于匹配一组动态来源。

maxAge 单位为分钟

Java 配置

要在 MVC Java 配置中启用 CORS,您可以使用 CorsRegistry 回调,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {

registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);

// Add more mappings...
}
}

XML 配置

要在 XML 命名空间中启用 CORS,可以使用 <mvc:cors> 元素,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
<mvc:cors>

<mvc:mapping path="/api/**"
allowed-origins="https://domain1.com, https://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="true"
max-age="123" />

<mvc:mapping path="/resources/**"
allowed-origins="https://domain1.com" />

</mvc:cors>

CORS 过滤器

可以通过 Spring 内置的 CorsFilter 支持 CORS。

要配置过滤器,请将 CorsConfigurationSource 传递给它的构造函数,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CorsConfiguration config = new CorsConfiguration();

// Possibly...
// config.applyPermitDefaultValues()

config.setAllowCredentials(true);
config.addAllowedOrigin("https://domain1.com");
config.addAllowedHeader("*");
config.addAllowedMethod("*");

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);

CorsFilter filter = new CorsFilter(source);

参考资料

Spring Web 应用

Spring MVC 提供了一种基于注解的编程模型,@Controller@RestController 组件使用注解来表达请求映射、请求输入、异常处理等。注解控制器具有灵活的方法签名,并且不必扩展基类或实现特定接口。以下示例显示了一个由注解定义的控制器:

1
2
3
4
5
6
7
8
9
@Controller
public class HelloController {

@GetMapping("/hello")
public String handle(Model model) {
model.addAttribute("message", "Hello World!");
return "index";
}
}

在前面的示例中,该方法接受一个 Model 并以 String 形式返回一个视图名称,但还存在许多其他选项。

快速入门

下面,通过一个简单的示例来展示如何通过 Spring 创建一个 Hello World Web 服务。

(1)pom.xml 中引入依赖

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

(2)定义 Controller

Spring 构建 RESTful 服务的方法,HTTP 请求由 Controller 处理。 这些组件由 @RestController 注解标识。

【示例】下面的示例定义了一个处理 /greeting 的 GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class GreetingController {

@GetMapping("/greeting")
public String greeting(@RequestParam(name = "name", required = false, defaultValue = "World") String name,
Model model) {
model.addAttribute("name", name);
return "greeting";
}

}

(3)创建启动类

1
2
3
4
5
6
7
8
9
10
11
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class HelloWorldApplication {

public static void main(String[] args) {
SpringApplication.run(HelloWorldApplication.class);
}

}

(4)启动服务:执行 HelloWorldApplication.main 方法启动 web 服务

(5)测试

打开浏览器,访问 http://localhost:8080/greeting,页面会显示如下内容:

1
Hello, World!

打开浏览器,访问 http://localhost:8080/greeting?name=dunwu,页面会显示如下内容:

1
Hello, dunwu!

Spring Web 组件

组件扫描

可以使用 Servlet 的 WebApplicationContext 中的标准 Spring bean 定义来定义控制器。@Controller 构造型允许自动检测,与 Spring 对检测类路径中的 @Component 类并为它们自动注册 bean 定义的一般支持保持一致。它还充当带注解类的构造型,表明其作为 Web 组件的角色。

要启用此类 @Controller 的自动检测,可以将组件扫描添加到您的 Java 配置中,如以下示例所示:

1
2
3
4
5
6
@Configuration
@ComponentScan("org.example.web")
public class WebConfig {

// ...
}

以下示例显示了与上述示例等效的 XML 配置:

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

<context:component-scan base-package="org.example.web"/>

<!-- ... -->

</beans>

AOP 代理

在某些情况下,可能需要在运行时使用 AOP 代理装饰控制器。一个例子是,如果选择直接在控制器上使用 @Transactional 注解。在这种情况下,特别是对于控制器,建议使用基于类的代理。直接在控制器上使用此类注解会自动出现这种情况。

如果控制器实现了一个接口,并且需要 AOP 代理,您可能需要显式配置基于类的代理。例如,对于 @EnableTransactionManagement ,可以更改为 @EnableTransactionManagement(proxyTargetClass = true),对于 <tx:annotation-driven/> ,您可以更改为 <tx:annotation-driven proxy-target-class="true"/>

@Controller

@RestController 是一个组合注解,它本身使用 @Controller@ResponseBody 元注解进行标记,以指示控制器的每个方法继承了类型级别的 @ResponseBody 注解,因此直接写入响应主体,而不是使用 HTML 模板进行视图解析和渲染。

@RequestMapping

可以使用 @RequestMapping 注解将请求映射到控制器方法。它具有各种属性,可以通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。可以在类级别使用它来表达共享映射,或者在方法级别使用它来缩小到特定端点的映射。

@RequestMapping 的主要参数:

  • path / method 指定映射路径与方法
  • params / headers 限定映射范围
  • consumes / produces 限定请求与响应格式

Spring 还提供了以下 @RequestMapping 的变体:

  • @GetMapping
  • @PostMapping
  • @PutMapping
  • @DeleteMapping
  • @PatchMapping

快捷方式是提供的自定义注解,因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 @RequestMapping,默认情况下,它与所有 HTTP 方法匹配。在类级别仍然需要 @RequestMapping 来表达共享映射。

以下示例具有类型和方法级别的映射:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RestController
@RequestMapping("/persons")
class PersonController {

@GetMapping("/{id}")
public Person getPerson(@PathVariable Long id) {
// ...
}

@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public void add(@RequestBody Person person) {
// ...
}
}

URI 模式

@RequestMapping 方法可以使用 URL 模式进行映射。有两种选择:

  • PathPattern - 与 URL 路径匹配的预解析模式也预解析为 PathContainer。该解决方案专为网络使用而设计,可有效处理编码和路径参数,并高效匹配。
  • AntPathMatcher - 根据字符串路径匹配字符串模式。这是在 Spring 配置中也使用的原始解决方案,用于在类路径、文件系统和其他位置选择资源。它的效率较低,并且字符串路径输入对于有效处理 URL 的编码和其他问题是一个挑战。

PathPattern 是 Web 应用程序的推荐解决方案,它是 Spring WebFlux 中的唯一选择。它从 5.3 版开始在 Spring MVC 中使用,从 6.0 版开始默认启用。请参阅 MVC 配置 以自定义路径匹配选项。

PathPattern 支持与 AntPathMatcher 相同的模式语法。此外,它还支持捕获模式,例如 {spring},用于匹配路径末尾的 0 个或多个路径段。PathPattern 还限制使用 ** 来匹配多个路径段,这样它只允许出现在模式的末尾。这消除了在为给定请求选择最佳匹配模式时出现的许多歧义。有关完整模式语法,请参阅 PathPatternAntPathMatcher

一些示例模式:

  • "/resources/ima?e.png" -匹配一个字符
  • "/resources/*.png" - 匹配零个或多个字符
  • "/resources/**" - 匹配多个字符
  • "/projects/{project}/versions" - 匹配路径段并将其捕获为变量
  • "/projects/{project:[a-z]+}/versions" - 使用正则表达式匹配并捕获变量

可以使用 @PathVariable 访问捕获的 URI 变量。例如:

1
2
3
4
@GetMapping("/owners/{ownerId}/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}

可以在类和方法级别声明 URI 变量,如以下示例所示:

1
2
3
4
5
6
7
8
9
@Controller
@RequestMapping("/owners/{ownerId}")
public class OwnerController {

@GetMapping("/pets/{petId}")
public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) {
// ...
}
}

URI 变量会自动转换为适当的类型,否则会引发 TypeMismatchException。默认支持简单类型(intlongDate 等),可以注册对任何其他数据类型的支持。请参见类型转换DataBinder

可以显式命名 URI 变量(例如,@PathVariable("customId")),但如果名称相同并且代码是使用 -parameters 编译器标志编译的,则可以省略该细节。

语法 {varName:regex} 使用正则表达式声明一个 URI 变量。例如,给定 URL "/spring-web-3.0.5.jar",以下方法提取名称、版本和文件扩展名:

1
2
3
4
@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}")
public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) {
// ...
}

URI 路径模式还可以嵌入 ${…} 占位符,这些占位符在启动时通过使用 PropertySourcesPlaceholderConfigurer 针对本地、系统、环境和其他属性源进行解析。例如,可以使用它来根据某些外部配置参数化基本 URL。

模式比较

当多个模式匹配一个 URL 时,必须选择最佳匹配。这是通过以下方式之一完成的,具体取决于是否启用了已解析的 PathPattern 以供使用:

两者都有助于对模式进行排序,更具体的模式位于顶部。如果模式具有较少的 URI 变量(计为 1)、单通配符(计为 1)和双通配符(计为 2),则模式不太具体。如果得分相同,则选择较长的模式。给定相同的分数和长度,选择 URI 变量多于通配符的模式。

默认映射模式 (/**) 被排除在评分之外并始终排在最后。此外,前缀模式(例如 /public/**)被认为不如其他没有双通配符的模式具体。

后缀匹配

从 5.3 开始,默认情况下 Spring MVC 不再执行 .* 后缀模式匹配,其中映射到 person 的控制器也隐式映射到 /person.*。因此,路径扩展不再用于解释请求的响应内容类型⟩——例如,/person.pdf/person.xml 等。

当浏览器过去发送难以一致解释的 Accept 请求头时,以这种方式使用文件扩展名是必要的。现在,这不再是必需的,使用 Accept 请求头应该是首选。

随着时间的推移,文件扩展名的使用在很多方面都被证明是有问题的。当使用 URI 变量、路径参数和 URI 编码覆盖时,它可能会导致歧义。关于基于 URL 的授权和安全性的推理也变得更加困难。

要在 5.3 之前的版本中完全禁用路径扩展,请设置以下内容:

除了通过 Accept 请求头之外,还有一种请求内容类型的方法仍然有用,例如在浏览器中键入 URL 时。路径扩展的一种安全替代方法是使用查询参数策略。如果您必须使用文件扩展名,请考虑通过 ContentNegotiationConfigurermediaTypes 属性将它们限制为明确注册的扩展名列表。

后缀匹配和 RFD

反射文件下载 (RFD) 攻击与 XSS 类似,因为它依赖于响应中反映的请求输入(例如,查询参数和 URI 变量)。然而,RFD 攻击不是将 JavaScript 插入 HTML,而是依赖于浏览器切换来执行下载,并在稍后双击时将响应视为可执行脚本。

在 Spring MVC 中,@ResponseBodyResponseEntity 方法存在风险,因为它们可以渲染不同的内容类型,客户端可以通过 URL 路径扩展请求这些内容类型。禁用后缀模式匹配并使用路径扩展进行内容协商可以降低风险,但不足以防止 RFD 攻击。

为了防止 RFD 攻击,在渲染响应主体之前,Spring MVC 添加了一个 Content-Disposition:inline;filename=f.txt 头以建议一个固定且安全的下载文件。仅当 URL 路径包含的文件扩展名既不安全也不明确注册用于内容协商时,才会执行此操作。但是,当 URL 直接输入浏览器时,它可能会产生副作用。

默认情况下,允许许多常见的路径扩展是安全的。具有自定义 HttpMessageConverter 实现的应用程序可以显式注册文件扩展名以进行内容协商,以避免为这些扩展名添加 Content-Disposition 头。请参阅 内容类型

关于 RFD 更多细节推荐参考 CVE-2015-5211

限定数据类型

您可以根据请求的 Content-Type 缩小请求映射,如以下示例所示:

1
2
3
4
@PostMapping(path = "/pets", consumes = "application/json")
public void addPet(@RequestBody Pet pet) {
// ...
}

consumes 属性还支持否定表达式 - 例如,!textplain 表示除 textplain 之外的任何内容类型。

您可以在类级别声明一个共享的 consumes 属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别的 consumes 属性会覆盖而不是扩展类级别的声明。

Producible Media Types

可以根据 Accept 请求头和控制器方法生成的内容类型列表来缩小请求映射,如以下示例所示:

1
2
3
4
5
@GetMapping(path = "/pets/{petId}", produces = "application/json")
@ResponseBody
public Pet getPet(@PathVariable String petId) {
// ...
}

媒体类型可以指定一个字符集。支持否定表达式——例如,!textplain 表示除 “text/plain” 之外的任何内容类型。

可以在类级别声明一个共享的 produces 属性。然而,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的 produces 属性会覆盖而不是扩展类级别的声明。

参数、请求头

可以根据请求参数条件缩小请求映射范围。可以测试是否存在请求参数 (myParam)、是否缺少请求参数 (!myParam) 或特定值 (myParam=myValue)。以下示例显示如何测试特定值:

1
2
3
4
@GetMapping(path = "/pets/{petId}", params = "myParam=myValue")
public void findPet(@PathVariable String petId) {
// ...
}

还可以使用相同的请求头条件,如以下示例所示:

1
2
3
4
@GetMapping(path = "/pets", headers = "myHeader=myValue")
public void findPet(@PathVariable String petId) {
// ...
}

HTTP HEAD, OPTIONS

@GetMapping(和 @RequestMapping(method=HttpMethod.GET))透明地支持 HTTP HEAD 以进行请求映射。控制器方法不需要改变。在 jakarta.servlet.http.HttpServlet 中应用的响应包装器确保将 Content-Length 头设置为写入的字节数(实际上没有写入响应)。

@GetMapping(和@RequestMapping(method=HttpMethod.GET))被隐式映射并支持 HTTP HEAD。HTTP HEAD 请求的处理方式就好像它是 HTTP GET 一样,除了不写入正文,而是计算字节数并设置 Content-Length 头。

默认情况下,通过将 Allow 响应头设置为所有具有匹配 URL 模式的 @RequestMapping 方法中列出的 HTTP 方法列表来处理 HTTP OPTIONS。

对于没有 HTTP 方法声明的 @RequestMappingAllow 头设置为 GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS。控制器方法应始终声明支持的 HTTP 方法(例如,通过使用 HTTP 方法特定变体:@GetMapping@PostMapping 等)。

You can explicitly map the @RequestMapping method to HTTP HEAD and HTTP OPTIONS, but that is not necessary in the common case.

可以显式地将 @RequestMapping 方法映射到 HTTP HEAD 和 HTTP OPTIONS,但在常见情况下这不是必需的。

自定义注解

Spring MVC 支持使用组合注解 进行请求映射。这些注解本身是用 @RequestMapping 进行元注解的,并且组合起来重新声明 @RequestMapping 属性的一个子集(或全部),具有更明确的目的。

@GetMapping@PostMapping@PutMapping@DeleteMapping@PatchMapping 是组合注解的示例。提供它们是因为,可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 @RequestMapping,默认情况下,它与所有 HTTP 方法匹配。如果您需要组合注解的示例,请查看这些注解的声明方式。

Spring MVC 还支持具有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,需要继承 RequestMappingHandlerMapping 并覆盖 getCustomMethodCondition 方法,您可以在其中检查自定义属性并返回您自己的 RequestCondition

显示注册

您可以以编程方式注册处理程序方法,您可以将其用于动态注册或高级情况,例如不同 URL 下的同一处理程序的不同实例。以下示例注册了一个处理程序方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class MyConfig {

@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler)
throws NoSuchMethodException {

RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build();

Method method = UserHandler.class.getMethod("getUser", Long.class);

mapping.registerMapping(info, handler, method);
}
}
  1. 为控制器注入目标处理程序和处理程序映射。

  2. 准备请求映射元数据。

  3. 获取处理程序方法。

  4. 添加注册。

处理方法

请求数据

  • @RequestParam

  • @RequestBody

  • @PathVariable

  • @RequestHeader

更多 Spring Web 方法参数可以参考: Method Arguments

响应数据

  • @ResponseBody

  • @ResponseStatus

  • ResponseEntity

  • HttpEntity

更多 Spring Web 方法返回值可以参考:Return Values

@ModelAttribute

可以使用 @ModelAttribute 注解:

  • @RequestMapping 方法中的方法参数上,用于模型创建或访问对象,并通过 WebDataBinder 将其绑定到请求。
  • 作为 @Controller@ControllerAdvice 类中的方法级注解,有助于在任何 @RequestMapping 方法调用之前初始化模型。
  • @RequestMapping 方法上标记它的返回值是一个模型属性。

本节讨论 @ModelAttribute 方法——前面列表中的第二项。一个控制器可以有任意数量的 @ModelAttribute 方法。所有这些方法都在同一控制器中的 @RequestMapping 方法之前被调用。@ModelAttribute 方法也可以通过 @ControllerAdvice 在控制器之间共享。

@ModelAttribute 方法具有灵活的方法签名。它们支持许多与 @RequestMapping 方法相同的参数,除了 @ModelAttribute 本身或与请求主体相关的任何内容。

以下示例显示了 @ModelAttribute 方法:

1
2
3
4
5
@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
model.addAttribute(accountRepository.findAccount(number));
// add more ...
}

以下示例仅添加一个属性:

1
2
3
4
@ModelAttribute
public Account addAccount(@RequestParam String number) {
return accountRepository.findAccount(number);
}

还可以将 @ModelAttribute 用作 @RequestMapping 方法上的方法级注解,在这种情况下,@RequestMapping 方法的返回值被解释为模型属性。这通常不是必需的,因为它是 HTML 控制器中的默认行为,除非返回值是一个 String 否则将被解释为视图名称。 @ModelAttribute 还可以自定义模型属性名称,如下例所示:

1
2
3
4
5
6
@GetMapping("/accounts/{id}")
@ModelAttribute("myAccount")
public Account handle() {
// ...
return account;
}

@InitBinder

@Controller@ControllerAdvice 类可以用 @InitBinder 方法来初始化 WebDataBinder 的实例,而这些方法又可以:

  • 将请求参数(即表单或查询数据)绑定到模型对象。
  • 将基于字符串的请求值(例如请求参数、路径变量、标头、cookie 等)转换为控制器方法参数的目标类型。
  • 在渲染 HTML 表单时将模型对象值格式化为 String 值。

@InitBinder 方法可以注册指定控制器 java.beans.PropertyEditor 或 Spring ConverterFormatter 组件。此外,您可以使用 MVC 配置 在全局共享的 FormattingConversionService 中注册 ConverterFormatter 类型。

@InitBinder 方法支持许多与 @RequestMapping 方法相同的参数,除了 @ModelAttribute(命令对象)参数。通常,它们使用 WebDataBinder 参数(用于注册)和 void 返回值声明。下面展示了一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
@Controller
public class FormController {

@InitBinder
public void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}

// ...
}

或者,当您通过共享的 FormattingConversionService 使用基于 Formatter 的设置时,您可以重复使用相同的方法并注册指定控制器的 Formatter 实现,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
@Controller
public class FormController {

@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd"));
}

// ...
}

在 Web 应用程序的上下文中,数据绑定涉及将 HTTP 请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象中的属性。

仅公开遵循 JavaBeans 命名约定public 属性用于数据绑定——例如,firstName 属性的 get/set 方法:public String getFirstName()public void setFirstName(String)

默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。这意味着您需要仔细考虑模型具有哪些公共属性,因为客户端可以将任何公共属性路径作为目标,甚至是一些预计不会针对给定用例的公共属性路径。

例如,给定一个 HTTP 表单数据端点,恶意客户端可以为存在于模型对象图中但不属于浏览器中显示的 HTML 表单的属性提供值。这可能导致在模型对象及其任何嵌套对象上设置数据,这些数据预计不会更新。

荐的方法是使用一个专用模型对象,它只公开与表单提交相关的属性。例如,在用于更改用户电子邮件地址的表单上,模型对象应声明最少的一组属性,例如以下 ChangeEmailForm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ChangeEmailForm {

private String oldEmailAddress;
private String newEmailAddress;

public void setOldEmailAddress(String oldEmailAddress) {
this.oldEmailAddress = oldEmailAddress;
}

public String getOldEmailAddress() {
return this.oldEmailAddress;
}

public void setNewEmailAddress(String newEmailAddress) {
this.newEmailAddress = newEmailAddress;
}

public String getNewEmailAddress() {
return this.newEmailAddress;
}

}

如果您不能或不想为每个数据绑定用例使用专用模型对象,则必须限制允许用于数据绑定的属性。理想情况下,可以通过 WebDataBinder 上的 setAllowedFields() 方法注册允许的字段模式 来实现这一点。

例如,要在您的应用程序中注册允许的字段模式,您可以在 @Controller@ControllerAdvice 组件中实现 @InitBinder 方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class ChangeEmailController {

@InitBinder
void initBinder(WebDataBinder binder) {
binder.setAllowedFields("oldEmailAddress", "newEmailAddress");
}

// @RequestMapping methods, etc.

}

除了注册允许的模式外,还可以通过 DataBinder及其子类中的 setDisallowedFields() 方法注册 _允许的字段模式_。但是请注意,“允许列表”比“拒绝列表”更安全。因此,setAllowedFields() 应该优于 setDisallowedFields()

请注意,匹配允许的字段模式是区分大小写的;然而,与不允许的字段模式匹配是不区分大小写的。此外,匹配不允许的模式的字段将不会被接受,即使它也恰好匹配允许列表中的模式。

表单处理

创建处理表单的 Controller

GreetingController 通过返回视图的名称处理 /greeting 的 GET 请求,这意味着返回的内容是名为 greeting.html 的视图内容。

greetingForm() 方法是通过使用 @GetMapping 专门映射到 GET 请求的,而 greetingSubmit() 是通过 @PostMapping 映射到 POST 请求的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class GreetingController {

@GetMapping("/greeting")
public String greetingForm(Model model) {
model.addAttribute("greeting", new Greeting());
return "greeting";
}

@PostMapping("/greeting")
public String greetingSubmit(@ModelAttribute Greeting greeting, Model model) {
model.addAttribute("greeting", greeting);
return "result";
}

}

定义需要提交的表单实体

1
2
3
4
5
6
7
8
9
10
import lombok.Data;

@Data
public class Greeting {

private long id;

private String content;

}

提交表单前端代码

提交实体的页面必须依赖某种视图技术,通过将视图名称转换为模板进行渲染,从而对HTML进行服务端渲染。在下面的例子中,使用了 Thymeleaf 模板引擎作为视图,它解析 greeting.html 的各种模板表达式以渲染表单。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Getting Started: Handling Form Submission</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<h1>Form</h1>
<form action="#" th:action="@{/greeting}" th:object="${greeting}" method="post">
<p>Id: <input type="text" th:field="*{id}" /></p>
<p>Message: <input type="text" th:field="*{content}" /></p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
</body>
</html>

文件上传

创建文件上传处理 Controller

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
import java.io.IOException;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;

import com.example.uploadingfiles.storage.StorageFileNotFoundException;
import com.example.uploadingfiles.storage.StorageService;

@Controller
public class FileUploadController {

private final StorageService storageService;

@Autowired
public FileUploadController(StorageService storageService) {
this.storageService = storageService;
}

@GetMapping("/")
public String listUploadedFiles(Model model) throws IOException {

model.addAttribute("files", storageService.loadAll().map(
path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class,
"serveFile", path.getFileName().toString()).build().toUri().toString())
.collect(Collectors.toList()));

return "uploadForm";
}

@GetMapping("/files/{filename:.+}")
@ResponseBody
public ResponseEntity<Resource> serveFile(@PathVariable String filename) {

Resource file = storageService.loadAsResource(filename);
return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION,
"attachment; filename=\"" + file.getFilename() + "\"").body(file);
}

@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file,
RedirectAttributes redirectAttributes) {

storageService.store(file);
redirectAttributes.addFlashAttribute("message",
"You successfully uploaded " + file.getOriginalFilename() + "!");

return "redirect:/";
}

@ExceptionHandler(StorageFileNotFoundException.class)
public ResponseEntity<?> handleStorageFileNotFound(StorageFileNotFoundException exc) {
return ResponseEntity.notFound().build();
}

}

FileUploadController 类使用 @Controller 注解,以便 Spring 可以扫描并注册它。 每个方法都标有 @GetMapping@PostMapping ,将路径和 HTTP 操作映射到指定的控制器。

在这种情况下:

  • GET /:从 StorageService 中查找当前上传文件的列表,并将其加载到 Thymeleaf 模板中。 它使用 MvcUriComponentsBuilder 计算指向实际资源的链接。

  • GET /files/{filename}:加载资源(如果存在)并使用 Content-Disposition 响应标头将其发送到浏览器进行下载。

  • POST /:处理一个多部分的消息文件,并将其交给 StorageService 进行保存。

定义存储文件的 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.core.io.Resource;
import org.springframework.web.multipart.MultipartFile;

import java.nio.file.Path;
import java.util.stream.Stream;

public interface StorageService {

void init();

void store(MultipartFile file);

Stream<Path> loadAll();

Path load(String filename);

Resource loadAsResource(String filename);

void deleteAll();

}

一个加单的 StorageService 实现:

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.stereotype.Service;
import org.springframework.util.FileSystemUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.stream.Stream;

@Service
public class FileSystemStorageServiceImpl implements StorageService {

private final Path rootLocation;

@Autowired
public FileSystemStorageServiceImpl(StorageProperties properties) {
this.rootLocation = Paths.get(properties.getLocation());
}

@Override
public void deleteAll() {
FileSystemUtils.deleteRecursively(rootLocation.toFile());
}

@Override
public void init() {
try {
Files.createDirectories(rootLocation);
} catch (IOException e) {
throw new StorageException("Could not initialize storage", e);
}
}

@Override
public Path load(String filename) {
return rootLocation.resolve(filename);
}

@Override
public Stream<Path> loadAll() {
try {
return Files.walk(this.rootLocation, 1).filter(path -> !path.equals(this.rootLocation))
.map(this.rootLocation::relativize);
} catch (IOException e) {
throw new StorageException("Failed to read stored files", e);
}
}

@Override
public Resource loadAsResource(String filename) {
try {
Path file = load(filename);
Resource resource = new UrlResource(file.toUri());
if (resource.exists() || resource.isReadable()) {
return resource;
} else {
throw new StorageFileNotFoundException("Could not read file: " + filename);
}
} catch (MalformedURLException e) {
throw new StorageFileNotFoundException("Could not read file: " + filename, e);
}
}

@Override
public void store(MultipartFile file) {
String filename = StringUtils.cleanPath(file.getOriginalFilename());
try {
if (file.isEmpty()) {
throw new StorageException("Failed to store empty file " + filename);
}
if (filename.contains("..")) {
// This is a security check
throw new StorageException(
"Cannot store file with relative path outside current directory " + filename);
}
try (InputStream inputStream = file.getInputStream()) {
Files.copy(inputStream, this.rootLocation.resolve(filename), StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new StorageException("Failed to store file " + filename, e);
}
}

}

创建文件上传表单

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
<html xmlns:th="https://www.thymeleaf.org">
<body>

<div th:if="${message}">
<h2 th:text="${message}"/>
</div>

<div>
<form method="POST" enctype="multipart/form-data" action="/">
<table>
<tr><td>File to upload:</td><td><input type="file" name="file" /></td></tr>
<tr><td></td><td><input type="submit" value="Upload" /></td></tr>
</table>
</form>
</div>

<div>
<ul>
<li th:each="file : ${files}">
<a th:href="${file}" th:text="${file}" />
</li>
</ul>
</div>

</body>
</html>

文件上传限制

如果使用 Spring Boot,可以使用一些属性设置来调整其自动配置的 MultipartConfigElement

将以下属性添加到现有属性设置中(在 src/main/resources/application.properties 中):

1
2
spring.servlet.multipart.max-file-size=128KB
spring.servlet.multipart.max-request-size=128KB
  • spring.servlet.multipart.max-file-size 设置为 128KB,表示总文件大小不能超过 128KB。
  • spring.servlet.multipart.max-request-size 设置为 128KB,这意味着 multipart/form-data 的总请求大小不能超过 128KB。

异常处理

@ExceptionHandler

@Controller@ControllerAdvice 类可以用 @ExceptionHandler 方法来处理来自控制器方法的异常,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
@Controller
public class SimpleController {

// ...

@ExceptionHandler
public ResponseEntity<String> handle(IOException ex) {
// ...
}
}

异常可能与正在传播的顶级异常(例如,抛出直接的 IOException)或包装器异常中的嵌套原因(例如,包装在 IllegalStateException 中的 IOException)相匹配。从 5.3 开始,这可以匹配任意原因级别,而以前只考虑直接原因。

对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,根异常匹配通常优先于原因异常匹配。更具体地说,ExceptionDepthComparator 用于根据抛出的异常类型的深度对异常进行排序。

或者,注解声明可以缩小要匹配的异常类型,如以下示例所示:

1
2
3
4
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(IOException ex) {
// ...
}

您甚至可以使用具有非常通用的参数签名的特定异常类型列表,如以下示例所示:

1
2
3
4
@ExceptionHandler({FileSystemException.class, RemoteException.class})
public ResponseEntity<String> handle(Exception ex) {
// ...
}

通常建议您在参数签名中尽可能具体,以减少根本和原因异常类型之间不匹配的可能性。考虑将一个多重匹配方法分解为单独的 @ExceptionHandler 方法,每个方法通过其签名匹配一个特定的异常类型。

在多 @ControllerAdvice 安排中,建议在具有相应顺序优先级的 @ControllerAdvice 上声明您的主要根异常映射。虽然根异常匹配优于原因,但这是在给定控制器或 @ControllerAdvice 类的方法中定义的。这意味着优先级较高的 @ControllerAdvice 上的原因匹配优于优先级较低的 @ControllerAdvice 上的任何匹配(例如,root)。

最后但同样重要的是, @ExceptionHandler 方法实现可以选择通过以原始形式重新抛出给定异常实例来退出处理。这在您只对根级匹配或无法静态确定的特定上下文中的匹配感兴趣的情况下很有用。重新抛出的异常通过剩余的解析链传播,就好像给定的 @ExceptionHandler 方法一开始就不会匹配一样。

Spring MVC 中对 @ExceptionHandler 方法的支持建立在 DispatcherServlet 级别 HandlerExceptionResolver 机制上。

附录:

@ExceptionHandler 支持的参数

@ExceptionHandler 支持返回值

参考资料

Spring MVC 之过滤器

spring-web 模块提供了一些有用的 Filter:

表单内容过滤器

浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以使用 HTTP PUT、PATCH 和 DELETE。 Servlet API 需要 ServletRequest.getParameter*() 系列方法来支持仅对 HTTP POST 的表单字段访问。

spring-web 模块提供了 FormContentFilter 来拦截内容类型为 applicationx-www-form-urlencoded 的 HTTP PUT、PATCH、DELETE 请求,从请求体中读取表单数据,并包装 ServletRequest 通过 ServletRequest.getParameter() 系列方法使表单数据可用。

转发过滤器

当请求通过代理(如负载均衡器)时,主机、端口和方案可能会发生变化,这使得从客户端角度创建指向正确主机、端口和方案的链接成为一项挑战。

RFC 7239 定义了 Forwarded HTTP 头,代理可以使用它来提供有关原始请求的信息。还有其他非标准头,包括 X-Forwarded-HostX-Forwarded-PortX-Forwarded-ProtoX-Forwarded-SslX-Forwarded-Prefix

ForwardedHeaderFilter 是一个 Servlet 过滤器,它修改请求以便 a) 根据 Forwarded 头更改主机、端口和 scheme;b) 删除这些头以消除进一步的影响。该过滤器依赖于包装请求,因此它必须排在其他过滤器之前,例如 RequestContextFilter,它应该与修改后的请求一起使用,而不是原始请求。

Forwarded 头有安全考量,因为应用程序无法知道头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么应将信任边界处的代理配置为删除来自外部的不受信任的 Forwarded 头。还可以使用 removeOnly=true 配置 ForwardedHeaderFilter,在这种情况下它会删除但不使用头。

为了支持异步请求和错误分派,此过滤器应使用 DispatcherType.ASYNCDispatcherType.ERROR 进行映射。如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer(参见 Servlet Config),所有过滤器都会自动为所有调度类型注册。但是,如果通过 web.xml 或在 Spring Boot 中通过 FilterRegistrationBean 注册过滤器,请确保除了 DispatcherType.REQUEST 之外还包括 DispatcherType.ASYNCDispatcherType.ERROR

ETag 过滤器

ShallowEtagHeaderFilter 过滤器通过缓存写入响应的内容并从中计算 MD5 哈希来创建“浅”ETag。下次客户端发送时,它会做同样的事情,但它还会将计算值与 If-None-Match 请求标头进行比较,如果两者相等,则返回 304 (NOT_MODIFIED)。

此策略节省网络带宽但不节省 CPU,因为必须为每个请求计算完整响应。前面描述的控制器级别的其他策略可以避免计算。

此过滤器有一个 writeWeakETag 参数,该参数将过滤器配置为写入类似于以下内容的弱 ETag:W"02a2d595e6ed9a0b24f027f2b63b134d6"(如 RFC 7232 Section 2.3 中所定义)。

为了支持异步请求,这个过滤器必须用 DispatcherType.ASYNC 映射,这样过滤器才能延迟并成功生成一个 ETag 到最后最后一次异步调度。如果使用 Spring Framework 的 AbstractAnnotationConfigDispatcherServletInitializer,所有过滤器都会自动为所有调度类型注册。但是,如果通过 web.xml 或在 Spring Boot 中通过 FilterRegistrationBean 注册过滤器,请确保包含 DispatcherType.ASYNC

跨域过滤器

Spring MVC 通过控制器上的注解为 CORS 配置提供细粒度支持。但是,当与 Spring Security 一起使用时,建议依赖内置的 CorsFilter,它必须在 Spring Security 的过滤器链之前订阅。

参考资料

Spring MVC 之 DispatcherServlet

简介

DispatcherServlet 是 Spring MVC 框架的核心组件,负责将客户端请求映射到相应的控制器,然后调用控制器处理请求并返回响应结果

DispatcherServlet 工作原理

DispatcherServlet 工作流程

DispatcherServlet 的工作流程大致如下图所示:

img

  1. 接收 Http 请求:当客户端发送 HTTP 请求时,DispatcherServlet 接收该请求并将其传递给 Spring MVC 框架。
  2. **选择 Handler**:DispatcherServlet 会根据请求的 URL 找到对应的处理器映射器 HandlerMapping,该映射器会根据配置文件中的 URL 映射规则找到合适的处理器 Handler
    • 绑定属性DispatcherServlet 会根据程序的 web 初始化策略关联各种 Resolver,如:LocaleResolverThemeResolver 等。
    • DispatcherServlet 根据 <servlet-name>-servlet.xml 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以HandlerExecutionChain 对象的形式返回。
      • 将请求映射到处理程序以及用于预处理和后处理的拦截器列表。映射基于一些标准,其细节因 HandlerMapping 实现而异。
      • 两个主要的 HandlerMapping 实现是 RequestMappingHandlerMapping(支持 @RequestMapping 注释方法)和 SimpleUrlHandlerMapping(维护 URI 路径模式到处理程序的显式注册)。
  3. **选择 HandlerAdapter**: DispatcherServlet 根据获得的 Handler,选择一个合适的 HandlerAdapter
    • HandlerAdapter 帮助 DispatcherServlet 调用映射到请求的 Handler,而不管实际调用 Handler 的方式如何。例如,调用带注解的控制器需要解析注解。HandlerAdapter 的主要目的是保护 DispatcherServlet 免受此类细节的影响。
  4. Handler 处理请求DispatcherServlet 提取 Request 中的模型数据,填充 Handler 入参,由 HandlerAdapter 负责调用 HandlerController)。 在填充 Handler 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作:
    • HttpMessageConverter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。
    • 数据转换:对请求消息进行数据转换。如 String 转换成 IntegerDouble 等。
    • 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。
    • 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到 BindingResultError 中。
  5. 返回 ModelAndView 对象Handler 处理完请求后,会返回一个 ModelAndView 对象,其中包含了处理结果(Model)和视图(View)信息。
  6. **选择 ViewResolver 渲染 ModelAndView**:根据返回的 ModelAndView,选择一个适合的 ViewResolver,并将 ModelAndView 传递给 ViewResolver 进行渲染,最后将渲染后的结果返回给客户端。

DispatcherServlet 源码解读

前面介绍了 DispatcherServlet 的工作流程,下面通过核心源码解读,来加深对 DispatcherServlet 工作原理的理解

(1)onRefresh 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}

/**
* 初始化此 servlet 使用的策略对象
* 可以在子类中重写以初始化更多策略对象
*/
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}

(2)doService 方法

DispatcherServlet 的核心方法 doService 源码如下:

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
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);

// 在包含的情况下保留请求属性的快照,以便能够在包含后恢复原始属性
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}

// 设置请求属性(绑定各种 Resolver),使框架对象可用于处理程序和视图对象
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}

RequestPath previousRequestPath = null;
if (this.parseRequestPath) {
previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE);
ServletRequestPathUtils.parseAndCache(request);
}

try {
// 请求分发
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// 恢复原始属性快照,以防包含
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
if (this.parseRequestPath) {
ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request);
}
}
}

(3)doDispatch 方法

doService 中的核心方法是 doDispatch,负责分发请求。

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
78
79
80
81
82
83
84
85
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 检查是否为multipart请求,如果是,则解析参数
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 确定适配当前请求的 Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// 确定适配当前请求的 HandlerAdapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 如果 Handler 支持,则处理 last-modified 头
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

// 请求的前置处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 调用实际的 Handler 处理请求并返回 ModelAndView(有可能为 null)
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
// 请求的后置处理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 处理响应结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// 替代 postHandle 和 afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// 清理 multipart 请求所使用的资源
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

上下文层次结构

DispatcherServlet 需要一个 WebApplicationContextApplicationContext 的扩展类)用于它自己的配置。WebApplicationContext 有一个指向 ServletContext 和与之关联的 Servlet 的链接。它还绑定到 ServletContext,以便应用程序可以在 RequestContextUtils 上使用静态方法来查找 WebApplicationContext

对于多数应用程序来说,拥有一个 WebApplicationContext 单例就足够。也可以有一个上下文层次结构,其中有一个根 WebApplicationContext 在多个 DispatcherServlet(或其他 Servlet)实例之间共享,每个实例都有自己的子 WebApplicationContext 配置。

WebApplicationContext 通常包含基础结构 bean,例如需要跨多个 Servlet 实例共享的数据存储和业务服务。这些 bean 是有效继承的,并且可以在特定 Servlet 的子 WebApplicationContext 中被覆盖(即重新声明),它通常包含指定 Servlet 的本地 bean。下图显示了这种关系:

【示例】配置 WebApplicationContext 层次结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}

@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}

【示例】web.xml 方式配置 WebApplicationContext 层次结构:

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

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>

<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>

</web-app>

配置

DispatcherServlet 与其他 Servlet 一样,需要使用 Java 配置或在 web.xml 中根据 Servlet 规范进行声明和映射。也就是说,DispatcherServlet 使用 Spring 配置来发现请求映射、视图解析、异常处理等所需的委托组件。

可以通过将 Servlet 初始化参数(init-param 元素)添加到 web.xml 文件中的 Servlet 声明来自定义各个 DispatcherServlet 实例。下表列出了支持的参数:

参数 说明
contextClass 实现 ConfigurableWebApplicationContext 的类,将由此 Servlet 实例化和本地配置。默认情况下,使用 XmlWebApplicationContext
contextConfigLocation 传递给上下文实例(由 contextClass 指定)以指示可以在何处找到上下文的字符串。该字符串可能包含多个字符串(使用逗号作为分隔符)以支持多个上下文。在具有两次定义的 bean 的多个上下文位置的情况下,最新的位置优先。
namespace WebApplicationContext 的命名空间。默认为 [servlet-name]-servlet
throwExceptionIfNoHandlerFound 当找不到请求的处理程序时是否抛出 NoHandlerFoundException。然后可以使用 HandlerExceptionResolver(例如,通过使用 @ExceptionHandler 控制器方法)捕获异常并像其他任何方法一样处理。默认情况下,它设置为 false,在这种情况下,DispatcherServlet 设置响应状态为 404 (NOT_FOUND) 而不会引发异常。请注意,如果 [默认 servlet 处理](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc -default-servlet-handler) 也被配置,未解决的请求总是转发到默认的 servlet 并且永远不会引发 404。

应用程序可以声明处理请求所需的特殊 Bean 类型中列出的基础结构 bean。DispatcherServlet 检查每个特殊 bean 的 WebApplicationContext。如果没有匹配的 bean 类型,它将回退到 DispatcherServlet.properties 中列出的默认类型。

在大多数情况下,MVC 配置是最好的起点。它以 Java 或 XML 声明所需的 bean,并提供更高级别的配置回调 API 来对其进行自定义。

注意:Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC,并提供了许多额外的方便选项。

在 Servlet 环境中,您可以选择以编程方式配置 Servlet 容器作为替代方案或与 web.xml 文件结合使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.web.WebApplicationInitializer;

public class MyWebApplicationInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}

WebApplicationInitializer 是 Spring MVC 提供的接口,可确保检测到自定义的实现并自动用于初始化任何 Servlet 3 容器。名为 AbstractDispatcherServletInitializerWebApplicationInitializer 的抽象基类实现通过覆盖方法来指定 servlet 映射和 DispatcherServlet 配置的位置,使得注册 DispatcherServlet 变得更加容易。

对于使用基于 Java 的 Spring 配置的应用程序,建议这样做,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}

@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}

@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}

如果使用基于 XML 的 Spring 配置,则应直接从 AbstractDispatcherServletInitializer 扩展,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}

@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}

@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}

AbstractDispatcherServletInitializer 还提供了一种方便的方法来添加 Filter 实例并将它们自动映射到 DispatcherServlet,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {

// ...

@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}

每个过滤器都根据其具体类型添加一个默认名称,并自动映射到 DispatcherServlet

AbstractDispatcherServletInitializerisAsyncSupported 保护方法提供了一个单独的位置来启用 DispatcherServlet 和映射到它的所有过滤器的异步支持。默认情况下,此标志设置为 true。

最后,如果需要进一步自定义 DispatcherServlet 本身,可以重写 createDispatcherServlet 方法。

【示例】Java 方式注册并初始化 DispatcherServlet,它由 Servlet 容器自动检测(请参阅 Servlet Config):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyWebApplicationInitializer implements WebApplicationInitializer {

@Override
public void onStartup(ServletContext servletContext) {

// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);

// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}

【示例】web.xml 方式注册并初始化 DispatcherServlet

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

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>

<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>

</web-app>

路径匹配

Servlet API 将完整的请求路径公开为 requestURI,并将其进一步细分为 contextPathservletPathpathInfo,它们的值因 Servlet 的映射方式而异。从这些输入中,Spring MVC 需要确定用于映射处理程序的查找路径,如果适用,它应该排除 contextPath 和任何 servletMapping 前缀。

servletPathpathInfo 已解码,这使得它们无法直接与完整的 requestURI 进行比较以派生 lookupPath,因此有必要对 requestURI 进行解码。然而,这引入了它自己的问题,因为路径可能包含编码的保留字符,例如 "/"";" 这反过来又会在解码后改变路径的结构,这也可能导致安全问题。此外,Servlet 容器可能会在不同程度上规范化 servletPath,这使得进一步无法对 requestURI 执行 startsWith 比较。

这就是为什么最好避免依赖基于前缀的 servletPath 映射类型附带的 servletPath。如果 DispatcherServlet 被映射为带有 "/" 的默认 Servlet,或者没有带 "/*" 的前缀,并且 Servlet 容器是 4.0+,则 Spring MVC 能够检测 Servlet 映射类型,并避免使用 servletPathpathInfo。在 3.1 Servlet 容器上,假设相同的 Servlet 映射类型,可以通过在 MVC 配置中通过路径匹配提供一个带有 alwaysUseFullPath=trueUrlPathHelper 来实现等效。

幸运的是,默认的 Servlet 映射 "/" 是一个不错的选择。但是,仍然存在一个问题,即需要对 requestURI 进行解码才能与控制器映射进行比较。这也是不可取的,因为可能会解码改变路径结构的保留字。如果不需要这样的字符,那么您可以拒绝它们(如 Spring Security HTTP 防火墙),或者您可以使用 urlDecode=false 配置 UrlPathHelper,但控制器映射需要与编码路径匹配,这可能并不总是有效。此外,有时 DispatcherServlet 需要与另一个 Servlet 共享 URL 空间,并且可能需要通过前缀进行映射。

在使用 PathPatternParser 和解析模式时解决了上述问题,作为使用 AntPathMatcher 进行字符串路径匹配的替代方法。PathPatternParser 从 5.3 版本开始就可以在 Spring MVC 中使用,并且从 6.0 版本开始默认启用。与需要解码查找路径或编码控制器映射的 AntPathMatcher 不同,解析的 PathPattern 与称为 RequestPath 的路径的解析表示匹配,一次一个路径段。这允许单独解码和清理路径段值,而没有改变路径结构的风险。解析的 PathPattern 也支持使用 servletPath 前缀映射,只要使用 Servlet 路径映射并且前缀保持简单,即它没有编码字符。

拦截器

所有 HandlerMapping 实现都支持处理拦截器,当想要将特定功能应用于某些请求时,这些拦截器很有用——例如,检查主体。拦截器必须使用 org.springframework.web.servlet 包中的三个方法实现 HandlerInterceptor,这三个方法应该提供足够的灵活性来进行各种预处理和后处理:

  • preHandle(..):在实际 handler 之前执行
  • postHandle(..):handler 之后执行
  • afterCompletion(..):完成请求后执行

preHandle(..) 方法返回一个布尔值。可以使用此方法中断或继续执行链的处理。当此方法返回 true 时,处理程序执行链将继续。当它返回 false 时,DispatcherServlet 假定拦截器本身已经处理请求(并且,例如,呈现适当的视图)并且不会继续执行其他拦截器和执行链中的实际处理程序。

有关如何配置拦截器的示例,请参阅 MVC 配置部分中的拦截器。还可以通过在各个 HandlerMapping 实现上使用 setter 来直接注册它们。

postHandle 方法对于 @ResponseBodyResponseEntity 的方法不太有用,它们的响应是在 HandlerAdapter 中和 postHandle 之前编写和提交的。这意味着对响应进行任何更改都为时已晚,例如添加额外的标头。对于此类场景,您可以实现 ResponseBodyAdvice 并将其声明为 Controller Advice bean 或直接在 RequestMappingHandlerAdapter 上进行配置。

解析器

DispatcherServlet 会加载多种解析器来处理请求,比较常见的有以下几个:

  • HandlerExceptionResolver - 解决异常的策略,可能将它们映射到处理程序、HTML 错误视图或其他目标。
  • ViewResolver - 将从处理程序返回的基于字符串的逻辑视图名称解析为用于呈现响应的实际视图。
  • LocaleResolver, LocaleContextResolver - 解析用户正在使用的本地化设置,可能还有他们的时区,以便能够提供国际化的视图。
  • ThemeResolver - 解析 Web 应用程序可以使用的主题——例如,提供个性化布局。
  • MultipartResolver - 通过一些 multipart 解析库的帮助解析 multipart 请求(例如,通过浏览器上传文件)。

HandlerExceptionResolver

WebApplicationContext 中声明的 HandlerExceptionResolver 用于解决请求处理期间抛出的异常。这些异常解析器允许自定义逻辑来解决异常。

对于 HTTP 缓存支持,处理程序可以使用 WebRequestcheckNotModified 方法,以及用于控制器的 HTTP 缓存中所述的带注释控制器的更多选项。

如果在请求映射期间发生异常或从请求处理程序(例如 @Controller)抛出异常,则 DispatcherServlet 委托 HandlerExceptionResolver 链来解决异常并提供替代处理,这通常是错误响应。

下表列出了可用的 HandlerExceptionResolver 实现:

HandlerExceptionResolver 说明
SimpleMappingExceptionResolver 异常类名称和错误视图名称之间的映射。用于在浏览器应用程序中呈现错误页面。
DefaultHandlerExceptionResolver 解决由 Spring MVC 引发的异常并将它们映射到 HTTP 状态代码。
ResponseStatusExceptionResolver 使用 @ResponseStatus 注解解决异常,并根据注解中的值将它们映射到 HTTP 状态代码。
ExceptionHandlerExceptionResolver 通过在 @Controller@ControllerAdvice 类中调用 @ExceptionHandler 方法来解决异常。

解析器链

您可以通过在 Spring 配置中声明多个 HandlerExceptionResolver bean 并根据需要设置它们的顺序属性来构成异常解析器链。order 属性越高,异常解析器的位置就越靠后。

HandlerExceptionResolver 的约定使它可以返回以下内容:

  • 指向错误视图的 ModelAndView

  • 如果异常是在解析器中处理的,则为空的 ModelAndView

  • 如果异常仍未解决,则为 null,供后续解析器尝试,如果异常仍然存在,则允许向上冒泡到 Servlet 容器。

MVC Config 自动为默认的 Spring MVC 异常、@ResponseStatus 注释的异常和对 @ExceptionHandler 方法的支持声明内置解析器。您可以自定义该列表或替换它。

错误页面

如果异常仍未被任何 HandlerExceptionResolver 处理并因此继续传播,或者如果响应状态设置为错误状态(即 4xx、5xx),Servlet 容器可以在 HTML 中呈现默认错误页面。要自定义容器的默认错误页面,您可以在 web.xml 中声明一个错误页面映射。以下示例显示了如何执行此操作:

1
2
3
<error-page>
<location>/error</location>
</error-page>

在前面的示例中,当出现异常或响应具有错误状态时,Servlet 容器会在容器内将 ERROR 分派到配置的 URL(例如,/error)。然后由 DispatcherServlet 处理,可能将其映射到 @Controller,后者可以返回带有模型的错误视图名称或呈现 JSON 响应,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class ErrorController {

@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<String, Object>();
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}

提示:Servlet API 不提供在 Java 中创建错误页面映射的方法。但是,您可以同时使用 WebApplicationInitializer 和最小的 web.xml

ViewResolver

Spring MVC 定义了 ViewResolverView 接口,让用户可以在浏览器中渲染模型,而无需限定于特定的视图技术。ViewResolver 提供视图名称和实际视图之间的映射。View 解决了在移交给特定视图技术之前准备数据的问题。

下表提供了有关 ViewResolver 一些子类:

ViewResolver Description
AbstractCachingViewResolver AbstractCachingViewResolver 的子类缓存它们解析的视图实例。缓存提高了某些视图技术的性能。您可以通过将 cache 属性设置为 false 来关闭缓存。此外,如果您必须在运行时刷新某个视图(例如,修改 FreeMarker 模板时),您可以使用 removeFromCache(String viewName, Locale loc) 方法。
UrlBasedViewResolver ViewResolver 接口的简单实现,无需显式映射定义即可将逻辑视图名称直接解析为 URL。如果您的逻辑名称以直接的方式匹配您的视图资源的名称,而不需要任意映射,那么这是合适的。
InternalResourceViewResolver UrlBasedViewResolver 的子类,支持 InternalResourceView(实际上是 Servlet 和 JSP)以及 JstlViewTilesView 等子类。可以使用 setViewClass(..) 为该解析器生成的所有视图指定视图类。
FreeMarkerViewResolver UrlBasedViewResolver 的子类,支持 FreeMarkerView 和它们的自定义子类。
ContentNegotiatingViewResolver ViewResolver 接口的实现,该接口根据请求文件名或 Accept 标头解析视图。
BeanNameViewResolver 将视图名称解释为当前应用程序上下文中的 bean 名称的 ViewResolver 接口的实现。这是一个非常灵活的变体,允许根据不同的视图名称混合和匹配不同的视图类型。每个这样的“视图”都可以定义为一个 bean,例如 在 XML 或配置类中。

处理

可以通过声明多个解析器来构成视图解析器链,如果需要,还可以通过设置 order 属性来指定顺序。顺序属性越高,视图解析器在链中的位置就越靠后。

ViewResolver 的约定指定它可以返回 null 以指示找不到视图。但是,对于 JSP 和 InternalResourceViewResolver,确定 JSP 是否存在的唯一方法是通过 RequestDispatcher 执行分派。因此,您必须始终将 InternalResourceViewResolver 配置为在视图解析器的整体顺序中排在最后。

配置视图解析就像将 ViewResolver 添加到 Spring 配置中一样简单。MVC Config 为视图解析器和添加无逻辑视图控制器提供了专用的配置 API,这对于没有控制器逻辑的 HTML 模板渲染很有用。

重定向

视图名称中的特殊前缀 redirect: 可以实现一个重定向。UrlBasedViewResolver(及其子类)将此识别为需要重定向的指令。视图名称的其余部分是重定向 URL。

最终效果与控制器返回 RedirectView 相同,但现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图名称(例如 redirect:/myapp/some/resource)相对于当前 Servlet 上下文重定向,而名称(例如 redirect:https://myhost.com/some/arbitrary/path)重定向到绝对 URL。

请注意,如果使用 @ResponseStatus 注解标记控制器方法,则注解值优先于 RedirectView 设置的响应状态。

转发

视图名称中的特殊前缀 forward: 可以实现一个转发。这将创建一个 InternalResourceView,它执行 RequestDispatcher.forward()。因此,此前缀对 InternalResourceViewResolverInternalResourceView(对于 JSP)没有用,但如果您使用另一种视图技术但仍想强制转发由 Servlet/JSP 引擎处理的资源,它可能会有所帮助。

内容协商

ContentNegotiatingViewResolver 本身不解析视图,而是委托给其他视图解析器并选择类似于客户端请求的表示的视图。可以从 Accept 头或查询参数(例如,"/path?format=pdf")确定表示形式。

ContentNegotiatingViewResolver 通过将请求媒体类型与其每个 ViewResolver 关联的 View 支持的媒体类型(也称为 Content-Type)进行比较,来选择合适的 View 来处理请求。列表中第一个具有兼容 Content-Type 的视图将处理结果返回给客户端。如果 ViewResolver 链无法提供兼容的视图,则会查阅通过 DefaultViews 属性指定的视图列表。后一个选项适用于单例视图,它可以呈现当前资源的适当表示,而不管逻辑视图名称如何。Accept 标头可以包含通配符(例如 text/*),在这种情况下,Content-Typetext/xml 的 View 是兼容的匹配项。

LocaleResolver

大部分的 Spring 架构都支持国际化,就像 Spring web MVC 框架所做的那样。DispatcherServlet 允许您使用客户端的语言环境自动解析消息。这是通过 LocaleResolver 对象完成的。

当收到请求时,DispatcherServlet 会寻找语言环境解析器,如果找到,它会尝试使用它来设置 Locale 环境。通过使用 RequestContext.getLocale() 方法,您始终可以检索由 Locale 解析器解析的语言环境。

除了自动识别 Locale 环境之外,您还可以为 handle 映射附加拦截器,在特定情况下更改 Locale 环境设置(例如,基于请求中的参数)。

Locale 解析器和拦截器在 org.springframework.web.servlet.i18n 包中定义,并以正常方式在您的应用程序上下文中配置。Spring 中有以下 Locale 解析器可供选择。

LocaleResolver

除了获取客户端的区域设置外,了解其时区通常也很有用。LocaleContextResolver 接口提供了 LocaleResolver 的扩展,让解析器提供更丰富的 LocaleContext,其中可能包括时区信息。

如果可用,可以使用 RequestContext.getTimeZone() 方法获取用户的 TimeZone。在 Spring 的 ConversionService 中注册的任何日期/时间 ConverterFormatter 对象会自动使用时区信息。

标头解析器

此 Locale 解析器检查客户端(例如网络浏览器)发送的请求中的 accept-language 头。通常,此头字段包含客户端操作系统的区域信息。请注意,此解析器不支持时区信息。

CookieLocaleResolver

This locale resolver inspects a Cookie that might exist on the client to see if a Locale or TimeZone is specified. If so, it uses the specified details. By using the properties of this locale resolver, you can specify the name of the cookie as well as the maximum age. The following example defines a CookieLocaleResolver:

此 Locale 解析器检查客户端上是否存在 Cookie,以查看是否指定了 LocaleTimeZone。如果是,它会使用指定的详细信息。通过使用此 Locale 解析器的属性,可以指定 cookie 的名称以及最长期限。以下示例定义了 CookieLocaleResolver

1
2
3
4
5
6
7
8
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">

<property name="cookieName" value="clientlanguage"/>

<!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
<property name="cookieMaxAge" value="100000"/>

</bean>

下表描述了 CookieLocaleResolver 的属性:

属性 默认值 Description
cookieName 类名 + LOCALE cookie 名
cookieMaxAge Servlet container default cookie 在客户端上保留的最长时间。如果指定了“-1”,则不会保留 cookie。它仅在客户端关闭浏览器之前可用。
cookiePath / 将 cookie 的可见性限制在您网站的特定部分。当指定 cookiePath 时,cookie 仅对该路径及其下方的路径可见。

SessionLocaleResolver

SessionLocaleResolver 允许您从可能与用户请求相关联的会话中检索 LocaleTimeZone。与 CookieLocaleResolver 相比,此策略将本地选择的 locale 设置存储在 Servlet 容器的 HttpSession 中。因此,这些设置对于每个会话都是临时的,因此会在每个会话结束时丢失。

注意,这与外部会话管理机制(例如 Spring Session 项目)没有直接关系。此 SessionLocaleResolver 根据当前 HttpServletRequest 评估和修改相应的 HttpSession 属性。

LocaleChangeInterceptor

可以通过将 LocaleChangeInterceptor 添加到一个 HandlerMapping 定义来启用区域设置更改。它检测请求中的参数并相应地更改 Locale 环境,在调度程序的应用程序上下文中调用 LocaleResolver 上的 setLocale 方法。下面的示例显示调用所有包含名为 siteLanguage 的参数的 *.view 资源,以更改语言环境。因此,例如,对 URL https://www.sf.net/home.view?siteLanguage=nl 的请求将站点语言更改为荷兰语。以下示例显示了如何拦截语言环境:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>

<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>

<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>

ThemeResolver

您可以应用 Spring Web MVC 框架主题来设置应用程序的整体外观,从而增强用户体验。主题是静态资源的集合,通常是样式表和图像,它们会影响应用程序的视觉风格。

要在 Web 应用程序中使用主题,必须设置 org.springframework.ui.context.ThemeSource 接口的实现。WebApplicationContext 接口扩展了 ThemeSource 但将其职责委托给了专门的实现。默认情况下,委托是 org.springframework.ui.context.support.ResourceBundleThemeSource ,它从类的根路径加载属性文件。要使用自定义的 ThemeSource 实现或配置 ResourceBundleThemeSource 的基本名称前缀,您可以在应用程序上下文中使用保留名称 themeSource 注册一个 bean。Web 应用程序上下文自动检测具有该名称的 bean 并使用它。

当使用 ResourceBundleThemeSource 时,主题是在一个简单的属性文件中定义的。属性文件列出了构成主题的资源,如以下示例所示:

1
2
styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg

属性的键是从视图代码中引用主题元素的名称。对于 JSP,通常使用 spring:theme 自定义标签来执行此操作,它与 spring:message 标签非常相似。以下 JSP 片段使用前面示例中定义的主题来自定义外观:

1
2
3
4
5
6
7
8
9
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
</head>
<body style="background=<spring:theme code='background'/>">
...
</body>
</html>

默认情况下, ResourceBundleThemeSource 使用空的基本名称前缀。因此,属性文件是从类路径的根加载的。因此,可以将 cool.properties 主题定义放在类路径根目录中(例如,在 /WEB-INF/classes 中)。ResourceBundleThemeSource 使用标准的 Java 资源包加载机制,允许主题完全国际化。例如,我们可以有一个 /WEB-INF/classes/cool_nl.properties,它引用一个带有荷兰语文本的特殊背景图像。

定义主题后,可以决定使用哪个要使用的主题。DispatcherServlet 查找名为 themeResolver 的 bean 以找出要使用的 ThemeResolver 实现。主题解析器的工作方式与 LocaleResolver 大致相同。它检测用于特定请求的主题,也可以更改请求的主题。下表描述了 Spring 提供的主题解析器:

Class Description
FixedThemeResolver 选择一个固定的主题,使用 defaultThemeName 属性设置。
SessionThemeResolver 主题在用户的 HTTP 会话中维护。 它只需要为每个会话设置一次,但不会在会话之间持续存在。
CookieThemeResolver 所选主题存储在客户端的 cookie 中。

Spring 还提供了一个 ThemeChangeInterceptor,它允许使用一个简单的请求参数在每个请求上更改主题。

MultipartResolver

org.springframework.web.multipart 包中的 MultipartResolver 是一种解析 multipart 请求(包括文件上传)的策略。 有一个基于容器的 StandardServletMultipartResolver 实现,用于 Servlet 多部分请求解析。 请注意,从具有新 Servlet 5.0+ 基线的 Spring Framework 6.0 开始,基于 Apache Commons FileUpload 的过时的 CommonsMultipartResolver 不再可用。

要启用 multipart 处理,需要在 DispatcherServlet Spring 配置中声明一个名为 multipartResolverMultipartResolverDispatcherServlet 检测到它并将其应用于传入请求。 当接收到内容类型为 multipart/form-data 的 POST 时,解析器解析将当前 HttpServletRequest 包装为 MultipartHttpServletRequest 的内容,以提供对已解析文件的访问以及将部分作为请求参数公开。

Servlet 多部分解析需要通过 Servlet 容器配置启用。 为此:

  • 在 Java 中,在 Servlet 注册上设置一个 MultipartConfigElement

  • web.xml 中,将 <multipart-config> 部分添加到 servlet 声明。

以下示例显示如何在 Servlet 注册上设置 MultipartConfigElement

1
2
3
4
5
6
7
8
9
10
11
12
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

// ...

@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {

// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}

}

一旦 Servlet multipart 配置好,就可以添加一个名为 multipartResolverStandardServletMultipartResolver 类型的 bean。

参考资料

《极客时间教程 - 机器学习 40 讲》笔记

开篇词 | 打通修炼机器学习的任督二脉

“机器学习”分为 3 个模块

  • 机器学习概观:介绍机器学习中超脱于具体模型和方法之上的一些共性问题
  • 统计学习(频率学派):利用不同的模型去拟合数据背后的规律;用拟合出的规律去推断和预测未知的结果
  • 符号学习(贝叶斯学派):即概率图模型,它计算的是变量间的相关关系,每个遍历的先验分布和大量复杂的积分技巧。

01 丨频率视角下的机器学习

频率学派认为概率是随机事件发生频率的极限值;

频率学派执行参数估计时,视参数为确定取值,视数据为随机变量;

频率学派主要使用最大似然估计法,让数据在给定参数下的似然概率最大化;

频率学派对应机器学习中的统计学习,以经验风险最小化作为模型选择的准则。

02 | 贝叶斯视角下的机器学习

贝叶斯学派认为概率是事件的可信程度或主体对事件的信任程度;

贝叶斯学派执行参数估计时,视参数为随机变量,视数据为确定取值;

贝叶斯学派主要使用最大后验概率法,让参数在先验信息和给定数据下的后验概率最大化;

贝叶斯学派对应机器学习中的概率图模型,可以在模型预测和选择中提供更加完整的信息。

03 丨学什么与怎么学

什么样的问题才能通过机器学习来解决呢?

首先,问题不能是完全随机的,需要具备一定的模式;

其次,问题本身不能通过纯计算的方法解决;

再次,有大量的数据可供使用。

机器学习的任务,就是使用数据计算出与目标函数最接近的假设,或者说拟合出最精确的模型 。

输入特征类型

  • 具体特征(concrete feature)
  • 原始特征(raw feature)
  • 抽象特征(abstract feature)

机器学习方法类型

  • 分类算法(classification)
  • 回归算法(regression)
  • 标注算法(tagging)

如果训练数据中的每组输入都有其对应的输出结果,这类学习任务就是监督学习(supervised learning),对没有输出的数据进行学习则是无监督学习(unsupervised learning)。监督学习具有更好的预测精度,无监督学习则可以发现数据中隐含的结构特性,起到的也是分类的作用,只不过没有给每个类别赋予标签而已。无监督学习可以用于对数据进行聚类或者密度估计,也可以完成异常检测这类监督学习中的预处理操作。直观地看,监督学习适用于预测任务,无监督学习适用于描述任务。

04 丨计算学习理论

Spring Data 综合

Spring Data Repository 抽象的目标是显著减少各种访问持久化存储的样板式代码。

核心概念

Repository 是 Spring Data 的核心接口。此接口主要用作标记接口,以捕获要使用的类型并帮助您发现扩展此接口的接口。CrudRepositoryListCrudRepository 接口为被管理的实体类提供复杂的 CRUD 功能。ListCrudRepository 提供等效方法,但它们返回 List,而 CrudRepository 方法返回 Iterable

CrudRepository 接口定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface CrudRepository<T, ID> extends Repository<T, ID> {

<S extends T> S save(S entity);

Optional<T> findById(ID primaryKey);

Iterable<T> findAll();

long count();

void delete(T entity);

boolean existsById(ID primaryKey);

// … more functionality omitted.
}

Spring Data 项目也提供了一些特定持久化技术的抽象接口,如:JpaRepository 或 MongoRepository。这些接口扩展了 CrudRepository 并暴露了一些持久化技术的底层功能。

除了 CrudRepository 之外,还有一个 PagingAndSortingRepository 接口,它添加了额外的方法来简化对实体的分页访问:

1
2
3
4
5
6
public interface PagingAndSortingRepository<T, ID>  {

Iterable<T> findAll(Sort sort);

Page<T> findAll(Pageable pageable);
}

【示例】要按页面大小 20 访问 User 的第二页,可以执行如下操作

1
2
PagingAndSortingRepository<User, Long> repository = // … get access to a bean
Page<User> users = repository.findAll(PageRequest.of(1, 20));

除了查询方法之外,计数和删除时的查询也是可用的。

【示例】根据姓氏计数

1
2
3
interface UserRepository extends CrudRepository<User, Long> {
long countByLastname(String lastname);
}

【示例】根据姓氏删除

1
2
3
4
5
6
interface UserRepository extends CrudRepository<User, Long> {

long deleteByLastname(String lastname);

List<User> removeByLastname(String lastname);
}

查询方法

使用 Spring Data 对数据库进行查询有以下四步:

  1. 声明一个扩展 Repository 或其子接口的接口,并指定泛型类型(实体类和 ID 类型),如以下示例所示:

    1
    interface PersonRepository extends Repository<Person, Long> { … }
  2. 在接口中声明查询方法

    1
    2
    3
    interface PersonRepository extends Repository<Person, Long> {
    List<Person> findByLastname(String lastname);
    }
  3. 使用 JavaConfigXML 配置为这些接口创建代理实例

    1
    2
    @EnableJpaRepositories
    class Config { … }
  4. 注入 Repository 实例并使用

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

    private final PersonRepository repository;

    SomeClient(PersonRepository repository) {
    this.repository = repository;
    }

    void doSomething() {
    List<Person> persons = repository.findByLastname("Matthews");
    }
    }

定义 Repository

首先需要定义一个 Repository 接口,该接口必须扩展 Repository 并且指定泛型类型(实体类和 ID 类型)。如果想为该实体暴露 CRUD 方法,可以扩展 CrudRepository 接口。

微调 Repository 定义

Spring Data 提供了很多种 Repository 以应对不同的需求场景。

CrudRepository 提供了 CRUD 功能。

ListCrudRepositoryCrudRepository 类似,但对于那些返回多个实体的方法,它返回一个 List 而不是 Iterable,这样使用可能更方便。

如果使用响应式框架,可以使用 ReactiveCrudRepositoryRxJava3CrudRepository

CoroutineCrudRepository 支持 Kotlin 的协程特性。

PagingAndSortingRepository 提供了分页、排序功能。

如果不想扩展 Spring Data 接口,还可以使用 @RepositoryDefinition 注释您的 Repository 接口。 扩展一个 CRUD Repository 接口,需要暴露一组完整的方法来操作实体。如果希望对暴露的方法有选择性,可以将要暴露的方法从 CRUD Repository 复制到自定义的 Repository 中。 这样做时,可以更改方法的返回类型。 如果可能,Spring Data 将遵循返回类型。 例如,对于返回多个实体的方法,可以选择 Iterable<T>List<T>Collection<T>VAVR 列表。

自定义基础 Repository 接口,必须用 @NoRepositoryBean 标记。 这可以防止 Spring Data 尝试直接创建它的实例并失败,因为它无法确定该 Repository 的实体,因为它仍然包含一个通用类型变量。

以下示例显示了如何有选择地暴露 CRUD 方法(在本例中为 findById 和 save):

1
2
3
4
5
6
7
8
9
10
11
@NoRepositoryBean
interface MyBaseRepository<T, ID> extends Repository<T, ID> {

Optional<T> findById(ID id);

<S extends T> S save(S entity);
}

interface UserRepository extends MyBaseRepository<User, Long> {
User findByEmailAddress(EmailAddress emailAddress);
}

使用多个 Spring 数据模块

有时,程序中需要使用多个 Spring Data 模块。在这种情况下,必须区分持久化技术。当检测到类路径上有多个 Repository 工厂时,Spring Data 进入严格的配置模式。

如果定义的 Repository 扩展了特定模块中的 Repository,则它是特定 Spring Data 模块的有效候选者。

如果实体类使用了特定模块的类型注解,则它是特定 Spring Data 模块的有效候选者。 Spring Data 模块接受第三方注解(例如 JPA 的 @Entity)或提供自己的注解(例如用于 Spring Data MongoDB 和 Spring Data Elasticsearch 的 @Document)。

以下示例显示了一个使用模块特定接口(在本例中为 JPA)的 Repository:

1
2
3
4
5
6
interface MyRepository extends JpaRepository<User, Long> { }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends JpaRepository<T, ID> { … }

interface UserRepository extends MyBaseRepository<User, Long> { … }

MyRepository 和 UserRepository 扩展了 JpaRepository。它们是 Spring Data JPA 模块的有效候选者。

以下示例显示了一个使用通用接口的 Repository

1
2
3
4
5
6
interface AmbiguousRepository extends Repository<User, Long> { … }

@NoRepositoryBean
interface MyBaseRepository<T, ID> extends CrudRepository<T, ID> { … }

interface AmbiguousUserRepository extends MyBaseRepository<User, Long> { … }

AmbiguousRepository 和 AmbiguousUserRepository 仅扩展了 Repository 和 CrudRepository。 虽然这在使用唯一的 Spring Data 模块时很好,但是存在多个模块时,无法区分这些 Repository 应该绑定到哪个特定的 Spring Data。

以下示例显示了一个使用带注解的实体类的 Repository

1
2
3
4
5
6
7
8
9
interface PersonRepository extends Repository<Person, Long> { … }

@Entity
class Person { … }

interface UserRepository extends Repository<User, Long> { … }

@Document
class User { … }

PersonRepository 引用 Person,它使用 JPA @Entity 注解进行标记,因此这个 Repository 显然属于 Spring Data JPA。 UserRepository 引用 User,它使用 Spring Data MongoDB 的 @Document 注解进行标记。

以下错误示例显示了一个使用带有混合注解的实体类的 Repository

1
2
3
4
5
6
7
interface JpaPersonRepository extends Repository<Person, Long> { … }

interface MongoDBPersonRepository extends Repository<Person, Long> { … }

@Entity
@Document
class Person { … }

此示例中的实体类同时使用了 JPA 和 Spring Data MongoDB 的注解。示例中定义了两个 Repository:JpaPersonRepository 和 MongoDBPersonRepository。 一个用于 JPA,另一个用于 MongoDB。 Spring Data 不再能够区分 Repository,这会导致未定义的行为。

区分 Repository 的最后一种方法是确定 Repository 扫描 package 的范围。

1
2
3
@EnableJpaRepositories(basePackages = "com.acme.repositories.jpa")
@EnableMongoRepositories(basePackages = "com.acme.repositories.mongo")
class Configuration { … }

定义查询方法

Repository 代理有两种方法可以从方法名称派生特定于存储的查询:

  • 通过直接从方法名称派生查询。
  • 通过使用手动定义的查询。

可用选项取决于实际存储。但是,必须有一个策略来决定创建什么实际查询。

查询策略

以下策略可用于Repository 基础结构来解析查询。 对于 Java 配置,您可以使用 EnableJpaRepositories 注释的 queryLookupStrategy 属性。 特定数据存储可能不支持某些策略。

  • CREATE 尝试从查询方法名称构造特定存储的查询。
  • USE_DECLARED_QUERY 尝试查找已声明的查询,如果找不到则抛出异常。
  • CREATE_IF_NOT_FOUND (默认)结合了 CREATEUSE_DECLARED_QUERY

查询创建

Spring Data 中有一套内置的查询构建器机制,可以自动映射符合命名和参数规则的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface PersonRepository extends Repository<Person, Long> {

List<Person> findByEmailAddressAndLastname(EmailAddress emailAddress, String lastname);

// Enables the distinct flag for the query
List<Person> findDistinctPeopleByLastnameOrFirstname(String lastname, String firstname);
List<Person> findPeopleDistinctByLastnameOrFirstname(String lastname, String firstname);

// Enabling ignoring case for an individual property
List<Person> findByLastnameIgnoreCase(String lastname);
// Enabling ignoring case for all suitable properties
List<Person> findByLastnameAndFirstnameAllIgnoreCase(String lastname, String firstname);

// Enabling static ORDER BY for a query
List<Person> findByLastnameOrderByFirstnameAsc(String lastname);
List<Person> findByLastnameOrderByFirstnameDesc(String lastname);
}

解析查询方法名称分为主语和谓语。第一部分 (find…By, exists…By) 定义查询的主语,第二部分构成谓词。 主语可以包含更多的表达。 find(或其他引入关键字)和 By 之间的任何文本都被认为是描述性的,除非使用其中一个结果限制关键字,例如 Distinct 在要创建的查询上设置不同的标志或 Top/First 限制查询结果。

参考:

Spring Data 支持的查询主语关键词

Spring Data 支持的查询谓语关键词

创建 Repository 实例

自定义 Repository 实现

Spring Data 扩展

参考资料