Skip to content

Commit 7aefb05

Browse files
committed
Add serialVersionUID tolerant java deserializer
Workaround for spring-projects#3737
1 parent 9759950 commit 7aefb05

File tree

4 files changed

+228
-0
lines changed

4 files changed

+228
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.support.serializer;
18+
19+
import java.io.IOException;
20+
import java.io.InputStream;
21+
import java.io.ObjectInputStream;
22+
import java.io.ObjectStreamClass;
23+
import java.io.Serializable;
24+
25+
import org.springframework.core.NestedIOException;
26+
import org.springframework.core.serializer.Deserializer;
27+
import org.springframework.util.LinkedMultiValueMap;
28+
import org.springframework.util.MultiValueMap;
29+
30+
/**
31+
* A {@link Deserializer} implementation that reads an input stream using Java serialization in a manner that
32+
* allows {@link Serializable} classes to be deserialized with more than one {@code serialVersionUID}.
33+
*
34+
* <pre>
35+
* <code>
36+
* VersionTolerantJavaDeserializer deser = new VersionTolerantJavaDeserializer();
37+
* deser.allowSerialVersionUID(MessageHistory.class, 1426799817181873282L);
38+
* deser.allowSerialVersionUID(MessageHistory.class, -2340400235574314134L);
39+
*
40+
* JdbcMessageStore messageStore = ...
41+
* messageStore.setDeserializer(deser);
42+
* Message{@literal <}Foo{@literal >} oldFoo = messageStore.getMessage(oldFooMsgUid);
43+
* </code>
44+
* </pre>
45+
*
46+
* @author Chris Bono
47+
*/
48+
public class VersionTolerantJavaDeserializer implements Deserializer<Object> {
49+
50+
private final MultiValueMap<Class<?>, Long> alternativeSerialVersionUIDs = new LinkedMultiValueMap<>();
51+
52+
/**
53+
* Allows an alternative {@code serialVersionUID} to be used when deserializing a class.
54+
* @param clazz the class
55+
* @param serialVersionUID the alternate uid to allow
56+
*/
57+
public void allowSerialVersionUID(Class<?> clazz, long serialVersionUID) {
58+
this.alternativeSerialVersionUIDs.add(clazz, serialVersionUID);
59+
}
60+
61+
@Override
62+
public Object deserialize(InputStream inputStream) throws IOException {
63+
VersionTolerantObjectInputStream objectInputStream = new VersionTolerantObjectInputStream(inputStream);
64+
try {
65+
return objectInputStream.readObject();
66+
}
67+
catch (ClassNotFoundException ex) {
68+
throw new NestedIOException("Failed to deserialize object type", ex);
69+
}
70+
}
71+
72+
private class VersionTolerantObjectInputStream extends ObjectInputStream {
73+
74+
private VersionTolerantObjectInputStream(InputStream in) throws IOException {
75+
super(in);
76+
}
77+
78+
@Override
79+
protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
80+
ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();
81+
82+
String className = resultClassDescriptor.getName();
83+
Class<?> localClass = Class.forName(className);
84+
if (!alternativeSerialVersionUIDs().containsKey(localClass)) {
85+
return resultClassDescriptor;
86+
}
87+
88+
ObjectStreamClass localClassDescriptor = ObjectStreamClass.lookup(localClass);
89+
if (localClassDescriptor == null) {
90+
return resultClassDescriptor;
91+
}
92+
93+
final long localSerialVersionUID = localClassDescriptor.getSerialVersionUID();
94+
final long streamSerialVersionUID = resultClassDescriptor.getSerialVersionUID();
95+
if (streamSerialVersionUID != localSerialVersionUID && alternativeSerialVersionUIDs().get(localClass)
96+
.contains(streamSerialVersionUID)) {
97+
return localClassDescriptor;
98+
}
99+
100+
return resultClassDescriptor;
101+
}
102+
103+
private MultiValueMap<Class<?>, Long> alternativeSerialVersionUIDs() {
104+
return VersionTolerantJavaDeserializer.this.alternativeSerialVersionUIDs;
105+
}
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.integration.support.serializer;
18+
19+
import java.io.FileInputStream;
20+
import java.io.FileOutputStream;
21+
import java.io.InvalidClassException;
22+
import java.io.ObjectInputStream;
23+
import java.io.ObjectOutputStream;
24+
import java.io.Serializable;
25+
26+
import org.junit.jupiter.api.Disabled;
27+
import org.junit.jupiter.api.Nested;
28+
import org.junit.jupiter.api.Test;
29+
30+
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
32+
33+
/**
34+
* Unit tests for {@link VersionTolerantJavaDeserializer}.
35+
*/
36+
public class VersionTolerantJavaDeserializerTests {
37+
38+
@Test
39+
void failsWhenNoAlternateUIDsConfigured() throws Exception {
40+
assertThat(Foo.serialVersionUID).isEqualTo(3L);
41+
String fileName = "./src/test/resources/deser/foo2";
42+
VersionTolerantJavaDeserializer deser = new VersionTolerantJavaDeserializer();
43+
try (FileInputStream fis = new FileInputStream(fileName)) {
44+
assertThatThrownBy(() -> deser.deserialize(fis))
45+
.isInstanceOf(InvalidClassException.class);
46+
}
47+
}
48+
49+
@Test
50+
void passesWhenAlternateUIDsConfigured() throws Exception {
51+
assertThat(Foo.serialVersionUID).isEqualTo(3L);
52+
53+
VersionTolerantJavaDeserializer deser = new VersionTolerantJavaDeserializer();
54+
deser.allowSerialVersionUID(VersionTolerantJavaDeserializerTests.Foo.class, 1L);
55+
deser.allowSerialVersionUID(VersionTolerantJavaDeserializerTests.Foo.class, 2L);
56+
57+
String fileName = "./src/test/resources/deser/foo1";
58+
try (FileInputStream fis = new FileInputStream(fileName)) {
59+
VersionTolerantJavaDeserializerTests.Foo oldFoo = (VersionTolerantJavaDeserializerTests.Foo) deser.deserialize(fis);
60+
assertThat(oldFoo.toString()).isEqualTo("Foo (name=uno, serialVersionUID=3)");
61+
}
62+
63+
fileName = "./src/test/resources/deser/foo2";
64+
try (FileInputStream fis = new FileInputStream(fileName)) {
65+
VersionTolerantJavaDeserializerTests.Foo oldFoo = (VersionTolerantJavaDeserializerTests.Foo) deser.deserialize(fis);
66+
assertThat(oldFoo.toString()).isEqualTo("Foo (name=dos, serialVersionUID=3)");
67+
}
68+
}
69+
70+
@Disabled
71+
@Nested
72+
class GenerateSerializedFiles {
73+
74+
@Test
75+
void generateOldFoo1SerializedFile() throws Exception {
76+
// Only run this once to generate the Foo1 serialized file - make sure Foo.serialVersionUID = 1L before running
77+
assertThat(Foo.serialVersionUID).isEqualTo(1L);
78+
String filename = "./src/test/resources/deser/foo1";
79+
doGenerateOldFooSerializedFile(filename, new Foo("uno"));
80+
}
81+
82+
@Test
83+
void generateOldFoo2SerializedFile() throws Exception {
84+
// Only run this once to generate the Foo2 serialized file - make sure Foo.serialVersionUID = 2L before running
85+
assertThat(Foo.serialVersionUID).isEqualTo(2L);
86+
String filename = "./src/test/resources/deser/foo2";
87+
doGenerateOldFooSerializedFile(filename, new Foo("dos"));
88+
}
89+
90+
private void doGenerateOldFooSerializedFile(String filename, Foo foo) throws Exception {
91+
FileOutputStream fos = new FileOutputStream(filename);
92+
ObjectOutputStream oos = new ObjectOutputStream(fos);
93+
oos.writeObject(foo);
94+
fos.close();
95+
FileInputStream fis = new FileInputStream(filename);
96+
ObjectInputStream ois = new ObjectInputStream(fis);
97+
Foo fooDeser = (Foo) ois.readObject();
98+
assertThat(fooDeser.getName()).isEqualTo(foo.getName());
99+
fis.close();
100+
}
101+
}
102+
103+
static class Foo implements Serializable {
104+
105+
private static final long serialVersionUID = 3L;
106+
107+
private String name;
108+
109+
public Foo(String name) {
110+
this.name = name;
111+
}
112+
113+
public String getName() {
114+
return this.name;
115+
}
116+
117+
public String toString() {
118+
return String.format("Foo (name=%s, serialVersionUID=%d)", this.name, serialVersionUID);
119+
}
120+
}
121+
}
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)