serialization: Java serialization companion code (UID drift, JEP 290/415 filters, records, JSON/Protobuf comparison)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh
This commit is contained in:
@@ -16,6 +16,7 @@ article; each module's own README has that article's version table, quickstart,
|
||||
| [`hashmap-concurrenthashmap`](hashmap-concurrenthashmap/) | Java HashMap vs ConcurrentHashMap: Complete Interview Guide |
|
||||
| [`exceptions`](exceptions/) | Java Exception Handling Deep Dive: Checked vs Unchecked, Suppression, and What Exceptions Actually Cost |
|
||||
| [`regex`](regex/) | Java Regex Tutorial: Pattern, Matcher, Groups, Lookarounds and ReDoS |
|
||||
| [`serialization`](serialization/) | Java Serialization in 2026: Why It's Dangerous and What Replaced It |
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<module>hashmap-concurrenthashmap</module>
|
||||
<module>exceptions</module>
|
||||
<module>regex</module>
|
||||
<module>serialization</module>
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# serialization
|
||||
|
||||
Companion code for the ankurm.com post *"Java Serialization in 2026: Why It's Dangerous and What Replaced It."*
|
||||
Module `serialization` in `java-core-examples`.
|
||||
|
||||
All explanation lives in the post; this module holds the runnable evidence and the captured output.
|
||||
Nothing here uses a real library gadget chain: the "attacker" classes are local classes that print a line.
|
||||
|
||||
## Versions
|
||||
|
||||
| Component | Version |
|
||||
|---|---|
|
||||
| JDK | 25.0.4.1+1 (Temurin, LTS) |
|
||||
| Jackson (`tools.jackson.core:jackson-databind`) | 3.2.3 |
|
||||
| protobuf-java | 4.36.2 |
|
||||
| JMH | 1.37 |
|
||||
| JUnit Jupiter | 5.11.0 |
|
||||
| Hardware | 2 vCPU x86-64 VM (timings are indicative, not a leaderboard) |
|
||||
|
||||
## Quickstart
|
||||
|
||||
```bash
|
||||
export JDK25_HOME=/path/to/jdk-25
|
||||
./scripts/run-all.sh # rebuilds and regenerates everything in output/
|
||||
```
|
||||
|
||||
## What is in here
|
||||
|
||||
| File | Shows | Output |
|
||||
|---|---|---|
|
||||
| `UidInStreamDemo` | the serialVersionUID is stored in the stream; patching it gives `InvalidClassException` | `01` |
|
||||
| `src/versions/` | two versions of one class compiled separately by `run-all.sh`, without and with an explicit UID | `02`, `03` |
|
||||
| `ReadObjectRunsCodeDemo` | `readObject` of the class named in the stream runs before the cast; an allow-list filter stops it | `04` |
|
||||
| `ResourceLimitsDemo` | forged array length, deep graph, `maxarray` / `maxdepth` / `maxbytes` | `05` |
|
||||
| `RecordsDemo` | records run the canonical constructor on deserialization; ordinary classes run none | `06` |
|
||||
| `FilterFactoryDemo` | JEP 415 filter factory with a per-request context, on top of `-Djdk.serialFilter` | `07` |
|
||||
| `Codecs`, `Order`, `order.proto`, `FormatSizeDemo` | the same object as Java serialization, Jackson 3 JSON and Protobuf wire format (hand-coded, no protoc) | `08` |
|
||||
| `SerializationBenchmark` | JMH round trip of the three encoders | `09` |
|
||||
| `SerializationTest` | 11 assertions behind the claims above | `10` |
|
||||
|
||||
The JMH run is 2 forks, 5 warmup and 8 measurement iterations of 1 s; re-running moves the numbers
|
||||
but the Java-serialization-is-slowest ordering held in every run here.
|
||||
@@ -0,0 +1,4 @@
|
||||
declared serialVersionUID = 1
|
||||
UID found in the stream = 1
|
||||
patched stream UID = 2
|
||||
InvalidClassException: com.ankurm.serialization.UidInStreamDemo$Ticket; local class incompatible: stream classdesc serialVersionUID = 2, local class serialVersionUID = 1
|
||||
@@ -0,0 +1,6 @@
|
||||
$ # implicit serialVersionUID (none declared)
|
||||
$ java -cp v1 DriftWrite
|
||||
wrote 113 bytes; serialVersionUID in stream = 1201216851776330172
|
||||
$ java -cp v2 DriftRead # v2 adds a field
|
||||
this class's serialVersionUID = -732213015030957059
|
||||
InvalidClassException: com.ankurm.serialization.drift.Account; local class incompatible: stream classdesc serialVersionUID = 1201216851776330172, local class serialVersionUID = -732213015030957059
|
||||
@@ -0,0 +1,6 @@
|
||||
$ # explicit serialVersionUID = 1L
|
||||
$ java -cp v1 DriftWrite
|
||||
wrote 113 bytes; serialVersionUID in stream = 1
|
||||
$ java -cp v2 DriftRead # v2 adds a field
|
||||
this class's serialVersionUID = 1
|
||||
read: Account[owner=asha, balance=500, email=null]
|
||||
@@ -0,0 +1,6 @@
|
||||
application expects a Greeting and writes: String greeting = (Greeting) in.readObject()
|
||||
>>> Noisy.readObject() is running -- code of the class named in the stream
|
||||
ClassCastException AFTER the side effect: class com.ankurm.serialization.ReadObjectRunsCodeDemo$Noisy cannot be cast to class com.ankurm.serialization.ReadObjectRunsCodeDemo$Greeting (com.ankurm.serialization.ReadObjectRunsCodeDemo$Noisy and com.ankurm.serialization.ReadObjectRunsCodeDemo$Greeting are in unnamed module of loader 'app')
|
||||
|
||||
same bytes, allow-list filter that only admits Greeting:
|
||||
InvalidClassException: filter status: REJECTED
|
||||
@@ -0,0 +1,16 @@
|
||||
max heap = 256 MiB
|
||||
forged stream is 37 bytes long but claims a byte[1000000000]
|
||||
no filter -> OutOfMemoryError: Java heap space
|
||||
maxarray=100000 -> InvalidClassException: filter status: REJECTED
|
||||
filter saw: class=class [B arrayLength=-1 depth=1 streamBytes=21 -> UNDECIDED
|
||||
filter saw: class=class [B arrayLength=1000000000 depth=1 streamBytes=27 -> REJECTED
|
||||
maxarray, logged -> InvalidClassException: filter status: REJECTED
|
||||
|
||||
a legitimate-looking chain of 200 nodes is 1324 bytes
|
||||
no filter -> accepted: Node
|
||||
maxdepth=50 -> InvalidClassException: filter status: REJECTED
|
||||
|
||||
maxbytes=10000, one 50 KB array -> accepted: byte[]
|
||||
an ArrayList of 5000 integers is 50125 bytes
|
||||
maxbytes=10000, 5000 integers -> InvalidClassException: filter status: REJECTED
|
||||
maxbytes=1000000, 5000 integers -> accepted: ArrayList
|
||||
@@ -0,0 +1,5 @@
|
||||
record default serialVersionUID = 0
|
||||
forged stream with years = -5:
|
||||
record -> InvalidObjectException: years out of range: -5
|
||||
cause: java.lang.IllegalArgumentException: years out of range: -5
|
||||
class -> AgeClass[years=-5] (no constructor ran)
|
||||
@@ -0,0 +1,13 @@
|
||||
$ java -Djdk.serialFilter="maxdepth=10;com.ankurm.serialization.*;java.lang.*;!*" FilterFactoryDemo
|
||||
jdk.serialFilter (system property) = maxdepth=10;com.ankurm.serialization.*;java.lang.*;!*
|
||||
Config.getSerialFilter() = maxdepth=10;com.ankurm.serialization.*;java.lang.*;!*
|
||||
factory before = java.io.ObjectInputFilter$Config$BuiltinFilterFactory
|
||||
factory installed; installing a second one:
|
||||
IllegalStateException: Cannot replace filter factory
|
||||
context A (Ok + String allowed):
|
||||
read Ok -> Ok[s=fine]
|
||||
read String -> a plain string
|
||||
read 50-deep chain -> InvalidClassException: filter status: REJECTED
|
||||
context B (String only):
|
||||
read String -> a plain string
|
||||
read Ok -> InvalidClassException: filter status: REJECTED
|
||||
@@ -0,0 +1,10 @@
|
||||
Java serialization : 382 bytes, starts with aced0005 (magic aced0005)
|
||||
Jackson 3 JSON : 223 bytes
|
||||
Protobuf wire : 80 bytes
|
||||
|
||||
JSON text: {"id":1000042,"customer":"Asha Mehta","currency":"INR","lines":[{"sku":"BK-JAVA-25","quantity":2,"priceMinor":49900},{"sku":"BK-JVM-INT","quantity":1,"priceMinor":79900},{"sku":"CBL-USB-C","quantity":3,"priceMinor":19900}]}
|
||||
|
||||
round trips equal : java=true json=true protobuf=true
|
||||
|
||||
JSON cannot name a class to instantiate unless you opt in. Feeding it a type hint:
|
||||
parsed as Order[id=1, customer=x, currency=INR, lines=[]]
|
||||
@@ -0,0 +1,4 @@
|
||||
Benchmark Mode Cnt Score Error Units
|
||||
SerializationBenchmark.jacksonJson avgt 16 3107.305 ± 467.508 ns/op
|
||||
SerializationBenchmark.javaSerialization avgt 16 11316.514 ± 538.967 ns/op
|
||||
SerializationBenchmark.protobufWire avgt 16 3736.484 ± 283.111 ns/op
|
||||
@@ -0,0 +1,4 @@
|
||||
-------------------------------------------------------------------------------
|
||||
Test set: com.ankurm.serialization.SerializationTest
|
||||
-------------------------------------------------------------------------------
|
||||
Tests run: 11, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.851 s -- in com.ankurm.serialization.SerializationTest
|
||||
@@ -0,0 +1,100 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>java-core-examples</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>serialization</artifactId>
|
||||
<name>serialization</name>
|
||||
<description>Java serialization in 2026: serialVersionUID drift, deserialization filters (JEP 290/415), records, and JSON/Protobuf alternatives with size and speed numbers.</description>
|
||||
|
||||
<properties>
|
||||
<jmh.version>1.37</jmh.version>
|
||||
<jackson.version>3.2.3</jackson.version>
|
||||
<protobuf.version>4.36.2</protobuf.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-core</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>tools.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>${protobuf.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.11.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>benchmarks</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<configuration>
|
||||
<release>25</release>
|
||||
<!--
|
||||
Same JDK-25-stops-discovering-annotation-processors-implicitly trap documented in
|
||||
the jmm module: JMH's @Benchmark-method-to-*_jmh.java generator needs to be declared
|
||||
explicitly here or the build silently produces a jar with nothing runnable in it.
|
||||
-->
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.openjdk.jmh</groupId>
|
||||
<artifactId>jmh-generator-annprocess</artifactId>
|
||||
<version>${jmh.version}</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.2.5</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals><goal>shade</goal></goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>org.openjdk.jmh.Main</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regenerates every file in ../output/. Requires JDK25_HOME.
|
||||
set -euo pipefail
|
||||
[[ -z "${JDK25_HOME:-}" ]] && { echo "JDK25_HOME must be set" >&2; exit 1; }
|
||||
cd "$(dirname "$0")/.."
|
||||
OUT=output; mkdir -p "$OUT"
|
||||
export JAVA_HOME="$JDK25_HOME"
|
||||
mvn -q -f ../pom.xml -pl serialization -am package 2>&1 | grep -v -E "Picked up|^WARNING" || true
|
||||
J="$JDK25_HOME/bin/java"; JC="$JDK25_HOME/bin/javac"
|
||||
M2="${HOME}/.m2/repository"
|
||||
CP="target/classes:$(ls $M2/tools/jackson/core/jackson-databind/3.2.3/*.jar):$(ls $M2/tools/jackson/core/jackson-core/3.2.3/*.jar):$(ls $M2/com/fasterxml/jackson/core/jackson-annotations/*/*.jar | tail -1):$(ls $M2/com/google/protobuf/protobuf-java/4.36.2/*.jar)"
|
||||
run() { f=$1; shift; echo "==> $f"; "$J" "$@" 2>&1 | grep -v "Picked up" > "$OUT/$f"; }
|
||||
|
||||
# 01: the UID travels inside the stream
|
||||
run 01-uid-in-stream.txt -cp "$CP" com.ankurm.serialization.UidInStreamDemo
|
||||
|
||||
# 02/03: two versions of the same class, with and without an explicit serialVersionUID
|
||||
for mode in implicit explicit; do
|
||||
rm -rf target/drift-$mode; mkdir -p target/drift-$mode
|
||||
for v in v1 v2; do
|
||||
d=target/drift-$mode/$v/com/ankurm/serialization/drift; mkdir -p $d
|
||||
if [[ $mode == explicit ]]; then
|
||||
sed 's#/\*UID\*/#private static final long serialVersionUID = 1L;#' src/versions/$v/Account.java > $d/Account.java
|
||||
else
|
||||
sed 's#/\*UID\*/##' src/versions/$v/Account.java > $d/Account.java
|
||||
fi
|
||||
cp src/versions/common/*.java $d/
|
||||
$JC -d target/drift-$mode/$v/classes $d/*.java 2>&1 | grep -v "Picked up" || true
|
||||
done
|
||||
done
|
||||
{
|
||||
echo '$ # implicit serialVersionUID (none declared)'
|
||||
echo '$ java -cp v1 DriftWrite'
|
||||
"$J" -cp target/drift-implicit/v1/classes com.ankurm.serialization.drift.DriftWrite target/account-implicit.ser 2>&1 | grep -v "Picked up"
|
||||
echo '$ java -cp v2 DriftRead # v2 adds a field'
|
||||
"$J" -cp target/drift-implicit/v2/classes com.ankurm.serialization.drift.DriftRead target/account-implicit.ser 2>&1 | grep -v "Picked up"
|
||||
} > "$OUT/02-drift-implicit.txt"
|
||||
{
|
||||
echo '$ # explicit serialVersionUID = 1L'
|
||||
echo '$ java -cp v1 DriftWrite'
|
||||
"$J" -cp target/drift-explicit/v1/classes com.ankurm.serialization.drift.DriftWrite target/account-explicit.ser 2>&1 | grep -v "Picked up"
|
||||
echo '$ java -cp v2 DriftRead # v2 adds a field'
|
||||
"$J" -cp target/drift-explicit/v2/classes com.ankurm.serialization.drift.DriftRead target/account-explicit.ser 2>&1 | grep -v "Picked up"
|
||||
} > "$OUT/03-drift-explicit.txt"
|
||||
|
||||
run 04-readobject-runs-code.txt -cp "$CP" com.ankurm.serialization.ReadObjectRunsCodeDemo
|
||||
run 05-resource-limits.txt -Xmx256m -cp "$CP" com.ankurm.serialization.ResourceLimitsDemo
|
||||
run 06-records.txt -cp "$CP" com.ankurm.serialization.RecordsDemo
|
||||
{
|
||||
echo '$ java -Djdk.serialFilter="maxdepth=10;com.ankurm.serialization.*;java.lang.*;!*" FilterFactoryDemo'
|
||||
"$J" -Djdk.serialFilter='maxdepth=10;com.ankurm.serialization.*;java.lang.*;!*' -cp "$CP" com.ankurm.serialization.FilterFactoryDemo 2>&1 | grep -v "Picked up"
|
||||
} > "$OUT/07-filter-factory.txt"
|
||||
run 08-format-sizes.txt -cp "$CP" com.ankurm.serialization.FormatSizeDemo
|
||||
echo "==> JMH"
|
||||
"$J" -jar target/benchmarks.jar SerializationBenchmark -rf text -rff "$OUT/09-jmh-raw.txt" > /dev/null 2>&1
|
||||
cp target/surefire-reports/com.ankurm.serialization.SerializationTest.txt "$OUT/10-tests.txt"
|
||||
echo Done
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import com.google.protobuf.CodedInputStream;
|
||||
import com.google.protobuf.CodedOutputStream;
|
||||
import tools.jackson.databind.json.JsonMapper;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Three encoders for {@link Order}: Java serialization, Jackson 3 JSON, and Protobuf wire format.
|
||||
* The Protobuf side is written against protobuf-java's CodedOutputStream / CodedInputStream using the
|
||||
* same field numbers a .proto file would declare (see order.proto). That is the real wire format, but
|
||||
* there is no protoc-generated class here.
|
||||
*/
|
||||
public final class Codecs {
|
||||
private Codecs() {}
|
||||
|
||||
public static final JsonMapper JSON = JsonMapper.builder().build();
|
||||
|
||||
// ---- Java serialization
|
||||
public static byte[] javaWrite(Order o) throws IOException { return Wire.write(o); }
|
||||
public static Order javaRead(byte[] b) throws Exception { return (Order) Wire.read(b); }
|
||||
|
||||
// ---- Jackson 3
|
||||
public static byte[] jsonWrite(Order o) { return JSON.writeValueAsBytes(o); }
|
||||
public static Order jsonRead(byte[] b) { return JSON.readValue(b, Order.class); }
|
||||
|
||||
// ---- Protobuf wire format: Order { int64 id=1; string customer=2; string currency=3; repeated Line lines=4; }
|
||||
// Line { string sku=1; int32 quantity=2; int64 price_minor=3; }
|
||||
public static byte[] pbWrite(Order o) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream(128);
|
||||
CodedOutputStream out = CodedOutputStream.newInstance(bos);
|
||||
out.writeInt64(1, o.id());
|
||||
out.writeString(2, o.customer());
|
||||
out.writeString(3, o.currency());
|
||||
for (Order.Line l : o.lines()) {
|
||||
ByteArrayOutputStream lb = new ByteArrayOutputStream(32);
|
||||
CodedOutputStream lo = CodedOutputStream.newInstance(lb);
|
||||
lo.writeString(1, l.sku());
|
||||
lo.writeInt32(2, l.quantity());
|
||||
lo.writeInt64(3, l.priceMinor());
|
||||
lo.flush();
|
||||
out.writeByteArray(4, lb.toByteArray());
|
||||
}
|
||||
out.flush();
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
public static Order pbRead(byte[] b) throws IOException {
|
||||
CodedInputStream in = CodedInputStream.newInstance(b);
|
||||
long id = 0; String customer = "", currency = ""; List<Order.Line> lines = new ArrayList<>();
|
||||
for (int tag; (tag = in.readTag()) != 0; ) {
|
||||
switch (tag >>> 3) {
|
||||
case 1 -> id = in.readInt64();
|
||||
case 2 -> customer = in.readStringRequireUtf8();
|
||||
case 3 -> currency = in.readStringRequireUtf8();
|
||||
case 4 -> {
|
||||
CodedInputStream li = CodedInputStream.newInstance(in.readByteArray());
|
||||
String sku = ""; int q = 0; long price = 0;
|
||||
for (int t; (t = li.readTag()) != 0; ) {
|
||||
switch (t >>> 3) {
|
||||
case 1 -> sku = li.readStringRequireUtf8();
|
||||
case 2 -> q = li.readInt32();
|
||||
case 3 -> price = li.readInt64();
|
||||
default -> li.skipField(t);
|
||||
}
|
||||
}
|
||||
lines.add(new Order.Line(sku, q, price));
|
||||
}
|
||||
default -> in.skipField(tag);
|
||||
}
|
||||
}
|
||||
return new Order(id, customer, currency, lines);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.function.BinaryOperator;
|
||||
|
||||
/**
|
||||
* JEP 415: a process-wide filter FACTORY chooses the filter for each ObjectInputStream from the
|
||||
* calling context. Here the context is a ThreadLocal holding a per-request filter, merged with a global one.
|
||||
* Run with: -Djdk.serialFilter="maxdepth=10;java.base/*;!*" (see run-all.sh).
|
||||
*/
|
||||
public class FilterFactoryDemo {
|
||||
static final ThreadLocal<ObjectInputFilter> CONTEXT = new ThreadLocal<>();
|
||||
|
||||
record Ok(String s) implements Serializable {}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("jdk.serialFilter (system property) = " + System.getProperty("jdk.serialFilter"));
|
||||
System.out.println("Config.getSerialFilter() = " + ObjectInputFilter.Config.getSerialFilter());
|
||||
System.out.println("factory before = " + ObjectInputFilter.Config.getSerialFilterFactory().getClass().getName());
|
||||
|
||||
BinaryOperator<ObjectInputFilter> factory = (current, requested) -> {
|
||||
ObjectInputFilter ctx = CONTEXT.get();
|
||||
// 'current' is the filter already in effect for the stream (the process-wide one on first call)
|
||||
ObjectInputFilter merged = ObjectInputFilter.merge(ctx, current);
|
||||
return ObjectInputFilter.merge(requested, merged);
|
||||
};
|
||||
ObjectInputFilter.Config.setSerialFilterFactory(factory);
|
||||
System.out.println("factory installed; installing a second one:");
|
||||
try {
|
||||
ObjectInputFilter.Config.setSerialFilterFactory(factory);
|
||||
} catch (IllegalStateException e) {
|
||||
System.out.println("IllegalStateException: " + e.getMessage().substring(0, e.getMessage().indexOf(':')));
|
||||
}
|
||||
|
||||
byte[] ok = Wire.write(new Ok("fine"));
|
||||
byte[] str = Wire.write("a plain string");
|
||||
|
||||
// Context A: a request that may read Ok records
|
||||
CONTEXT.set(ObjectInputFilter.Config.createFilter("com.ankurm.serialization.FilterFactoryDemo$Ok;com.ankurm.serialization.ResourceLimitsDemo$Node;java.lang.String;!*"));
|
||||
System.out.println("context A (Ok + String allowed):");
|
||||
System.out.println(" read Ok -> " + Wire.read(ok));
|
||||
System.out.println(" read String -> " + Wire.read(str));
|
||||
|
||||
// the process-wide maxdepth=10 from -Djdk.serialFilter still applies underneath the context filter
|
||||
try {
|
||||
Wire.read(Wire.write(ResourceLimitsDemo.chain(50)));
|
||||
} catch (InvalidClassException e) {
|
||||
System.out.println(" read 50-deep chain -> InvalidClassException: " + e.getMessage());
|
||||
}
|
||||
|
||||
// Context B: a request that may only read Strings
|
||||
CONTEXT.set(ObjectInputFilter.Config.createFilter("java.lang.String;!*"));
|
||||
System.out.println("context B (String only):");
|
||||
System.out.println(" read String -> " + Wire.read(str));
|
||||
try {
|
||||
Wire.read(ok);
|
||||
} catch (InvalidClassException e) {
|
||||
System.out.println(" read Ok -> InvalidClassException: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/** Prints the encoded size of the same Order in each format, plus what each one looks like on the wire. */
|
||||
public class FormatSizeDemo {
|
||||
public static void main(String[] args) throws Exception {
|
||||
Order o = Order.sample();
|
||||
byte[] java = Codecs.javaWrite(o), json = Codecs.jsonWrite(o), pb = Codecs.pbWrite(o);
|
||||
System.out.println("Java serialization : " + java.length + " bytes, starts with " + Wire.hexHead(java, 4) + " (magic aced0005)");
|
||||
System.out.println("Jackson 3 JSON : " + json.length + " bytes");
|
||||
System.out.println("Protobuf wire : " + pb.length + " bytes");
|
||||
System.out.println();
|
||||
System.out.println("JSON text: " + new String(json, StandardCharsets.UTF_8));
|
||||
System.out.println();
|
||||
System.out.println("round trips equal : java=" + o.equals(Codecs.javaRead(java))
|
||||
+ " json=" + o.equals(Codecs.jsonRead(json)) + " protobuf=" + o.equals(Codecs.pbRead(pb)));
|
||||
|
||||
System.out.println();
|
||||
System.out.println("JSON cannot name a class to instantiate unless you opt in. Feeding it a type hint:");
|
||||
String hostile = "{\"@class\":\"java.lang.ProcessBuilder\",\"id\":1,\"customer\":\"x\",\"currency\":\"INR\",\"lines\":[]}";
|
||||
try {
|
||||
System.out.println(" parsed as " + Codecs.JSON.readValue(hostile, Order.class));
|
||||
} catch (Exception e) {
|
||||
System.out.println(" " + e.getClass().getSimpleName() + ": " + e.getMessage().lines().findFirst().orElse(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/** The same small business object encoded three ways for the size/speed comparison. */
|
||||
public record Order(long id, String customer, String currency, List<Line> lines) implements Serializable {
|
||||
|
||||
public record Line(String sku, int quantity, long priceMinor) implements Serializable {}
|
||||
|
||||
public static Order sample() {
|
||||
return new Order(1_000_042L, "Asha Mehta", "INR", List.of(
|
||||
new Line("BK-JAVA-25", 2, 49_900),
|
||||
new Line("BK-JVM-INT", 1, 79_900),
|
||||
new Line("CBL-USB-C", 3, 19_900)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* Safe demonstration of the core problem: deserialization executes code from the class named in the
|
||||
* stream, BEFORE the caller gets the object back and therefore before any cast or instanceof check.
|
||||
* The "payload" here only prints a line. No real library class is involved.
|
||||
*/
|
||||
public class ReadObjectRunsCodeDemo {
|
||||
|
||||
/** A class that happens to be on the classpath and has a readObject with a side effect. */
|
||||
static class Noisy implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
String note = "hello";
|
||||
|
||||
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
|
||||
in.defaultReadObject();
|
||||
System.out.println(" >>> Noisy.readObject() is running -- code of the class named in the stream");
|
||||
}
|
||||
}
|
||||
|
||||
/** The class the application thinks it is reading. */
|
||||
record Greeting(String text) implements Serializable {}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
byte[] bytes = Wire.write(new Noisy());
|
||||
System.out.println("application expects a Greeting and writes: String greeting = (Greeting) in.readObject()");
|
||||
try {
|
||||
Greeting g = (Greeting) Wire.read(bytes);
|
||||
System.out.println("got " + g);
|
||||
} catch (ClassCastException e) {
|
||||
System.out.println("ClassCastException AFTER the side effect: " + e.getMessage());
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("same bytes, allow-list filter that only admits Greeting:");
|
||||
ObjectInputFilter onlyGreeting = ObjectInputFilter.Config.createFilter(
|
||||
"com.ankurm.serialization.ReadObjectRunsCodeDemo$Greeting;!*");
|
||||
try {
|
||||
Wire.read(bytes, onlyGreeting);
|
||||
} catch (InvalidClassException e) {
|
||||
System.out.println("InvalidClassException: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/** Records deserialize through their canonical constructor; ordinary classes do not run any constructor. */
|
||||
public class RecordsDemo {
|
||||
|
||||
record AgeRecord(int years) implements Serializable {
|
||||
AgeRecord {
|
||||
if (years < 0 || years > 150) throw new IllegalArgumentException("years out of range: " + years);
|
||||
}
|
||||
}
|
||||
|
||||
static class AgeClass implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
final int years;
|
||||
AgeClass(int years) {
|
||||
if (years < 0 || years > 150) throw new IllegalArgumentException("years out of range: " + years);
|
||||
this.years = years;
|
||||
}
|
||||
@Override public String toString() { return "AgeClass[years=" + years + "]"; }
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("record default serialVersionUID = " + ObjectStreamClass.lookup(AgeRecord.class).getSerialVersionUID());
|
||||
|
||||
byte[] rec = Wire.write(new AgeRecord(30));
|
||||
byte[] cls = Wire.write(new AgeClass(30));
|
||||
// the int field is the last four bytes of each stream
|
||||
Wire.putInt(rec, rec.length - 4, -5);
|
||||
Wire.putInt(cls, cls.length - 4, -5);
|
||||
|
||||
System.out.println("forged stream with years = -5:");
|
||||
try {
|
||||
System.out.println(" record -> " + Wire.read(rec));
|
||||
} catch (InvalidObjectException e) {
|
||||
System.out.println(" record -> InvalidObjectException: " + e.getMessage());
|
||||
System.out.println(" cause: " + e.getCause());
|
||||
}
|
||||
System.out.println(" class -> " + Wire.read(cls) + " (no constructor ran)");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* Resource exhaustion without any gadget: a stream can ask for a huge array or a very deep object
|
||||
* graph. The JEP 290 limits maxarray / maxdepth / maxbytes reject it before the allocation or recursion.
|
||||
*/
|
||||
public class ResourceLimitsDemo {
|
||||
|
||||
static class Node implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
Node next;
|
||||
}
|
||||
|
||||
static Node chain(int depth) {
|
||||
Node head = new Node(), cur = head;
|
||||
for (int i = 1; i < depth; i++) { cur.next = new Node(); cur = cur.next; }
|
||||
return head;
|
||||
}
|
||||
|
||||
/** Serialize a byte[10], then patch the declared array length to 'claimed'. The array length int is 14 bytes from the end. */
|
||||
static byte[] forgedArray(int claimed) throws IOException {
|
||||
byte[] b = Wire.write(new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9});
|
||||
Wire.putInt(b, b.length - 14, claimed);
|
||||
return b;
|
||||
}
|
||||
|
||||
static void attempt(String label, byte[] bytes, ObjectInputFilter filter) {
|
||||
try {
|
||||
Object o = filter == null ? Wire.read(bytes) : Wire.read(bytes, filter);
|
||||
System.out.println(label + " -> accepted: " + o.getClass().getSimpleName());
|
||||
} catch (InvalidClassException e) {
|
||||
System.out.println(label + " -> InvalidClassException: " + e.getMessage());
|
||||
} catch (EOFException e) {
|
||||
System.out.println(label + " -> EOFException (stream ended; the array WAS allocated first)");
|
||||
} catch (OutOfMemoryError e) {
|
||||
System.out.println(label + " -> OutOfMemoryError: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
System.out.println(label + " -> " + e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
System.out.println("max heap = " + Runtime.getRuntime().maxMemory() / (1024 * 1024) + " MiB");
|
||||
|
||||
byte[] bomb = forgedArray(1_000_000_000);
|
||||
System.out.println("forged stream is " + bomb.length + " bytes long but claims a byte[1000000000]");
|
||||
attempt("no filter ", bomb, null);
|
||||
attempt("maxarray=100000 ", bomb, ObjectInputFilter.Config.createFilter("maxarray=100000"));
|
||||
ObjectInputFilter limit = ObjectInputFilter.Config.createFilter("maxarray=100000");
|
||||
attempt("maxarray, logged ", bomb, info -> {
|
||||
ObjectInputFilter.Status st = limit.checkInput(info);
|
||||
System.out.println(" filter saw: class=" + info.serialClass() + " arrayLength=" + info.arrayLength()
|
||||
+ " depth=" + info.depth() + " streamBytes=" + info.streamBytes() + " -> " + st);
|
||||
return st;
|
||||
});
|
||||
|
||||
System.out.println();
|
||||
byte[] deep = Wire.write(chain(200));
|
||||
System.out.println("a legitimate-looking chain of 200 nodes is " + deep.length + " bytes");
|
||||
attempt("no filter ", deep, null);
|
||||
attempt("maxdepth=50 ", deep, ObjectInputFilter.Config.createFilter("maxdepth=50;com.ankurm.serialization.ResourceLimitsDemo$Node;!*"));
|
||||
|
||||
System.out.println();
|
||||
byte[] one = Wire.write(new byte[50_000]);
|
||||
attempt("maxbytes=10000, one 50 KB array ", one, ObjectInputFilter.Config.createFilter("maxbytes=10000"));
|
||||
java.util.ArrayList<Integer> many = new java.util.ArrayList<>();
|
||||
for (int i = 0; i < 5_000; i++) many.add(i);
|
||||
byte[] list = Wire.write(many);
|
||||
System.out.println("an ArrayList of 5000 integers is " + list.length + " bytes");
|
||||
attempt("maxbytes=10000, 5000 integers ", list, ObjectInputFilter.Config.createFilter("maxbytes=10000"));
|
||||
attempt("maxbytes=1000000, 5000 integers ", list, ObjectInputFilter.Config.createFilter("maxbytes=1000000"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import org.openjdk.jmh.annotations.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/** Round trip (encode + decode) of the same Order. Indicative only; 2 vCPU VM. */
|
||||
@State(Scope.Benchmark)
|
||||
@BenchmarkMode(Mode.AverageTime)
|
||||
@OutputTimeUnit(TimeUnit.NANOSECONDS)
|
||||
@Warmup(iterations = 5, time = 1)
|
||||
@Measurement(iterations = 8, time = 1)
|
||||
@Fork(2)
|
||||
public class SerializationBenchmark {
|
||||
final Order order = Order.sample();
|
||||
|
||||
@Benchmark public Order javaSerialization() throws Exception { return Codecs.javaRead(Codecs.javaWrite(order)); }
|
||||
@Benchmark public Order jacksonJson() { return Codecs.jsonRead(Codecs.jsonWrite(order)); }
|
||||
@Benchmark public Order protobufWire() throws Exception { return Codecs.pbRead(Codecs.pbWrite(order)); }
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/** The serialVersionUID is written into the stream next to the class name; the reader compares it with its own class. */
|
||||
public class UidInStreamDemo {
|
||||
|
||||
static class Ticket implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
String seat = "12A";
|
||||
}
|
||||
|
||||
/** Offset of the 8-byte UID: magic+version (4), TC_OBJECT (1), TC_CLASSDESC (1), name length (2), name. */
|
||||
static int uidOffset(Class<?> c) { return 4 + 1 + 1 + 2 + c.getName().length(); }
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
byte[] bytes = Wire.write(new Ticket());
|
||||
int off = uidOffset(Ticket.class);
|
||||
long inStream = java.nio.ByteBuffer.wrap(bytes, off, 8).getLong();
|
||||
System.out.println("declared serialVersionUID = " + ObjectStreamClass.lookup(Ticket.class).getSerialVersionUID());
|
||||
System.out.println("UID found in the stream = " + inStream);
|
||||
|
||||
bytes[off + 7] = 2; // pretend the writer was running version 2 of the class
|
||||
System.out.println("patched stream UID = " + java.nio.ByteBuffer.wrap(bytes, off, 8).getLong());
|
||||
try {
|
||||
Wire.read(bytes);
|
||||
} catch (InvalidClassException e) {
|
||||
System.out.println("InvalidClassException: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import java.io.*;
|
||||
|
||||
/** Small helpers shared by the demos: serialize to bytes, deserialize from bytes, hex dump. */
|
||||
final class Wire {
|
||||
private Wire() {}
|
||||
|
||||
static byte[] write(Object o) throws IOException {
|
||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||
try (ObjectOutputStream out = new ObjectOutputStream(bos)) { out.writeObject(o); }
|
||||
return bos.toByteArray();
|
||||
}
|
||||
|
||||
static Object read(byte[] bytes) throws IOException, ClassNotFoundException {
|
||||
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) { return in.readObject(); }
|
||||
}
|
||||
|
||||
static Object read(byte[] bytes, ObjectInputFilter filter) throws IOException, ClassNotFoundException {
|
||||
try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
|
||||
in.setObjectInputFilter(filter);
|
||||
return in.readObject();
|
||||
}
|
||||
}
|
||||
|
||||
/** Overwrites four bytes with a big-endian int at the given offset. */
|
||||
static void putInt(byte[] b, int off, int v) {
|
||||
b[off] = (byte) (v >>> 24); b[off + 1] = (byte) (v >>> 16); b[off + 2] = (byte) (v >>> 8); b[off + 3] = (byte) v;
|
||||
}
|
||||
|
||||
static String hexHead(byte[] b, int n) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < Math.min(n, b.length); i++) sb.append(String.format("%02x", b[i]));
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
syntax = "proto3";
|
||||
|
||||
// The schema that Codecs.pbWrite/pbRead follow by hand (field numbers and wire types).
|
||||
message Order {
|
||||
int64 id = 1;
|
||||
string customer = 2;
|
||||
string currency = 3;
|
||||
repeated Line lines = 4;
|
||||
}
|
||||
|
||||
message Line {
|
||||
string sku = 1;
|
||||
int32 quantity = 2;
|
||||
int64 price_minor = 3;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.ankurm.serialization;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.*;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SerializationTest {
|
||||
|
||||
@Test void uidMismatchThrowsInvalidClassException() throws Exception {
|
||||
byte[] b = Wire.write(new UidInStreamDemo.Ticket());
|
||||
b[UidInStreamDemo.uidOffset(UidInStreamDemo.Ticket.class) + 7] = 2;
|
||||
InvalidClassException e = assertThrows(InvalidClassException.class, () -> Wire.read(b));
|
||||
assertTrue(e.getMessage().contains("local class incompatible"));
|
||||
}
|
||||
|
||||
@Test void readObjectRunsBeforeTheCast() throws Exception {
|
||||
PrintStream old = System.out;
|
||||
ByteArrayOutputStream cap = new ByteArrayOutputStream();
|
||||
System.setOut(new PrintStream(cap));
|
||||
try {
|
||||
byte[] b = Wire.write(new ReadObjectRunsCodeDemo.Noisy());
|
||||
assertThrows(ClassCastException.class, () -> { ReadObjectRunsCodeDemo.Greeting g = (ReadObjectRunsCodeDemo.Greeting) Wire.read(b); });
|
||||
} finally { System.setOut(old); }
|
||||
assertTrue(cap.toString().contains("Noisy.readObject() is running"));
|
||||
}
|
||||
|
||||
@Test void allowListRejectsUnexpectedClass() throws Exception {
|
||||
byte[] b = Wire.write(new ReadObjectRunsCodeDemo.Noisy());
|
||||
ObjectInputFilter f = ObjectInputFilter.Config.createFilter("com.ankurm.serialization.ReadObjectRunsCodeDemo$Greeting;!*");
|
||||
assertThrows(InvalidClassException.class, () -> Wire.read(b, f));
|
||||
}
|
||||
|
||||
@Test void maxarrayRejectsForgedLengthBeforeAllocation() throws Exception {
|
||||
byte[] bomb = ResourceLimitsDemo.forgedArray(1_000_000_000);
|
||||
ObjectInputFilter f = ObjectInputFilter.Config.createFilter("maxarray=100000");
|
||||
assertThrows(InvalidClassException.class, () -> Wire.read(bomb, f));
|
||||
}
|
||||
|
||||
@Test void maxdepthRejectsDeepChain() throws Exception {
|
||||
byte[] deep = Wire.write(ResourceLimitsDemo.chain(200));
|
||||
assertNotNull(Wire.read(deep));
|
||||
ObjectInputFilter f = ObjectInputFilter.Config.createFilter("maxdepth=50");
|
||||
assertThrows(InvalidClassException.class, () -> Wire.read(deep, f));
|
||||
}
|
||||
|
||||
@Test void maxbytesChecksAtCallbacksNotAtTheArrayHeader() throws Exception {
|
||||
ObjectInputFilter f = ObjectInputFilter.Config.createFilter("maxbytes=10000");
|
||||
assertNotNull(Wire.read(Wire.write(new byte[50_000]), f)); // single array: accepted
|
||||
ArrayList<Integer> many = new ArrayList<>();
|
||||
for (int i = 0; i < 5_000; i++) many.add(i);
|
||||
assertThrows(InvalidClassException.class, () -> Wire.read(Wire.write(many), f));
|
||||
}
|
||||
|
||||
@Test void recordConstructorValidationRunsOnDeserialization() throws Exception {
|
||||
byte[] rec = Wire.write(new RecordsDemo.AgeRecord(30));
|
||||
Wire.putInt(rec, rec.length - 4, -5);
|
||||
InvalidObjectException e = assertThrows(InvalidObjectException.class, () -> Wire.read(rec));
|
||||
assertInstanceOf(IllegalArgumentException.class, e.getCause());
|
||||
}
|
||||
|
||||
@Test void plainClassSkipsItsConstructor() throws Exception {
|
||||
byte[] cls = Wire.write(new RecordsDemo.AgeClass(30));
|
||||
Wire.putInt(cls, cls.length - 4, -5);
|
||||
assertEquals(-5, ((RecordsDemo.AgeClass) Wire.read(cls)).years);
|
||||
}
|
||||
|
||||
@Test void recordDefaultUidIsZero() {
|
||||
assertEquals(0L, ObjectStreamClass.lookup(RecordsDemo.AgeRecord.class).getSerialVersionUID());
|
||||
}
|
||||
|
||||
@Test void allThreeFormatsRoundTrip() throws Exception {
|
||||
Order o = Order.sample();
|
||||
assertEquals(o, Codecs.javaRead(Codecs.javaWrite(o)));
|
||||
assertEquals(o, Codecs.jsonRead(Codecs.jsonWrite(o)));
|
||||
assertEquals(o, Codecs.pbRead(Codecs.pbWrite(o)));
|
||||
}
|
||||
|
||||
@Test void sizeOrderingIsJavaGreaterThanJsonGreaterThanProtobuf() throws Exception {
|
||||
Order o = Order.sample();
|
||||
int j = Codecs.javaWrite(o).length, s = Codecs.jsonWrite(o).length, p = Codecs.pbWrite(o).length;
|
||||
assertTrue(j > s && s > p, j + " " + s + " " + p);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.ankurm.serialization.drift;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class DriftRead {
|
||||
public static void main(String[] args) throws Exception {
|
||||
long uid = ObjectStreamClass.lookup(Account.class).getSerialVersionUID();
|
||||
System.out.println("this class's serialVersionUID = " + uid);
|
||||
try (ObjectInputStream in = new ObjectInputStream(Files.newInputStream(Path.of(args[0])))) {
|
||||
System.out.println("read: " + in.readObject());
|
||||
} catch (InvalidClassException e) {
|
||||
System.out.println("InvalidClassException: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ankurm.serialization.drift;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
|
||||
public class DriftWrite {
|
||||
public static void main(String[] args) throws Exception {
|
||||
Path file = Path.of(args[0]);
|
||||
try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(file))) {
|
||||
out.writeObject(new Account("asha", 500));
|
||||
}
|
||||
long uid = ObjectStreamClass.lookup(Account.class).getSerialVersionUID();
|
||||
System.out.println("wrote " + Files.size(file) + " bytes; serialVersionUID in stream = " + uid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.ankurm.serialization.drift;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** Version 1 of the class: two fields. The marker line below is replaced by run-all.sh. */
|
||||
public class Account implements Serializable {
|
||||
/*UID*/
|
||||
final String owner;
|
||||
final long balance;
|
||||
|
||||
public Account(String owner, long balance) { this.owner = owner; this.balance = balance; }
|
||||
|
||||
@Override public String toString() { return "Account[owner=" + owner + ", balance=" + balance + "]"; }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.ankurm.serialization.drift;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/** Version 2 of the class: one extra field, email. The marker line below is replaced by run-all.sh. */
|
||||
public class Account implements Serializable {
|
||||
/*UID*/
|
||||
final String owner;
|
||||
final long balance;
|
||||
final String email;
|
||||
|
||||
public Account(String owner, long balance) { this.owner = owner; this.balance = balance; this.email = null; }
|
||||
|
||||
@Override public String toString() { return "Account[owner=" + owner + ", balance=" + balance + ", email=" + email + "]"; }
|
||||
}
|
||||
Reference in New Issue
Block a user