这篇笔记的目标是把 MessagePack 放到 Java 实际开发语境里重新梳理一遍:它是什么、为什么会比 JSON 更紧凑,以及在
Jackson、ObjectMapper和RedisTemplate里应该怎么使用。
重点放在可直接落地的序列化方案和常见误区上,尤其是依赖选择、
ObjectMapper的正确配置、数值类型的精度边界,以及 “看起来用了 MessagePack,实际上还是 JSON 序列化” 这种很容易混淆的场景。
参考资料:
官方资料:MessagePack Official Site 、 MessagePack Specification 、 msgpack-java README 、 msgpack-jackson README
Spring 资料:Spring Data Redis Reference 、 RedisSerializer Javadoc
[TOC]
1. MessagePack 是什么
MessagePack 的官方描述很直接:
It’s like JSON, but fast and small.
可以把它概括为一种面向跨语言交换的二进制序列化格式。它和 JSON 一样,都可以表达对象、数组、字符串、数字、布尔值和空值;不同点在于,JSON 是文本格式,而 MessagePack 是二进制格式。
这意味着它在很多场景下会更适合传输和存储:
- 网络传输时,字节体积通常更小
- 编码和解码过程通常更直接
- 跨语言兼容性较好,适合服务间通信
- 对缓存、消息队列、二进制协议封装更友好
需要注意的是,MessagePack 更小并不是因为它用了 Huffman 编码。更准确地说,它的紧凑性来自规范中定义的一组二进制格式族,例如:
- 小整数可以直接编码进单字节
- 短字符串只需要额外的长度前缀
- 数组、Map、二进制数据都有针对长度区间设计的格式
所以它的优势是“按类型和长度选择更紧凑的二进制表示”,而不是通用压缩算法本身。
把它和 JSON 放到同一个表里看,会更容易把各自的特点区分开:
| 维度 | MessagePack | JSON |
|---|---|---|
| 数据形态 | 二进制格式 | 文本格式 |
| 可读性 | 不能直接肉眼阅读 | 可以直接阅读 |
| 体积 | 通常更小 | 通常更大 |
| 传输与存储 | 更适合网络传输、缓存、消息载荷 | 更适合日志、配置、接口调试 |
| 调试方式 | 依赖反序列化工具或程序解析 | 直接看字符串即可 |
| 集成方式 | 往往通过编解码器或 ObjectMapper 接入 |
生态最通用,默认支持更多 |
2. Java 里怎么选依赖
在 Java 生态里,MessagePack 常见有两种使用方式:
- 直接使用底层库,自己做 pack/unpack
- 通过 Jackson 扩展,把 MessagePack 当成一种数据格式接入
ObjectMapper
这两种方式的选择可以直接按使用目标区分:
| 方式 | 典型 API | 更适合的场景 | 主要代价 |
|---|---|---|---|
| 底层编解码 | MessagePacker、MessageUnpacker |
协议实现、对字节布局有精细控制的场景 | 需要自己维护字段读写顺序和类型 |
| Jackson 数据绑定 | ObjectMapper、MessagePackFactory |
Java 业务对象、DTO、缓存对象序列化 | 依赖 Jackson 的映射语义和模块配置 |
如果只是要接入 Jackson 的对象映射能力,通常直接引入 jackson-dataformat-msgpack 即可,它会依赖 msgpack-core。
1
2
3
4
5
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>jackson-dataformat-msgpack</artifactId>
<version>${msgpack.version}</version>
</dependency>
如果项目里需要直接操作底层二进制编解码,也可以显式引入核心库:
1
2
3
4
5
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack-core</artifactId>
<version>${msgpack.version}</version>
</dependency>
这里有一个容易踩坑的点:很多旧文章里还会写 org.msgpack:msgpack。这个坐标对应的是更早期的旧实现,现代项目更常见的是 msgpack-core 和 jackson-dataformat-msgpack 这一套。
另外,jackson-dataformat-msgpack 在默认的 POJO 序列化/反序列化语义上,与 msgpack-java v0.6 及更早版本并不兼容;如果项目里有非常老的历史数据,需要额外评估兼容策略。
3. 基本用法
3.1 POJO 的序列化与反序列化
最常见的写法,是把 MessagePackFactory 交给 ObjectMapper:
1
2
3
4
5
6
7
ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
ExamplePojo pojo = new ExamplePojo("komamitsu");
byte[] bytes = objectMapper.writeValueAsBytes(pojo);
ExamplePojo deserialized = objectMapper.readValue(bytes, ExamplePojo.class);
System.out.println(deserialized.getName()); // komamitsu
如果项目里已经深度依赖 Jackson,这种方式几乎没有额外学习成本,因为使用方式和 JSON 的 ObjectMapper 很接近,只是底层 Factory 换成了 MessagePack。
有些版本里也可以直接使用更简洁的写法:
1
ObjectMapper objectMapper = new MessagePackMapper();
如果对象里存在 BigInteger 或 BigDecimal,在支持该 API 的版本里,通常更建议显式使用:
1
2
ObjectMapper objectMapper = new MessagePackMapper()
.handleBigIntegerAndBigDecimalAsString();
这样做的目的不是把 MessagePack 变成字符串格式,而是在库内部把高精度数值按字符串语义编码,避免因为底层数值类型限制导致精度丢失或序列化异常。
3.2 反序列化时忽略未知字段
在服务升级、字段新增、灰度发布这些场景里,反序列化时经常不希望因为多了一个字段就直接失败。这时可以显式关闭 FAIL_ON_UNKNOWN_PROPERTIES:
1
2
ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
这项配置的含义是:当输入数据中出现目标类没有声明、也没有对应处理逻辑的字段时,反序列化不抛异常,而是忽略这些字段。
3.3 List 的序列化与反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
List<Object> list = new ArrayList<>();
list.add("Foo");
list.add("Bar");
list.add(42);
byte[] bytes = objectMapper.writeValueAsBytes(list);
List<Object> deserialized = objectMapper.readValue(
bytes,
new TypeReference<List<Object>>() {}
);
System.out.println(deserialized); // [Foo, Bar, 42]
3.4 Map 的序列化与反序列化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
Map<String, Object> map = new HashMap<>();
map.put("name", "komamitsu");
map.put("age", 42);
byte[] bytes = objectMapper.writeValueAsBytes(map);
Map<String, Object> deserialized = objectMapper.readValue(
bytes,
new TypeReference<Map<String, Object>>() {}
);
System.out.println(deserialized); // {name=komamitsu, age=42}
3.5 两个常用读取入口
如果已经明确目标类型,通常使用:
1
objectMapper.readValue(bytes, ExamplePojo.class);
如果只是想先读取成树结构,再按需处理字段,可以使用:
1
JsonNode jsonNode = objectMapper.readTree(bytes);
前者适合“有稳定模型类”的场景,后者适合“先观察结构,再决定怎么解析”的场景。
3.6 高精度数值与时间类型的边界
MessagePack 本身对整数、浮点数支持很好,但在 Java 项目里还有两类字段需要额外留意:高精度数值和时间类型。
先看高精度数值。BigInteger 往往还能按整数类型处理,但 BigDecimal 会更敏感,因为 MessagePack 的浮点类型本质上对应 IEEE 754 的 float32 / float64。如果一个 BigDecimal 无法被无损表示成 double,就可能出现两类问题:
- 精度被截断,序列化后数值发生变化
- 编码阶段直接抛出异常
所以在金额、指标计算结果、科学计数等对精度敏感的场景里,最好不要把“可以序列化成功”直接等价成“可以无损保存”。如果项目使用的 msgpack-jackson 版本支持相关 API,优先考虑 handleBigIntegerAndBigDecimalAsString() 这一类显式配置。
另一类是 Java 8 时间类型,例如 LocalDateTime、Instant、OffsetDateTime。这里容易产生一个误解:既然已经把 ObjectMapper 换成了 MessagePackFactory,是不是时间类型也会自动处理。实际上不会。MessagePack 只负责底层数据格式,Java 时间类型是否能正确映射,仍然取决于 Jackson 模块是否注册完整。例如:
1
2
3
ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory());
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
如果工程里还没有引入 jackson-datatype-jsr310,这里还需要补上对应依赖。
也就是说,MessagePack 解决的是“用什么格式写字节”,而不是“所有 Java 类型都会自动拥有合适的映射规则”。这类模块化配置和 JSON 场景本质上是同一套思路。
4. 封装一个可复用的工具类
如果项目里有多个模块都要处理 MessagePack,一个简单、无状态的工具类会比手写单例更清晰,也更不容易出错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public final class MessagePackUtil {
private static final ObjectMapper MAPPER = new ObjectMapper(new MessagePackFactory())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
private MessagePackUtil() {
}
public static byte[] serialize(Object value) throws JsonProcessingException {
if (value == null) {
return null;
}
return MAPPER.writeValueAsBytes(value);
}
public static <T> T deserialize(byte[] bytes, Class<T> clazz) throws IOException {
if (bytes == null) {
return null;
}
return MAPPER.readValue(bytes, clazz);
}
public static <T> T deserialize(byte[] bytes, TypeReference<T> typeReference) throws IOException {
if (bytes == null) {
return null;
}
return MAPPER.readValue(bytes, typeReference);
}
public static JsonNode deserializeToTree(byte[] bytes) throws IOException {
if (bytes == null) {
return null;
}
return MAPPER.readTree(bytes);
}
}
这一版和常见旧代码相比,主要做了几件事:
ObjectMapper明确绑定到MessagePackFactory- 去掉了不必要的手写单例逻辑
- 泛型方法补上
TypeReference<T>的类型参数 - 工具类职责只保留“序列化 / 反序列化”,不混入额外状态
5. 放到 RedisTemplate 里时要注意什么
原理上,RedisTemplate 完全可以搭配 MessagePack 使用,因为 Redis 最终存的就是字节数组。
但这里最容易出现一个误区:只要用了 Jackson 的序列化器,并不等于已经用了 MessagePack。
例如下面这种写法:
1
2
Jackson2JsonRedisSerializer<Object> serializer =
new Jackson2JsonRedisSerializer<>(Object.class);
它序列化出来的仍然是 JSON,而不是 MessagePack。也就是说,RedisTemplate 里如果挂的是 Jackson2JsonRedisSerializer,那本质上还是 JSON 方案。
从链路上看,Redis 场景里真正发生的是下面这件事:
graph LR
A[业务对象] --> B[ObjectMapper]
B --> C[MessagePack bytes]
C --> D[RedisSerializer]
D --> E[Redis]
E --> D2[RedisSerializer]
D2 --> C2[MessagePack bytes]
C2 --> B2[ObjectMapper]
B2 --> A2[业务对象]
也就是说,Redis 并不知道 value 里放的是 JSON 还是 MessagePack;真正决定编码格式的是挂在 RedisTemplate 上的序列化器,以及序列化器内部使用的 ObjectMapper。
如果确实想让 Redis 的 value 使用 MessagePack,可以自定义一个 RedisSerializer,内部把 ObjectMapper 换成 MessagePack 版本:
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
public class MessagePackRedisSerializer<T> implements RedisSerializer<T> {
private final ObjectMapper objectMapper;
private final JavaType javaType;
public MessagePackRedisSerializer(ObjectMapper objectMapper, Class<T> type) {
this.objectMapper = objectMapper;
this.javaType = objectMapper.getTypeFactory().constructType(type);
}
@Override
public byte[] serialize(T value) throws SerializationException {
if (value == null) {
return null;
}
try {
return objectMapper.writeValueAsBytes(value);
} catch (IOException e) {
throw new SerializationException("MessagePack serialize error", e);
}
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length == 0) {
return null;
}
try {
return objectMapper.readValue(bytes, javaType);
} catch (IOException e) {
throw new SerializationException("MessagePack deserialize error", e);
}
}
}
这种按目标类型构造 RedisSerializer<T> 的方案更适合 value 类型固定 的场景,例如 RedisTemplate<String, UserCacheDTO>。因为反序列化时目标类型是明确的,byte[] -> UserCacheDTO 这条链路不会有歧义。
如果缓存 value 是 Object,或者同一个模板里可能放很多不同 DTO,问题就会变成“读出来之后到底该还原成哪一种 Java 类型”。这时有几种常见选择:
- 一个
RedisTemplate对应一种稳定 value 类型,避免混用 - key 前缀或业务分区严格隔离不同对象类型
- 序列化时显式携带类型信息,再按约定做反序列化
这里要特别注意,类型信息不是 MessagePack 自动帮忙解决的。它只是把数据编码成二进制;至于反序列化时如何恢复成具体 Java 类,仍然需要应用层自己定义规则。
还有一个很实际的取舍是可观测性。用 JSON 时,redis-cli 或控制台工具里通常能直接看出对象大致内容;换成 MessagePack 之后,体积和效率可能更好,但排查问题时往往需要额外反序列化工具,或者在应用里补一个调试入口。
然后再把它接到 RedisTemplate:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Bean
public RedisTemplate<String, UserCacheDTO> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
ObjectMapper objectMapper = new ObjectMapper(new MessagePackFactory())
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
RedisSerializer<UserCacheDTO> valueSerializer =
new MessagePackRedisSerializer<>(objectMapper, UserCacheDTO.class);
RedisTemplate<String, UserCacheDTO> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(valueSerializer);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(valueSerializer);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
对应依赖一般至少需要:
1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>jackson-dataformat-msgpack</artifactId>
<version>${msgpack.version}</version>
</dependency>
这里还有一个工程上的取舍:
- 如果缓存对象类型固定,且比较关注体积和编解码效率,MessagePack 很合适
- 如果缓存对象类型很多、还希望直接在 Redis CLI 里肉眼排查,JSON 往往更省心
- 如果对象里包含高精度数值或复杂时间类型,需要额外检查 Jackson 模块和数值处理策略
所以在 Redis 场景里,MessagePack 不一定是默认答案,但它确实是一个很实用的二进制序列化选项。
6. 小结
把 MessagePack 放到 Java 项目里看,核心其实有四件事:
- 先分清楚自己要的是底层二进制编解码,还是 Jackson 风格的数据绑定
- 新项目优先使用
msgpack-core和jackson-dataformat-msgpack - 在
RedisTemplate里真正想用 MessagePack,就要替换成基于MessagePackFactory的序列化器,而不是继续使用 JSON serializer - 对
BigDecimal、时间类型、多态对象这些边界场景,仍然要单独设计映射和反序列化策略
如果只是把它理解成 “二进制版 JSON”,这个理解并不算错;但真正落地时,重点不在概念类比,而在对象映射链路、依赖选型和序列化器配置是否一致。