Blog-Archiv

Sonntag, 25. April 2021

No Java Stateful Enum Serialization

Java enums are safe against serialization, but in case a serialized enum value holds a state, this state will be lost. None of the fields we define on an enum will be serialized with any of the enum's values.

This Blog shows how we can easily try out serialization between two different JVMs.
For brevity I omitted any JavaDoc, which I would not recommend for real-world source-code.

Example Enum

It doesn't make a difference whether we define a non-serializable or a serializable field in our test-enum, because any field will be ignored. Here is an enum that simply wraps a mutable String field:

public enum StatefulEnum
{
    JOHNDOE("John Doe"),
    ONEMORE("One More"),
    ;
    
    private String state;
    
    private StatefulEnum(String state)    {
        this.state = state;
    }
    
    public String getState()	{
    	return state;
    }
    public void setState(String state)	{
    	this.state = state;
    }
}

This is the enum we are about to test. We will modify the state of StatefulEnum.JOHNDOE, then serialize that enum value, then deserialize it again and look whether it kept the modified state.

Serializer

What we need for this is a serialization utility:

import java.io.*;

public final class Serializer
{
    public static byte[] serialize(Object object)    {
        try {
            final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            final ObjectOutputStream objectStream = new ObjectOutputStream(byteStream);
            objectStream.writeObject(object);
            objectStream.close();
            return byteStream.toByteArray();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }   

    public static Object deserialize(byte[] bytes)    {
        try {
            final InputStream inputStream = new ByteArrayInputStream(bytes);
            return new ObjectInputStream(inputStream).readObject();
        }
        catch (Exception e) {
            throw new RuntimeException(e);
        }
    }   

    private Serializer() {}
}

Calling Serializer.serialize() we can turn any object into a byte array, calling Serializer.deserialize() we can turn the byte array back into an object that can be casted to its target type.

Serialization Test

Now we have to think about how we can test the enum behavior concerning serialization. Do we need to set up an RMI or JMS environment now?

We will use the file system as serialization target and deserialization source. That means instead of transferring data between two JVM instances running in parallel, we will perform two consecutive Java launches, the first writes to a file, the second reads from it.

Strategy is:

  • In case the file doesn't exist, an enum value's state will be modified and the value will be written into a file.

  • Afterwards the file will be read and the enum value will be deserialized and printed.

  • Finally the file will be deleted in case it existed on launching the application, so that the third launch again will write the file.

That way we can observe what happens when deserializing the enum value in the same JVM where we wrote it, and what happens when deserializing it in another JVM.

Here is the test application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.io.*;

public class EnumSerializationTest
{
    public static void main(String[] args) throws Exception {
        new EnumSerializationTest(StatefulEnum.JOHNDOE, "StatefulEnum.ser");
    }

    public EnumSerializationTest(StatefulEnum toSerialize, String fileName) throws Exception {
        final File file = new File(fileName);
        final boolean filePresent = file.exists();
        
        if (filePresent == false) {
            modifyEnumState(toSerialize, "Vasya Pupkin");
            writeEnumToFile(file, toSerialize);
        }
        
        final StatefulEnum deserialized = readEnumFromFile(file);
        System.out.println("Deserialized state of "+deserialized+" = "+deserialized.getState());
        
        if (filePresent == true) {
            file.delete();    // make ready for next launch
            System.out.println("Deleted file "+file);
        }
    }
    
    private void modifyEnumState(StatefulEnum statefulEnum, String newState) {
        System.out.println("Original state of "+statefulEnum+" = "+statefulEnum.getState());
        
        statefulEnum.setState(newState);
        
        System.out.println("Modified state of "+statefulEnum+" = "+statefulEnum.getState());
    }

    private void writeEnumToFile(final File file, final StatefulEnum statefulEnum) throws Exception {
        System.out.println("Writing "+statefulEnum+" = "+statefulEnum.getState()+" to "+file+" ....");
        
        final byte[] serializedBytes = Serializer.serialize(statefulEnum);
        try (final OutputStream outputStream = new FileOutputStream(file)) {
            outputStream.write(serializedBytes);
        }   // auto-close
    }

    private StatefulEnum readEnumFromFile(final File file) throws Exception {
        System.out.println("Reading file "+file+" ....");
        
        try (final InputStream inputStream = new FileInputStream(file)) {
            final byte[] deserializedBytes = inputStream.readAllBytes();
            return (StatefulEnum) Serializer.deserialize(deserializedBytes);
        }   // auto-close
    }
}

On line 6 we decide which enum value we will use for the test, and we assign a file name.

On line 13 we decide whether we have to write the file or just read it. In case the file doesn't exist, we put a new state into the enum value and serialize it to the file with given name, done on line 15.

In any case we read the now always existing serialization from the file and print it out, done in line 18 and 19. Mind that if the file already existed on application start, the JVM will read-in what another JVM had written before, else it will read-in what it wrote itself in the same run.

Finally, on line 22, we delete the file in case it existed on startup. Thus we toggle the existence of the file with every second launch, that makes the application handsome.

The remaining methods, from line 27 on, just do what their name tells, they read and write the file, and they contain print-statements so that we can see what happens. The try() statement will silently close any stream inside the parentheses. On line 49 is the cast-operator that must be applied when deserializing an object.

Can you guess what the output is?

Result

First launch:

java EnumSerializationTest
Original state of JOHNDOE = John Doe
Modified state of JOHNDOE = Vasya Pupkin
Writing JOHNDOE = Vasya Pupkin to StatefulEnum.ser ....
Reading file StatefulEnum.ser ....
Deserialized state of JOHNDOE = Vasya Pupkin

The enum value JOHNDOE was printed with its original state, then it got a new state "Vasya Pupkin", and was serialized with that state. When we read-in that enum value again, it looks like the state was preserved in the serialization. But this is not true.

Second launch:

java EnumSerializationTest
Reading file StatefulEnum.ser ....
Deserialized state of JOHNDOE = John Doe
Deleted file StatefulEnum.ser

Because this was another JVM that holds a pristine JOHNDOE, it turns out that "Vasya Pupkin" has not been serialized into the persistent file. The state of the deserialized JOHNDOE is the initial "John Doe".

So why did the first launch look successful in serializing the state?
Serialization of enums is different from that of normal objects. While normal objects would throw an exception on serialization with non-serializable fields, this doesn't happen with enums (although I didn't demonstrate this here). On deserialization, the ObjectInputStream associates any deserialized enum value with the existing one (enum values are singletons!), and if we print it, we see whatever state has been attached to the existing one. This explains the misleading output of the first launch.

Conclusion

The Java enum documentation clearly states that fields of enum values will not be serialized. You can check this with the source-code above.




Keine Kommentare: