From 97e7352f454953f9ccbcb4f001bef24b8531889e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 19:03:28 +0000 Subject: [PATCH] serialization: Java serialization companion code (UID drift, JEP 290/415 filters, records, JSON/Protobuf comparison) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01KqJyCidz3ZgRyHABv2GVJh --- README.md | 1 + pom.xml | 1 + serialization/README.md | 42 ++++++++ serialization/output/01-uid-in-stream.txt | 4 + serialization/output/02-drift-implicit.txt | 6 ++ serialization/output/03-drift-explicit.txt | 6 ++ .../output/04-readobject-runs-code.txt | 6 ++ serialization/output/05-resource-limits.txt | 16 +++ serialization/output/06-records.txt | 5 + serialization/output/07-filter-factory.txt | 13 +++ serialization/output/08-format-sizes.txt | 10 ++ serialization/output/09-jmh-raw.txt | 4 + serialization/output/10-tests.txt | 4 + serialization/pom.xml | 100 ++++++++++++++++++ serialization/scripts/run-all.sh | 57 ++++++++++ .../java/com/ankurm/serialization/Codecs.java | 77 ++++++++++++++ .../serialization/FilterFactoryDemo.java | 61 +++++++++++ .../ankurm/serialization/FormatSizeDemo.java | 28 +++++ .../java/com/ankurm/serialization/Order.java | 17 +++ .../serialization/ReadObjectRunsCodeDemo.java | 46 ++++++++ .../com/ankurm/serialization/RecordsDemo.java | 42 ++++++++ .../serialization/ResourceLimitsDemo.java | 75 +++++++++++++ .../serialization/SerializationBenchmark.java | 19 ++++ .../ankurm/serialization/UidInStreamDemo.java | 31 ++++++ .../java/com/ankurm/serialization/Wire.java | 36 +++++++ serialization/src/main/order.proto | 15 +++ .../serialization/SerializationTest.java | 85 +++++++++++++++ .../src/versions/common/DriftRead.java | 16 +++ .../src/versions/common/DriftWrite.java | 15 +++ serialization/src/versions/v1/Account.java | 14 +++ serialization/src/versions/v2/Account.java | 15 +++ 31 files changed, 867 insertions(+) create mode 100644 serialization/README.md create mode 100644 serialization/output/01-uid-in-stream.txt create mode 100644 serialization/output/02-drift-implicit.txt create mode 100644 serialization/output/03-drift-explicit.txt create mode 100644 serialization/output/04-readobject-runs-code.txt create mode 100644 serialization/output/05-resource-limits.txt create mode 100644 serialization/output/06-records.txt create mode 100644 serialization/output/07-filter-factory.txt create mode 100644 serialization/output/08-format-sizes.txt create mode 100644 serialization/output/09-jmh-raw.txt create mode 100644 serialization/output/10-tests.txt create mode 100644 serialization/pom.xml create mode 100755 serialization/scripts/run-all.sh create mode 100644 serialization/src/main/java/com/ankurm/serialization/Codecs.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/FilterFactoryDemo.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/FormatSizeDemo.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/Order.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/ReadObjectRunsCodeDemo.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/RecordsDemo.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/ResourceLimitsDemo.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/SerializationBenchmark.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/UidInStreamDemo.java create mode 100644 serialization/src/main/java/com/ankurm/serialization/Wire.java create mode 100644 serialization/src/main/order.proto create mode 100644 serialization/src/test/java/com/ankurm/serialization/SerializationTest.java create mode 100644 serialization/src/versions/common/DriftRead.java create mode 100644 serialization/src/versions/common/DriftWrite.java create mode 100644 serialization/src/versions/v1/Account.java create mode 100644 serialization/src/versions/v2/Account.java diff --git a/README.md b/README.md index e9fb543..b4de231 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/pom.xml b/pom.xml index 94b6fef..1ef3162 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,7 @@ hashmap-concurrenthashmap exceptions regex + serialization diff --git a/serialization/README.md b/serialization/README.md new file mode 100644 index 0000000..c1b4497 --- /dev/null +++ b/serialization/README.md @@ -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. diff --git a/serialization/output/01-uid-in-stream.txt b/serialization/output/01-uid-in-stream.txt new file mode 100644 index 0000000..52d74f0 --- /dev/null +++ b/serialization/output/01-uid-in-stream.txt @@ -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 diff --git a/serialization/output/02-drift-implicit.txt b/serialization/output/02-drift-implicit.txt new file mode 100644 index 0000000..eb6fe44 --- /dev/null +++ b/serialization/output/02-drift-implicit.txt @@ -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 diff --git a/serialization/output/03-drift-explicit.txt b/serialization/output/03-drift-explicit.txt new file mode 100644 index 0000000..8f0f975 --- /dev/null +++ b/serialization/output/03-drift-explicit.txt @@ -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] diff --git a/serialization/output/04-readobject-runs-code.txt b/serialization/output/04-readobject-runs-code.txt new file mode 100644 index 0000000..18bc4ee --- /dev/null +++ b/serialization/output/04-readobject-runs-code.txt @@ -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 diff --git a/serialization/output/05-resource-limits.txt b/serialization/output/05-resource-limits.txt new file mode 100644 index 0000000..310beae --- /dev/null +++ b/serialization/output/05-resource-limits.txt @@ -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 diff --git a/serialization/output/06-records.txt b/serialization/output/06-records.txt new file mode 100644 index 0000000..7bd318b --- /dev/null +++ b/serialization/output/06-records.txt @@ -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) diff --git a/serialization/output/07-filter-factory.txt b/serialization/output/07-filter-factory.txt new file mode 100644 index 0000000..63dc6d6 --- /dev/null +++ b/serialization/output/07-filter-factory.txt @@ -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 diff --git a/serialization/output/08-format-sizes.txt b/serialization/output/08-format-sizes.txt new file mode 100644 index 0000000..7e0a0ae --- /dev/null +++ b/serialization/output/08-format-sizes.txt @@ -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=[]] diff --git a/serialization/output/09-jmh-raw.txt b/serialization/output/09-jmh-raw.txt new file mode 100644 index 0000000..f0c4977 --- /dev/null +++ b/serialization/output/09-jmh-raw.txt @@ -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 diff --git a/serialization/output/10-tests.txt b/serialization/output/10-tests.txt new file mode 100644 index 0000000..07b78da --- /dev/null +++ b/serialization/output/10-tests.txt @@ -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 diff --git a/serialization/pom.xml b/serialization/pom.xml new file mode 100644 index 0000000..4838b67 --- /dev/null +++ b/serialization/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + + + com.ankurm + java-core-examples + 1.0 + + + serialization + serialization + Java serialization in 2026: serialVersionUID drift, deserialization filters (JEP 290/415), records, and JSON/Protobuf alternatives with size and speed numbers. + + + 1.37 + 3.2.3 + 4.36.2 + + + + + org.openjdk.jmh + jmh-core + ${jmh.version} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + tools.jackson.core + jackson-databind + ${jackson.version} + + + com.google.protobuf + protobuf-java + ${protobuf.version} + + + org.junit.jupiter + junit-jupiter + 5.11.0 + test + + + + + benchmarks + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 25 + + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh.version} + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.2.5 + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + shade + + + + org.openjdk.jmh.Main + + + + + + + + + diff --git a/serialization/scripts/run-all.sh b/serialization/scripts/run-all.sh new file mode 100755 index 0000000..edf4a6a --- /dev/null +++ b/serialization/scripts/run-all.sh @@ -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 diff --git a/serialization/src/main/java/com/ankurm/serialization/Codecs.java b/serialization/src/main/java/com/ankurm/serialization/Codecs.java new file mode 100644 index 0000000..c80956e --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/Codecs.java @@ -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 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); + } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/FilterFactoryDemo.java b/serialization/src/main/java/com/ankurm/serialization/FilterFactoryDemo.java new file mode 100644 index 0000000..da9395c --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/FilterFactoryDemo.java @@ -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 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 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()); + } + } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/FormatSizeDemo.java b/serialization/src/main/java/com/ankurm/serialization/FormatSizeDemo.java new file mode 100644 index 0000000..c669af3 --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/FormatSizeDemo.java @@ -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("")); + } + } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/Order.java b/serialization/src/main/java/com/ankurm/serialization/Order.java new file mode 100644 index 0000000..4916b86 --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/Order.java @@ -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 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))); + } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/ReadObjectRunsCodeDemo.java b/serialization/src/main/java/com/ankurm/serialization/ReadObjectRunsCodeDemo.java new file mode 100644 index 0000000..7128450 --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/ReadObjectRunsCodeDemo.java @@ -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()); + } + } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/RecordsDemo.java b/serialization/src/main/java/com/ankurm/serialization/RecordsDemo.java new file mode 100644 index 0000000..f00316d --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/RecordsDemo.java @@ -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)"); + } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/ResourceLimitsDemo.java b/serialization/src/main/java/com/ankurm/serialization/ResourceLimitsDemo.java new file mode 100644 index 0000000..16672ad --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/ResourceLimitsDemo.java @@ -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 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")); + } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/SerializationBenchmark.java b/serialization/src/main/java/com/ankurm/serialization/SerializationBenchmark.java new file mode 100644 index 0000000..c4fa48b --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/SerializationBenchmark.java @@ -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)); } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/UidInStreamDemo.java b/serialization/src/main/java/com/ankurm/serialization/UidInStreamDemo.java new file mode 100644 index 0000000..b7e0592 --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/UidInStreamDemo.java @@ -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()); + } + } +} diff --git a/serialization/src/main/java/com/ankurm/serialization/Wire.java b/serialization/src/main/java/com/ankurm/serialization/Wire.java new file mode 100644 index 0000000..bc111e7 --- /dev/null +++ b/serialization/src/main/java/com/ankurm/serialization/Wire.java @@ -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(); + } +} diff --git a/serialization/src/main/order.proto b/serialization/src/main/order.proto new file mode 100644 index 0000000..57eec02 --- /dev/null +++ b/serialization/src/main/order.proto @@ -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; +} diff --git a/serialization/src/test/java/com/ankurm/serialization/SerializationTest.java b/serialization/src/test/java/com/ankurm/serialization/SerializationTest.java new file mode 100644 index 0000000..7223676 --- /dev/null +++ b/serialization/src/test/java/com/ankurm/serialization/SerializationTest.java @@ -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 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); + } +} diff --git a/serialization/src/versions/common/DriftRead.java b/serialization/src/versions/common/DriftRead.java new file mode 100644 index 0000000..85a27ea --- /dev/null +++ b/serialization/src/versions/common/DriftRead.java @@ -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()); + } + } +} diff --git a/serialization/src/versions/common/DriftWrite.java b/serialization/src/versions/common/DriftWrite.java new file mode 100644 index 0000000..1dc6724 --- /dev/null +++ b/serialization/src/versions/common/DriftWrite.java @@ -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); + } +} diff --git a/serialization/src/versions/v1/Account.java b/serialization/src/versions/v1/Account.java new file mode 100644 index 0000000..5f743a8 --- /dev/null +++ b/serialization/src/versions/v1/Account.java @@ -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 + "]"; } +} diff --git a/serialization/src/versions/v2/Account.java b/serialization/src/versions/v2/Account.java new file mode 100644 index 0000000..265a587 --- /dev/null +++ b/serialization/src/versions/v2/Account.java @@ -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 + "]"; } +}