SpringBoot SerializationUtils cloning (deserialization) class loader inconsistency problem (ClassCastException)

Problem analysis

When using the org.apache.commons.lang.SerializationUtils.clone method in SpringBoot, it was found that a type inconsistency error occurred when the cloned class was forced to the corresponding class. After inspection, it was found that two seemingly identical Class loaders for classes are inconsistent

Scene

Error message

java.lang.ClassCastException: com.tianqiauto.tis.pc.dingdanyupai.po.PrePoint cannot be cast to com.tianqiauto.tis.pc.dingdanyupai.po.PrePoint

Detection information

Solution analysis

Now that the class loader is found to be inconsistent, you need to find out how the class loader is specified when the class is deserialized.

When delving into the SerializationUtils.clone method, we found that the bytecode is converted into an object through jdk’s deserialization class ObjectInputStream

It is found that the returned object is created by cons, and cons is a Constructor, then you need to determine where cons is generated , thus inferring the basis for generating the class loader, and cons exists in ObjectStreamClass, enter ObjectStreamClass desc = readClassDesc(false); to determine < When does code>cons assign a value?

It is found that when desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false)); is executed, cons has a value and enters initNonProxyIn method

It was found that it ultimately pointed to a map of Caches.localDescs

localDescs is a global static variable, so you need to know where to add the value

Breakpoint debugging found that new ObjectStreamClass(cl) will be placed in localDescs during serialization, so enter new ObjectStreamClass(cl) In the method, find the source of cons

It is found that cons is generated by Class cl, which means that the class loader information of cons is generated by Class Determined by the class loader of cl, reverse search for the loading of cl

It is found that the class loading information of cl is provided by latestUserDefinedLoader(). After consulting the data, it is found that latestUserDefinedLoader() will find the first non-root based on the stack frame information. Class loader or extended class loader, and SerializationUtils belongs to the scope loaded by ApplicationClassLoader, so the object returned by SerializationUtils.clone(point) is ApplicationClassLoader loads

Solution

Option 1 (recommended)

Copy the methods in SerializationUtils.clone to the project

public class TestClassLoaderController {<!-- -->

    private static PrePoint point = new PrePoint();


    @GetMapping("/testClassLoader")
    public AjaxResult testClassLoader() {<!-- -->
        PrePoint deserialize = (PrePoint) cloneObject(point);
        return null;

    }

    private static Object cloneObject(PrePoint point) {<!-- -->
        ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
        serialize(point, baos);
        byte[] bytes = baos.toByteArray();
        return deserialize(bytes);
    }

    public static Object deserialize(byte[] objectData) {<!-- -->
        if (objectData == null) {<!-- -->
            throw new IllegalArgumentException("The byte[] must not be null");
        }
        ByteArrayInputStream bais = new ByteArrayInputStream(objectData);
        return deserialize(bais);
    }

    public static Object deserialize(InputStream inputStream) {<!-- -->
        if (inputStream == null) {<!-- -->
            throw new IllegalArgumentException("The InputStream must not be null");
        }
        ObjectInputStream in = null;
        try {<!-- -->
            // stream closed in the finally
            in = new ObjectInputStream(inputStream);
            return in.readObject();

        } catch (ClassNotFoundException ex) {<!-- -->
            throw new SerializationException(ex);
        } catch (IOException ex) {<!-- -->
            throw new SerializationException(ex);
        } finally {<!-- -->
            try {<!-- -->
                if (in != null) {<!-- -->
                    in.close();
                }
            } catch (IOException ex) {<!-- -->
                // ignore close exception
            }
        }
    }

    public static void serialize(Serializable obj, OutputStream outputStream) {<!-- -->
        if (outputStream == null) {<!-- -->
            throw new IllegalArgumentException("The OutputStream must not be null");
        }
        ObjectOutputStream out = null;
        try {<!-- -->
            // stream closed in the finally
            out = new ObjectOutputStream(outputStream);
            out.writeObject(obj);

        } catch (IOException ex) {<!-- -->
            throw new SerializationException(ex);
        } finally {<!-- -->
            try {<!-- -->
                if (out != null) {<!-- -->
                    out.close();
                }
            } catch (IOException ex) {<!-- -->
                // ignore close exception
            }
        }
    }
}

Option 2

Turn off hot reloading

spring:
  devtools:
    restart:
      enabled: false

or

@SpringBootApplication
public class SSMPApplication {<!-- -->
    public static void main(String[] args) {<!-- -->
        System.setProperty("spring.devtools.restart.enabled","false");
        SpringApplication.run(SSMPApplication.class);
    }
}

Option three

Remove hot-loaded jar packages

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
  <optional>true</optional> <!-- This needs to be true for hot deployment to be effective -->
</dependency>