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 |
|
| [`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 |
|
| [`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 |
|
| [`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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<module>hashmap-concurrenthashmap</module>
|
<module>hashmap-concurrenthashmap</module>
|
||||||
<module>exceptions</module>
|
<module>exceptions</module>
|
||||||
<module>regex</module>
|
<module>regex</module>
|
||||||
|
<module>serialization</module>
|
||||||
</modules>
|
</modules>
|
||||||
|
|
||||||
<properties>
|
<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