Что это и зачем
Сериализация — преобразование объекта в поток байтов (для записи в файл/передачи по сети/кеширования).
Десериализация — обратное восстановление объекта из байтов.
Где встречается:
- распределённые системы (RPC, REST, Kafka), сессии (HttpSession), кэши (Redis, Hazelcast), очереди (JMS), снапшоты состояния.
Базовая сериализация: Serializable
Минимальный пример
import java.io.*;
public class User implements Serializable {
private static final long serialVersionUID = 1L; // фиксируем версию класса
private String username;
private transient String password; // не сериализуем
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
// запись
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("user.bin"))) {
out.writeObject(new User("alice", "p@ss"));
}
// чтение
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("user.bin"))) {
User u = (User) in.readObject();
}Ключевые моменты
- Маркерный интерфейс
Serializable(без методов) разрешает стандартный механизм. serialVersionUID— версия класса. Если не указать, JVM сгенерирует автоматически → при малейших изменениях структуры возможныInvalidClassException.
Ручное фиксированиеserialVersionUIDстабилизирует совместимость.transientпомечает поля, которые не должны попадать в поток (пароли, кеши, зависимости, lazy-прокси и т.п.).
Кастомизация процесса
1. writeObject/readObject
Позволяет контролировать сериализацию/десериализацию.
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject(); // стандартная логика
// дополнительно шифруем пароль и пишем
String enc = password == null ? null : encrypt(password);
out.writeObject(enc);
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject(); // стандартная логика
String enc = (String) in.readObject();
this.password = enc == null ? null : decrypt(enc);
}2. readResolve / writeReplace
readResolve()— возвращает замещающий объект после десериализации (полезно для синглтонов/кэшей).writeReplace()— подменяет объект перед сериализацией (паттерн Serialization Proxy).
// Синглтон
public class AppConfig implements Serializable {
private static final AppConfig INSTANCE = new AppConfig();
private AppConfig() {}
public static AppConfig getInstance() { return INSTANCE; }
private Object readResolve() { return INSTANCE; } // сохраняем синглтон-гарантию
}3. serialPersistentFields
Определяет «логическое» множество сериализуемых полей, независимо от реальных.
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("username", String.class)
};Полный контроль: Externalizable
Если нужна полная ручная сериализация:
import java.io.*;
public class Point implements Externalizable {
private int x, y;
public Point() {} // обязателен публичный конструктор без аргументов
public Point(int x, int y) { this.x = x; this.y = y; }
@Override public void writeExternal(ObjectOutput out) throws IOException {
out.writeInt(x); out.writeInt(y);
}
@Override public void readExternal(ObjectInput in) throws IOException {
this.x = in.readInt(); this.y = in.readInt();
}
}Плюсы: контроль формата, скорость, совместимость. Минусы: больше кода, выше риск ошибок.
Безопасность: фильтрация и ограничения (JEP 290)
Проблема: Java-сериализация может привести к уязвимостям (gadget-chains, RCE) при десериализации недоверенных данных.
Решения:
- ObjectInputFilter (JDK 9+): белые/чёрные списки классов, лимиты графа.
import java.io.*;
import java.util.Objects;
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
"maxdepth=50;maxbytes=1048576;!*;java.base/*;com.myapp.*"
);
try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("in.bin"))) {
in.setObjectInputFilter(filter);
Object obj = in.readObject();
}- Никогда не десериализуйте данные из внешних источников стандартной Java-сериализацией.
- Для сетевых протоколов — альтернативные форматы (см. ниже).
Совместимость версий (версионирование)
Что ломает совместимость:
- удалили/изменили тип поля;
- изменили иерархию (родителя);
- переименовали класс/пакет;
- нет подходящего конструктора (для
Externalizable).
Стратегии:
- фиксировать
serialVersionUID; - для новых полей — давать значения по умолчанию в
readObject; - использовать Serialization Proxy: сериализуется простой “DTO-прокси”, а не сама сложная сущность;
- по возможности — перейти на протоколы с явной схемой (Protobuf/Avro).
Производительность и подводные камни
- Медленнее и шумнее для GC, чем бинарные специализированные форматы (Protobuf/Kryo).
- Плохо переносится между языками (Java-специфично).
- Циклические графы объектов поддерживаются, но могут взрывать размер.
- JPA-сущности с LAZY-полями: прокси-объекты библиотек часто не сериализуются →
NotSerializableException. Для DTO — маппинг (MapStruct), а не прямая сериализация. transientполя после десериализации будутnull/0/false — не забывайте корректно инициализировать.
Альтернативы (рекомендуются в продакшене)
1. JSON (чаще всего Jackson)
Плюсы: читаемо, кросс-языково, легко отлаживать. Минусы: объём, скорость ниже бинарных.
// Jackson
import com.fasterxml.jackson.databind.ObjectMapper;
ObjectMapper om = new ObjectMapper();
String json = om.writeValueAsString(new User("alice", "p@ss")); // пароль можно @JsonIgnore
User u = om.readValue(json, User.class);Аннотации: @JsonProperty, @JsonIgnore, @JsonCreator, @JsonTypeInfo (полиморфизм), @JsonInclude.
2. Protocol Buffers (Google Protobuf)
Плюсы: быстро, компактно, строгая схема с версионированием, отличная кросс-языковая поддержка. Минусы: бинарный (нечитаемый глазами), нужна компиляция .proto.
// user.proto
syntax = "proto3";
package demo;
message User {
string username = 1;
string email = 2;
}Генерируем Java-классы (protoc), далее:
UserProto.User u = UserProto.User.newBuilder()
.setUsername("alice").setEmail("a@x.io").build();
byte[] bytes = u.toByteArray();
UserProto.User restored = UserProto.User.parseFrom(bytes);3. Avro
Плюсы: динамичная/эволюционная схема (часто в Kafka), сжатие, бинарный формат, хорош для логирования событий.
Минусы: сложнее старт, чем JSON.
4. Kryo
Плюсы: очень быстро, компактно, может сериализовать произвольные графы.
Минусы: чувствителен к изменениям классов, нужен контроль регистраций, скорее in-JVM/кеши/Spark, чем публичные API.
Best practices (итого)
- Избегайте стандартной Java-сериализации в публичных протоколах и при работе с недоверенными данными.
- Фиксируйте
serialVersionUID. - Помечайте чувствительные данные как
transientили используйте DTO без секретов. - Для тонкой логики —
writeObject/readObject,readResolve/writeReplace, Serialization Proxy. - Для безопасности — ObjectInputFilter (JEP 290), лимиты глубины/байт/классов.
- Для межъязыкового взаимодействия и стабильного версионирования — Protobuf/Avro; для человекочитаемого — JSON/Jackson.
- С JPA/LAZY — сериализуйте DTO, а не сущности.
- В распределённых кэшах/очередях — используйте поддерживаемые сериализаторы (Kafka Serde, Redis codecs, Spring MessageConverter).