ValkeyCacheService: pluggable ObjectInputStream for cross-relocation reads#5
ValkeyCacheService: pluggable ObjectInputStream for cross-relocation reads#5frew wants to merge 2 commits into
Conversation
…reads
Adds an ObjectInputStreamFactory so callers can supply an ObjectInputStream
that bridges shaded ("streak."-relocated) and un-relocated class names. Lets
processes built with different shadowJar relocations share one Valkey cache
without ClassNotFoundException on each other's Java-serialized entries. Defaults
to a plain ObjectInputStream, so existing callers are unaffected.
There was a problem hiding this comment.
Code Review
This pull request introduces an ObjectInputStreamFactory functional interface to ValkeyCacheService, allowing callers to supply custom ObjectInputStream implementations (e.g., for class name remapping during deserialization). It adds new constructors to accept this factory, updates the deserialization logic to use it, and includes corresponding unit tests. The feedback suggests adding defensive null checks for the client parameter in the constructor and for the ObjectInputStream returned by the factory to prevent potential NullPointerExceptions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (defaultExpirationSeconds <= 0) { | ||
| throw new IllegalArgumentException("defaultExpirationSeconds must be positive, got " + defaultExpirationSeconds); | ||
| } | ||
| this.client = client; |
There was a problem hiding this comment.
The client parameter is a required dependency for ValkeyCacheService but is currently assigned directly without a null check. To prevent potential NullPointerExceptions later during cache operations, it is recommended to enforce defensive programming by validating that client is not null using Objects.requireNonNull.
| this.client = client; | |
| this.client = Objects.requireNonNull(client, "client"); |
| final ObjectInputStream ois = objectInputStreamFactory.create(bais)) { | ||
| return ois.readObject(); |
There was a problem hiding this comment.
If a custom ObjectInputStreamFactory implementation incorrectly returns null, calling ois.readObject() will throw a generic NullPointerException. To adhere to defensive programming practices, we should check if the returned ObjectInputStream is null using Objects.requireNonNull.
| final ObjectInputStream ois = objectInputStreamFactory.create(bais)) { | |
| return ois.readObject(); | |
| final ObjectInputStream ois = Objects.requireNonNull(objectInputStreamFactory.create(bais), "ObjectInputStreamFactory returned null")) { | |
| return ois.readObject(); |
Why
MailFoo shares one Valkey Objectify entity cache across processes that relocate
com.google.*(Guava, protobuf) differently in their shadowJars (app server = un-relocated; ServerSpark/JettyServers =streak.-relocated).ValkeyCacheService.deserializeused a plainObjectInputStream, and JDK serialization embeds the writer's class names, so a relocated writer's entry fails in an un-relocated reader with:Change
ObjectInputStreamFactoryhook;deserializebuilds its stream through it. Defaults to a plainObjectInputStream, so existing callers are unaffected.KeySubstitutingObjectInputStreamthat bridges shaded ↔ canonical names (inreadClassDescriptor(), the only hook allowed to substitute a differently-named class post-JDK-17).(client, factory)and(client, ttl, factory).Tests
injectedFactoryRemapsClassNamesOnRead(descriptor swap honored on the read path) andfactoryConstructorRoundTripsAndAppliesDefaultTtl.-7plus the factory hook, published as6.1.4-streak-valkey-8.Note: the Valkey testcontainers suite requires Docker;
test-compileand the new logic were verified locally.