Session Serialization: Implementing Serializable and Performance Optimization
Both Tomcat clustering and Redis session sharing use serialization to transmit or store session data externally. Failing to implement serialization correctly results in NotSerializableException errors. Implementing it carelessly leads to performance degradation or version mismatch issues.
What is Serialization?β
Serialization converts a Java object in memory into a byte stream. Deserialization is the reverse.
[Java Object] ββserializeβββΆ [Byte Array] ββtransmitβββΆ [Byte Array] ββdeserializeβββΆ [Java Object]
UserSession Network/Redis UserSession
Serialization Requirements for Session Objectsβ
// All objects stored in the session must implement Serializable
HttpSession session = request.getSession();
session.setAttribute("user", userObject); // userObject must be Serializable
Implementing Serializable Correctlyβ
Basic Implementationβ
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
public class UserSession implements Serializable {
// Always declare serialVersionUID explicitly.
// Without it, the JVM auto-calculates the value β which changes when the
// class is modified, causing InvalidClassException on older serialized data.
private static final long serialVersionUID = 1L;
private Long userId;
private String username;
private String email;
private String role;
private LocalDateTime loginTime;
private List<String> permissions;
// transient: excluded from serialization (sensitive data or non-serializable objects)
private transient String rawPassword; // Security: exclude
private transient HttpSession httpSession; // Cannot be serialized
// Default constructor required for deserialization
public UserSession() {}
public UserSession(Long userId, String username, String role) {
this.userId = userId;
this.username = username;
this.role = role;
this.loginTime = LocalDateTime.now();
}
// getters/setters...
}
Nested Objects Also Need Serializableβ
// Permission must also implement Serializable
public class Permission implements Serializable {
private static final long serialVersionUID = 1L;
private String resource;
private String action;
}
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private List<Permission> permissions; // Permission must be Serializable
}
Managing serialVersionUIDβ
Poorly managed serialVersionUID causes deserialization errors during rolling deployments when old and new versions run simultaneously.
When Version Conflicts Occurβ
// v1.0: only userId, username
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private Long userId;
private String username;
}
// v1.1: add email field β keep same serialVersionUID
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L; // same value
private Long userId;
private String username;
private String email; // New field β deserialized as null (intended behavior)
}
With the same serialVersionUID, new fields are initialized to null when deserializing old data. This is expected.
// Dangerous: changing field type or removing a field
public class UserSession implements Serializable {
private static final long serialVersionUID = 1L;
private String userId; // Long β String: deserialization fails!
}
Rules:
- Adding fields: keep the same
serialVersionUID - Deleting or changing the type of fields: increment
serialVersionUID
Java Serialization Limitations and Alternativesβ
Java's built-in serialization is disadvantaged in terms of performance and size.
UserSession (userId, username, role):
Java serialization: ~350 bytes, ~0.5ms
JSON (Jackson): ~80 bytes, ~0.2ms
Kryo: ~50 bytes, ~0.05ms
Spring Session + JSON (GenericJackson2JsonRedisSerializer)β
Spring Session uses JSON serialization by default β faster and more readable than Java serialization.
{
"@class": "com.example.model.UserSession",
"userId": 1001,
"username": "admin",
"role": "ADMIN",
"loginTime": ["java.time.LocalDateTime", [2024, 1, 15, 10, 30, 0, 0]]
}
Note: Class type information (@class) is stored with JSON. If the class package or name changes, deserialization will fail. Take care when refactoring.
Minimize Data Stored in Sessions (Most Important)β
// Bad: storing complex objects in session
session.setAttribute("user", userEntityWithAllRelations); // thousands of bytes
session.setAttribute("orders", allOrderHistory); // tens of thousands of bytes
// Good: store only IDs, fetch from DB when needed
session.setAttribute("userId", user.getId()); // 8 bytes
session.setAttribute("role", user.getRole()); // ~10 bytes
// Fetch the rest from DB/cache when needed
Smaller sessions mean less serialization/deserialization time and less Redis memory usage.
Serialization Performance Testβ
public class SerializationBenchmark {
public static void main(String[] args) throws Exception {
UserSession session = new UserSession(1001L, "admin", "ADMIN");
// Measure serialization time
long start = System.nanoTime();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(session);
byte[] serialized = baos.toByteArray();
long serializeTime = System.nanoTime() - start;
System.out.printf("Serialized size: %d bytes%n", serialized.length);
System.out.printf("Serialization time: %.3f ms%n", serializeTime / 1_000_000.0);
// Measure deserialization time
start = System.nanoTime();
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
ObjectInputStream ois = new ObjectInputStream(bais);
UserSession deserialized = (UserSession) ois.readObject();
long deserializeTime = System.nanoTime() - start;
System.out.printf("Deserialization time: %.3f ms%n", deserializeTime / 1_000_000.0);
}
}
Session Serialization Checklistβ
Pre-deployment verification:
β
All classes stored in session implement Serializable
β
All Serializable classes have an explicit serialVersionUID declaration
β
Non-serializable or sensitive fields have the transient keyword
β
Nested objects (including List and Map values) are all Serializable
β
Session data is minimized (store IDs only, not full objects)
β
serialVersionUID updated when fields are deleted or types changed
β
Old-version deserialization tested during rolling deployments
Fixing NotSerializableExceptionβ
java.io.NotSerializableException: com.example.model.OrderDetail
When this occurs:
- Find the non-serializable class name in the stack trace (
OrderDetail) - Add
implements Serializableto that class - Add
private static final long serialVersionUID = 1L; - Mark any non-serializable fields (Connection, InputStream, etc.) as
transient - Redeploy and test
// Before fix
public class OrderDetail {
private Long orderId;
private Connection dbConnection; // cannot be serialized
}
// After fix
public class OrderDetail implements Serializable {
private static final long serialVersionUID = 1L;
private Long orderId;
private transient Connection dbConnection; // add transient
}