Skip to content

ResolvableType#getGenerics() breaks serialization #36346

@marschall

Description

@marschall

Calling ResolvableType#getGenerics() may cause a ResolvableType to become unserializable if the type is generic. Consider the following test

Spring version: 7.0.4 and main, likely 6.x but I didn't check
Java: OpenJDK 17, 21, 25

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.List;

import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;

import static org.assertj.core.api.Assertions.assertThat;

class SerializationTests {

  @Test
  void serialize() throws Exception {
    ResolvableType listType = ResolvableType.forClass(List.class);
    testSerialization(listType); // this works
    assertThat(listType.getGenerics()).hasSize(1);
    testSerialization(listType); // this breaks
  }

  private static ResolvableType testSerialization(ResolvableType type) throws Exception {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    try (ObjectOutputStream oos = new ObjectOutputStream(bos)) {
      oos.writeObject(type);
    }
    try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
      ResolvableType read = (ResolvableType) ois.readObject();
      assertThat(read).isEqualTo(type);
      assertThat(read.getType()).isEqualTo(type.getType());
      assertThat(read.resolve()).isEqualTo(type.resolve());
      return read;
    }
  }

}

Failing with the following exception

java.io.NotSerializableException: sun.reflect.generics.reflectiveObjects.TypeVariableImpl
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1085)
	at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1451)
	at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1408)
	at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1317)
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1079)
	at java.base/java.io.ObjectOutputStream.writeArray(ObjectOutputStream.java:1260)
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1075)
	at java.base/java.io.ObjectOutputStream.defaultWriteFields(ObjectOutputStream.java:1451)
	at java.base/java.io.ObjectOutputStream.writeSerialData(ObjectOutputStream.java:1408)
	at java.base/java.io.ObjectOutputStream.writeOrdinaryObject(ObjectOutputStream.java:1317)
	at java.base/java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1079)
	at java.base/java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:325)
	at SerializationTests.testSerialization(SerializationTests.java:26)
	at SerializationTests.serialize(SerializationTests.java:20)

The issues is that calling ResolvableType#getGenerics() lazily initializes the generics field with a ResolvedType that references a sun.reflect.generics.reflectiveObjects.TypeVariableImpl which is not serializable. Unfortunately not all implementations of java.lang.reflect.Type are Serializable.

Ideally this would be fixed in OpenJDK but users may not want to wait so long.

I didn't check whether the other lazily computed fields superType and interfaces have the same issue.

An possible fix for this specific issue would be to simply not serialize the lazily computed fields. However doing so in a way that does not break the serialized form can be a bit tricky. Possibly wither implementing writeObject or defining serialPersistentFields.

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)status: backportedAn issue that has been backported to maintenance branchestype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions