Spring 中通过 RedisTemplate 处理对象
一、RedisTemplate
大多数用户可能会使用 RedisTemplate
及其对应的包 org.springframework.data.redis.core
,或者其响应式变体 ReactiveRedisTemplate
。该模板实际上是 Redis 模块的核心类,因为它提供了丰富的功能集。模板为与 Redis 的交互提供了高层抽象。虽然 ReactiveRedisConnection
或 RedisConnection
提供了接受和返回二进制值(字节数组)的低级方法,但模板处理了序列化和连接管理,使用户无需处理这些细节。
RedisTemplate
类实现了 RedisOperations
接口,其响应式变体 ReactiveRedisTemplate
实现了 ReactiveRedisOperations
。
对 ReactiveRedisTemplate
或 RedisTemplate
实例上操作的首选引用方式是通过 ReactiveRedisOperations
或 RedisOperations接口
。
此外,template 提供了操作视图(遵循 Redis 命令参考中的分组), 这些视图为针对特定类型或特定键(通过 KeyBound 接口)的操作提供了丰富且通用的接口,如下表所述:
传统命令式编程
Key Type Operations
接口 | 描述 |
---|---|
GeoOperations |
Redis 地理位置信息操作,GEOADD 、GEORADIUS 等 |
HashOperations |
Redis Hash 操作 |
HyperLogLogOperations |
Redis 的 HyperLogLog 数据结构操作, PFADD 、PFCOUNT 等 |
ListOperations |
Redis List 操作 |
SetOperations |
Redis Set 操作 |
ValueOperations |
Redis String 操作 |
ZSetOperations |
Redis ZSet 操作 |
Key Bound Operations
接口 | 描述 |
---|---|
BoundGeoOperations |
Redis 键绑定地理位置信息操作 |
BoundHashOperations |
Redis 键绑定 Hash 操作 |
BoundKeyOperations |
Redis 键绑定操作 |
BoundListOperations |
Redis 键绑定 List 操作 |
BoundSetOperations |
Redis 键绑定 Set 操作 |
BoundValueOperations |
Redis 键绑定 String 操作 |
BoundZSetOperations |
Redis 键绑定 ZSet 操作 |
响应式编程
接口 | 描述 |
---|---|
ReactiveGeoOperations |
Redis 地理位置信息操作,GEOADD 、GEORADIUS 等 |
ReactiveHashOperations |
Redis Hash 操作 |
`ReactiveHyperLogLogOperations | Redis 的 HyperLogLog 数据结构操作, PFADD 、PFCOUNT 等 |
ReactiveListOperations |
Redis List 操作 |
ReactiveValueOperations |
Redis String 操作 |
ReactiveSetOperations |
Redis Set 操作 |
ReactiveZSetOperations |
Redis ZSet 操作 |
该模板是线程安全的,可以在多个实例之间重用。
RedisTemplate
在大多数操作中使用基于 Java 的序列化器。这意味着通过模板写入或读取的任何对象都会通过 Java 进行序列化和反序列化。
可以更改模板的序列化机制,Redis 模块提供了多种实现,这些实现在 org.springframework.data.redis.serializer
包中提供。你还可以将任何序列化器设置为 null
,并通过将 enableDefaultSerializer
属性设置为 false
来使用 RedisTemplate
处理原始字节数组。需要注意的是,模板要求所有键都不能为 null
。然而,值可以为 null
,只要底层序列化器接受它们即可。
对于需要特定模板视图的情况,将该视图声明为依赖项并注入模板。容器会自动执行转换,消除 opsFor[X]
调用,如下例所示:
1 |
|
1 | public class Example { |
二、字符串操作便捷类
由于在 Redis 中存储的键和值通常是 java.lang.String
,Redis 模块提供了两个扩展类,分别是 StringRedisConnection
(及其 DefaultStringRedisConnection
实现)和 StringRedisTemplate
,作为处理密集字符串操作的便捷解决方案。除了绑定到字符串键之外,这些模板和连接底层使用 StringRedisSerializer
,这意味着存储的键和值是可读的(假设 Redis 和你的代码使用相同的编码)。
示例如下:
1 |
|
1 | public class Example { |
与其他 Spring 模板一样,RedisTemplate
和 StringRedisTemplate
通过 RedisCallback
接口让你可以直接与 Redis 通信。这一特性赋予你完全的控制权,因为它直接与 RedisConnection
通信。需要注意的是,当使用 StringRedisTemplate
时,回调接收的是 StringRedisConnection
的实例。RedisCallback
使用示例如下:
1 | public void useCallback() { |
三、序列化
从框架的角度来看,存储在 Redis 中的数据只是字节。尽管 Redis 本身支持多种类型,但这些类型大多指的是数据的存储方式,而不是它们所代表的内容。用户需要决定信息是转换为字符串还是其他对象。
在 Spring Data 中,用户(自定义)类型与原始数据(反之亦然)之间的转换由 Spring Data Redis 在 org.springframework.data.redis.serializer
包中处理。
该包包含两种类型的序列化器,顾名思义,它们负责序列化过程:
- 基于
RedisSerializer
的双向序列化器。 - 使用
RedisElementReader
和RedisElementWriter
的元素读取器和写入器。
这两种变体的主要区别在于,RedisSerializer
主要将数据序列化为 byte[]
,而读取器和写入器使用 ByteBuffer
。
有多种实现可用(包括已经在本文档中提到的两种):
JdkSerializationRedisSerializer
,这是RedisCache
和RedisTemplate
默认使用的序列化器。StringRedisSerializer
。
然而,用户还可以使用 OxmSerializer
通过 Spring OXM 支持进行 对象/XML 映射,或者使用 Jackson2JsonRedisSerializer
或 GenericJackson2JsonRedisSerializer
将数据存储为 JSON 格式。
请注意,存储格式不仅限于值。它可以用于键、值或哈希,没有任何限制。
默认情况下,
RedisCache
和RedisTemplate
配置为使用 Java 原生序列化器。Java 原生序列化因允许通过利用易受攻击的库和类注入未验证的字节码而导致远程代码执行而闻名。操纵的输入可能导致在应用程序的反序列化步骤中运行不必要的代码。因此,在不受信任的环境中不要使用序列化。一般来说,我们强烈推荐使用其他消息格式(如 JSON)。如果你担心由于 Java 序列化导致的安全漏洞,可以考虑在核心 JVM 层面使用通用的序列化过滤机制:
- Serialization Filtering
- JEP 290: Filter Incoming Serialization Data
- Deserialization of untrusted data | OWASP Foundation
这些资源提供了关于如何防止序列化漏洞的详细信息和最佳实践。
1、更改 RedisTemplate 的序列化器
RedisTemplate
默认使用的序列化器为 JdkSerializationRedisSerializer
,但是其存在一些潜在的安全问题,尤其是在处理不受信任的数据时。以下是主要的安全风险:
- 反序列化漏洞:Java 的内置序列化机制存在反序列化漏洞,攻击者可以通过构造恶意的序列化数据来执行任意代码。如果 Redis 中存储的数据来自不可信的源,这将是一个严重的安全风险。例如,攻击者可以构造一个恶意的序列化对象,当应用程序尝试反序列化这个对象时,可能会触发远程代码执行(RCE)。
- 性能问题:
JdkSerializationRedisSerializer
的性能相对较差,特别是在处理大量数据时。Java 的内置序列化机制通常比其他序列化库(如 Jackson、FST 等)慢。
使用 JDK 自带的序列化方式,有明显的缺点:
- 要求存储的对象,都要实现
java.io.Serializable
接口,比较笨重。- 存储数据为二进制格式,查看时不够友好
- 序列化的结果非常庞大,是 Json 格式的 5 倍左右,消耗 Redis 的大量内存
当然也有优点:反序列时不需要提供类型信息
RedisTemplate
相关代码如下:
1 | public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware { |
更换序列化器为 GenericJackson2JsonRedisSerializer
:
1 |
|
2、使用 Jackson 序列化器处理 JSR 310 数据类型
替换默认的 JDK 序列化器为 Jackson 之后,在处理对象时,出现以下错误:
1 | org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Java 8 date/time type `java.time.LocalDate` not supported by default: add Module "com.fasterxml.jackson.datatype:jackson-datatype-jsr310" to enable handling |
当序列化的对象中出现 JSR 310 中的数据类型,比如:LocalDate
、LocalDateTime
时,需要进行处理。异常信息中也提示了,需要引入新的模组 jackson-datatype-jsr310
,该模组在 org.springframework.data.spring-data-redis
中有定义,但是是可选项,默认是不会引入的,需要在项目的 POM 中手动引入。
1 | <dependency> |
之后需要在生成 RedisTemplate
对象注入容器之前配置序列化器时注册模块 JavaTimeModule
:
1 |
|
查看 Redis 中的数据,LocalDate
和 LocalDateTime
类型被序列化为 Json 时,结果如下:
1 | { |
3、Jackson 序列化和反序列化时处理对象
jackson 序列化时
3.1 JsonTypeInfo.As
Jackson
的 JsonTypeInfo.As
枚举定义了几种不同的类型信息包含机制,这些机制决定了类型信息如何在 JSON 中表示。每种选择都有其特定的用途和适用场景。以下是 JsonTypeInfo.As
的几种选择及其区别:
3.1.1 PROPERTY
使用单个可配置属性,该属性与实际数据(POJO 属性)一起作为一个单独的元属性包含。
示例:
1 | { |
特点:
- 类型信息作为单独的属性包含在 JSON 对象中。
- 默认的包含选择,适用于大多数情况。
- 类型信息属性名称可以通过
@JsonTypeId
注解自定义。
3.1.2 WRAPPER_OBJECT
将类型化的 JSON 值(POJO 序列化为 JSON)包装在一个 JSON 对象中,该对象有一个条目,其中字段名是序列化的类型标识符,值是实际的 JSON 值。
示例:
1 | { |
特点:
- 类型信息作为 JSON 对象的键。
- 适用于需要明确类型信息的情况。
- 类型信息必须可以序列化为字符串。
3.1.3 WRAPPER_ARRAY
将类型化的 JSON 值(POJO 序列化为 JSON)包装在一个包含两个元素的 JSON 数组中:第一个元素是序列化的类型标识符,第二个元素是序列化的 POJO 作为 JSON 对象。
示例:
1 | [ |
特点:
- 类型信息作为数组的第一个元素。
- 适用于需要紧凑表示的情况。
- 类型信息必须可以序列化为字符串。
3.1.4 EXTERNAL_PROPERTY
与 PROPERTY
类似,但属性在层次结构中高一级包含,即作为与 JSON 对象同级别的兄弟属性。
示例:
1 | { |
特点:
- 类型信息作为与 JSON 对象同级别的兄弟属性。
- 适用于需要将类型信息与其他数据分开的情况。
- 只能用于属性,不能用于类型(类)。
- 不能用于容器值(数组、集合、映射)。
3.1.5 EXISTING_PROPERTY
与 PROPERTY
在反序列化方面相似,但在序列化时是由一个“常规”可访问属性产生的。这意味着 TypeSerializer
不会做任何事情,而是期望使用某种其他机制(如默认的 POJO 属性序列化或自定义序列化器)输出具有定义名称的属性。
示例:
1 | { |
特点:
- 类型信息作为常规属性包含在 JSON 对象中。
- 适用于需要特定放置类型信息的情况。
TypeSerializer
被抑制,类型信息由其他机制输出。- 输出顺序可以控制,适用于需要精确控制类型信息位置的场景。
3.1.6 总结
- PROPERTY:类型信息作为单独的属性包含在 JSON 对象中,是最常用的默认选择。
- WRAPPER_OBJECT:类型信息作为 JSON 对象的键,适用于需要明确类型信息的情况。
- WRAPPER_ARRAY:类型信息作为数组的第一个元素,适用于需要紧凑表示的情况。
- EXTERNAL_PROPERTY:类型信息作为与 JSON 对象同级别的兄弟属性,适用于需要将类型信息与其他数据分开的情况。
- EXISTING_PROPERTY:类型信息作为常规属性包含在 JSON 对象中,适用于需要特定放置类型信息的情况。
3.2 DefaultTyping
定义了一个名为 DefaultTyping
的枚举类型,用于配置 Jackson 库中的 ObjectMapper
类如何处理默认的类型信息。当序列化或反序列化 Java 对象到 JSON 时,如果对象的类型不能直接从 JSON 中推断出来(例如,当使用 java.lang.Object
或者抽象类/接口作为属性类型时),Jackson 可以通过添加类型信息来解决多态问题。
DefaultTyping
枚举提供了几种不同的策略来控制何时应该应用默认的类型信息。这些策略包括:
JAVA_LANG_OBJECT
:只有当属性的声明类型为java.lang.Object
(包括没有显式类型的泛型)时,才使用默认类型。OBJECT_AND_NON_CONCRETE
:对于声明类型为java.lang.Object
或者是抽象类型(抽象类或接口)的属性使用默认类型。这不包括数组类型,并且从 2.4 版本开始,不适用于TreeNode
及其子类型。NON_CONCRETE_AND_ARRAYS
:除了OBJECT_AND_NON_CONCRETE
覆盖的所有类型外,还包括这些类型的数组。NON_FINAL
:对于所有非最终类型(除了少数可以正确从 JSON 推断出的“自然”类型如String
,Boolean
,Integer
,Double
以及原始类型),以及所有这些类型的数组使用默认类型。NON_FINAL_AND_ENUMS
:与NON_FINAL
相同,但还包含了枚举类型。这个选项是从 2.16 版本开始提供的,旨在允许默认类型化枚举类型而不必使用EVERYTHING
,后者有安全风险。EVERYTHING
(已废弃):几乎对所有类型使用默认类型化,除了少数可以从 JSON 正确推断的“自然”类型和原始类型。它还对所有类型的数组启用类型化。但是,这种设置通常不是你需要的,因为它会在很多不需要的地方添加类型信息,从而导致冗余数据。此选项自 2.17 版本起被标记为废弃,并在 3.0 版本中移除。
选择合适的 DefaultTyping
策略很重要,因为不当的配置可能会引入不必要的类型信息,增加 JSON 的大小,或者更严重地,可能引入安全漏洞。例如,如果允许不受信任的输入并启用了广泛的默认类型化,攻击者可能利用这一点执行反序列化攻击。因此,建议仅对已知且受信的类型启用默认类型化,并考虑使用 PolymorphicTypeValidator
进行进一步的限制。
相关链接
JEP 290: Filter Incoming Serialization Data
Deserialization of untrusted data | OWASP Foundation
OB links
OB tags
#Redis #Spring #SpringBoot