序列化是Java开发中一个看似简单实则暗藏玄机的技术点。记得我刚入行时,第一次遇到NotSerializableException异常时的困惑,以及后来在分布式系统中踩过的各种序列化坑,这些经历让我深刻认识到理解序列化机制的重要性。
序列化本质上是一种对象状态的持久化机制。当我们需要将内存中的对象状态保存到文件、数据库,或者通过网络传输到其他JVM时,就需要序列化。这个过程就像把三维立体的乐高模型拆解成二维的说明书——既保留了所有必要信息,又便于存储和传输。
Java原生序列化使用ObjectOutputStream将对象转换为字节流,这个字节流包含了:
实现Serializable接口看似简单,实则有很多细节需要注意:
java复制public class User implements Serializable {
// 显式声明serialVersionUID是好习惯
private static final long serialVersionUID = 1L;
private String username;
private transient String password; // 不会被序列化
private static int MAX_AGE = 120; // 静态字段不会被序列化
// 必须有无参构造器(反序列化时JVM需要)
public User() {}
}
注意:很多人不知道的是,
Serializable是一个标记接口(marker interface),它没有任何方法需要实现。它的作用仅仅是告诉JVM这个类的对象可以被序列化。
serialVersionUID是序列化版本的唯一标识符。如果没有显式声明,JVM会根据类结构自动计算一个。这会导致一个常见问题:当类结构发生变化(如增加字段)时,自动生成的UID会改变,导致旧版本序列化的数据无法反序列化。
建议的实践方式:
当调用ObjectOutputStream.writeObject()时,JVM会执行以下操作:
一个典型的序列化文件头如下:
code复制AC ED 00 05 // 魔数
73 // 表示这是一个对象
... // 类描述信息
... // 字段值数据
Java原生反序列化最大的问题是安全漏洞。攻击者可以构造特殊的字节流,在反序列化时执行任意代码。著名的漏洞包括:
防护措施:
java复制// 使用ObjectInputFilter限制反序列化的类
ObjectInputStream ois = new ObjectInputStream(inputStream);
ois.setObjectInputFilter(filter);
虽然不推荐在新项目中使用原生序列化,但对于遗留系统,可以注意以下优化点:
writeObject和readObject方法控制序列化过程java复制private void writeObject(ObjectOutputStream oos) throws IOException {
// 自定义序列化逻辑
oos.defaultWriteObject(); // 默认序列化
}
Externalizable接口:比Serializable更高效,但需要完全自己实现序列化逻辑
对象复用:对于频繁序列化的对象,考虑对象池技术
Jackson是目前Java生态中最主流的JSON库,Spring Boot默认集成。其核心类是ObjectMapper:
java复制ObjectMapper mapper = new ObjectMapper()
.enable(SerializationFeature.INDENT_OUTPUT) // 美化输出
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); // 忽略未知属性
// 序列化
String json = mapper.writeValueAsString(user);
// 反序列化
User user = mapper.readValue(json, User.class);
// 处理复杂泛型
List<User> users = mapper.readValue(json,
new TypeReference<List<User>>(){});
日期处理:
java复制public class Event {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;
}
多态处理:
java复制@JsonTypeInfo(use = Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = Dog.class, name = "dog"),
@JsonSubTypes.Type(value = Cat.class, name = "cat")
})
public abstract class Animal {}
视图控制:
java复制public class Views {
public static class Public {}
public static class Internal extends Public {}
}
public class Item {
@JsonView(Views.Public.class)
public int id;
@JsonView(Views.Internal.class)
public String internalNote;
}
// 使用时
mapper.setConfig(mapper.getSerializationConfig()
.withView(Views.Public.class));
java复制JsonFactory factory = mapper.getFactory();
JsonParser parser = factory.createParser(new File("large.json"));
while (parser.nextToken() != null) {
// 流式处理
}
java复制mapper.registerModule(new AfterburnerModule());
| 特性 | Java原生序列化 | Jackson JSON | Protobuf | Kryo |
|---|---|---|---|---|
| 数据格式 | 二进制 | 文本 | 二进制 | 二进制 |
| 可读性 | 不可读 | 可读 | 不可读 | 不可读 |
| 跨语言支持 | 仅Java | 广泛支持 | 广泛支持 | 主要Java |
| 性能 | 中等 | 中等 | 极高 | 极高 |
| 安全性 | 低 | 高 | 高 | 中等 |
| 模式演进 | 需要版本控制 | 灵活 | 需要.proto | 较灵活 |
| 内存占用 | 较大 | 较大 | 极小 | 小 |
推荐使用JSON序列化的场景:
考虑二进制序列化的场景:
避免使用Java原生序列化的场景:
问题1:反序列化时出现InvalidClassException
问题2:JSON反序列化时未知属性被忽略
FAIL_ON_UNKNOWN_PROPERTIES=false@JsonIgnoreProperties(ignoreUnknown=true)问题3:循环引用导致栈溢出
@JsonIdentityInfoGsonBuilder.serializeNulls()MapperFeature.USE_ANNOTATIONS可以缓存虽然JSON仍是主流,但新兴的序列化方案值得关注:
Protocol Buffers:
FlatBuffers:
Kryo:
在实际项目中,我通常会根据具体需求选择:
最后分享一个真实案例:在一次性能调优中,我们将缓存序列化从JSON切换到Kryo,序列化大小减少了60%,吞吐量提升了3倍。这告诉我们,在合适的场景选择正确的序列化方案,能带来显著的性能提升。