Skip to main content
  1. Posts/

Serialising in Java - Birds Eye View

Sven Ruppert
Author
Sven Ruppert
20+ years of Java, specialised in Security, Vaadin and Developer Relations. When not coding, you’ll find me in the woods with an axe.
Table of Contents

Serialisation in Java is implemented to convert the state of an object into a byte stream, which can be quickly persisted to a file or sent over a network. This process is essential for persisting object data, supporting network communication, and facilitating sharing of objects between different parts of a distributed system.

Basics of Serialization:
#

Object Serialization:
#

- Serialization is converting an object into a sequence of bytes.

- The serialised form of an object can be saved to a file or sent over a network.

- Deserialization is the process of reconstructing the object from its serialised form.

Serializable Interface:
#

- In Java, the java.io.Serializable interface makes a class serialisable.

- This interface acts as a marker interface, indicating that class objects can be serialised.

Transient Keyword:
#

- The transient keyword can mark instance variables that should not be serialised.

- Transient variables are not included in the serialised form when an object is serialised.

class MyClass implements Serializable {
    int normalVar;
    transient int transientVar;
}

The Serialisation Process:
#

ObjectOutput/InputStream:
#

- The ObjectOutputStream class is used for serialising objects.

- The ObjectInputStream class is used for deserialising objects.

Serialisable Fields:
#

- Only the serialisation process will include the fields of a class marked as serialisable (i.e., non-transient and their types are serialisable).

Serialisable Methods:
#

- If a class provides its own writeObject and readObject methods, these methods will be responsible for the custom serialisation and deserialisation logic.

private void writeObject(ObjectOutputStream out) throws IOException {
    // Custom serialisation logic
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
    // Custom deserialisation logic
}

Serialisation Control:
#

SerialVersionUID:
#

- Every serialisable class has a version number, known as serialVersionUID.

- It is used during deserialisation to verify that the sender and receiver of a serialised object have loaded classes for that object that are compatible concerning serialisation.

Externalisable Interface:
#

In Java, the **Externalizable** interface provides a way to customise an object’s serialisation and deserialisation process. Unlike the Serializable interface, which performs automatic serialisation, **Externalizable** allows you more control over the process by implementing two methods: **writeExternal** and **readExternal** .

Here’s a simple example to demonstrate the use of Externalizable:

import java.io.*;

class Person implements Externalizable {
    private String name;
    private int age;
    // Required public no-argument constructor
    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // Custom serialization logic
        out.writeObject(name);
        out.writeInt(age);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        // Custom deserialization logic
        name = (String) in.readObject();
        age = in.readInt();
    }
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class ExternalizableExample {

    public static void main(String[] args) {
        // Create a Person object
        Person person = new Person("John", 30);
        // Serialize the object to a file
        try (ObjectOutputStream oos 
               = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
        } catch (IOException e) {
            e.printStackTrace();
        }

        // Deserialize the object from the file
        Person deserializedPerson = null;
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            deserializedPerson = (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }

        // Print the deserialized object
        if (deserializedPerson != null) {
            System.out.println("Deserialized Person: " + deserializedPerson);
        }
    }
}

In this example, the Person class implements the Externalizable interface and provides custom serialisation and deserialisation logic in the **writeExternal** and **readExternal** methods. The main method demonstrates how to create a Person object, serialise it to a file, and then deserialise it back into a new object.

Why should I use Externalizable?
#

The **Externalizable** interface in Java provides a more flexible and customised approach to serialisation compared to the default serialisation mechanism provided by the Serializable interface. Here are some reasons you might want to use Externalizable:

Selective Serialization :
#

With **Externalizable** , you have complete control over which fields get serialised and how they are serialised. This can be useful when you exclude certain fields from serialisation or perform custom serialisation for specific fields.

Performance Optimization:
#

**Externalizable** allows you to implement custom serialisation logic that might be more efficient than the default serialisation mechanism. You can choose to serialise only essential data, avoiding unnecessary information and potentially reducing the size of the serialised data.

Versioning Control:
#

When your class evolves and you need to maintain backwards or forward compatibility, Externalizable provides a way to handle versioning explicitly. You can implement custom logic to manage different versions of the serialised data, making it more adaptable to changes in your class structure.

Security Considerations:
#

Custom serialisation logic can help handle security-related concerns. For example, encrypt sensitive information before serialisation or perform other security checks during the serialisation/deserialisation process.

Resource Management:
#

With **Externalizable** , you control resource management during serialisation and deserialisation. You can explicitly release and acquire resources as needed, which can be crucial when resources must be managed carefully.

Reducing Serialized Size:
#

If your class has transient or derived fields that need not be serialised, you can exclude them from the serialised form, leading to a smaller serialised size.

It’s worth noting that while **Externalizable** provides more control, it also requires more manual effort to implement, as you need to write custom serialisation and deserialisation logic. In many cases, the default serialisation provided by Serializable is sufficient and more straightforward. However, **Externalizable** offers a powerful alternative for scenarios where customisation is necessary.

Summary :
#

Serialisation is a fundamental concept in Java that facilitates the storage and transmission of object data. It provides a versatile mechanism for persisting object states, supporting network communication, and enabling interoperability between distributed system components. While the default serialisation mechanism is suitable for many cases, understanding advanced topics like custom serialisation, versioning, and security considerations is essential for developing robust and efficient Java applications.

UseCases for Serialisation in Java
#

The main reasons for implementing serialisation in Java are as follows:

Persistence :
#

Serialisation allows objects to be saved to a file and later restored. This is useful for applications that need to store and retrieve the state of objects, such as in database interactions or for caching purposes.

Persistence - Example:
#

Let’s consider a simple example where we have a Person class that we want to persist to a file using serialisation.

import java.io.*;

class Person implements Serializable {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        // Create a Person object
        Person person = new Person("John Doe", 25);
        // Serialize the object to a file
        serializePerson(person, "person.ser");
        // Deserialize the object from the file
        Person deserializedPerson = deserializePerson("person.ser");
        // Display the deserialised object
        System.out.println("Deserialized Person: " + deserializedPerson);
    }

    private static void serializePerson(Person person, String fileName) {
        try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream(fileName))) {
            // Write the object to the file
            outputStream.writeObject(person);
            System.out.println("Serialization successful. Object saved to " + fileName);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static Person deserializePerson(String fileName) {
        Person person = null;
        try (ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(fileName))) {
            // Read the object from the file
            person = (Person) inputStream.readObject();
            System.out.println("Deserialization successful. Object loaded from " + fileName);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
        return person;
    }
}

In this example, the Person class implements the Serializable interface, indicating that its instances can be serialised. The SerializationExample class demonstrates how to serialise a Person object to a file (person.ser) and then deserialise it back into a new Person object. The serializePerson and deserializePerson methods handle the serialisation and deserialisation processes, respectively.

Network Communication :
#

Serialisation enables the easy transmission of objects between different Java Virtual Machines (JVMs) over a network. Converting objects to a byte stream can be sent across a network and reconstructed on the receiving end.

Network Communication - Example:
#

In this example, we’ll create a simple client-server communication scenario using Java sockets and serialisation. The server will send a serialised object to the client over the network.

Let’s start with the server:

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

class Person implements Serializable {
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }
}

public class Server {
    public static void main(String[] args) {
        try {
            // Create a server socket
            ServerSocket serverSocket = new ServerSocket(12345);
            System.out.println("Server listening on port 12345...");
            while (true) {
                // Wait for a client to connect
                Socket clientSocket = serverSocket.accept();
                System.out.println("Client connected: " + clientSocket.getInetAddress());
                // Create a Person object
                Person person = new Person("Alice", 30);
                // Serialize and send the object to the client
                sendObjectToClient(person, clientSocket);
                // Close the client socket
                clientSocket.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void sendObjectToClient(Serializable obj, Socket socket) {
        try (ObjectOutputStream outputStream = new ObjectOutputStream(socket.getOutputStream())) {
            // Write the object to the output stream
            outputStream.writeObject(obj);
            System.out.println("Object sent to client.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

And here’s the client:

import java.io.*;
import java.net.Socket;
public class Client {

    public static void main(String[] args) {
        try {
            // Connect to the server
            Socket socket = new Socket("localhost", 12345);
            // Receive the serialised object from the server
            Person receivedPerson = receiveObjectFromServer(socket);
            // Display the received object
            System.out.println("Received Person from server: " + receivedPerson);
            // Close the socket
            socket.close();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    private static Person receiveObjectFromServer(Socket socket) 
                               throws IOException, ClassNotFoundException {
        try (ObjectInputStream inputStream = new ObjectInputStream(socket.getInputStream())) {
            // Read the object from the input stream
            return (Person) inputStream.readObject();
        }
    }
}

In this example, the Server class listens for incoming connections on port 12345. When a client connects, it creates a Person object, serialises it, and sends it to the client using the sendObjectToClient method. The Client class connects to the server, receives the serialised Person object, and then deserialises and prints it.

Object Cloning :
#

Serialisation provides a way to create a deep copy of an object. By serialising an object and then deserialising it, you effectively create a new copy of the original object. This can be useful when you need to duplicate complex object structures.

Object Cloning - Example:
#

In this example, I’ll demonstrate object cloning using serialisation in Java. We’ll create a Person class and use serialisation and deserialisation to perform deep cloning.

import java.io.*;

class Address implements Serializable {
    private String street;
    private String city;
    public Address(String street, String city) {
        this.street = street;
        this.city = city;
    }

    @Override
    public String toString() {
        return "Address{street='" + street + "', city='" + city + "'}";
    }
}

class Person implements Serializable {
    private String name;
    private int age;
    private Address address;
    public Person(String name, int age, Address address) {
        this.name = name;
        this.age = age;
        this.address = address;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + ", address=" + address + '}';
    }
}

public class ObjectCloningExample {

    public static void main(String[] args) {
        // Create a Person object
        Address originalAddress = new Address("123 Main St", "Cityville");
        Person originalPerson = new Person("John Doe", 25, originalAddress);
        // Clone the object using serialisation and deserialisation
        Person clonedPerson = cloneObject(originalPerson);
        // Modify the original object
        originalPerson.setName("Jane Doe");
        originalPerson.setAge(30);
        originalAddress.setStreet("456 Oak St");
        // Display the original and cloned objects
        System.out.println("Original Person: " + originalPerson);
        System.out.println("Cloned Person: " + clonedPerson);
    }

    private static Person cloneObject(Person original) {
        try {
            // Serialize the original object
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            out.writeObject(original);
            out.flush();
            // Deserialize the object to create a deep copy
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream in = new ObjectInputStream(bis);
            return (Person) in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

The Person class contains an Address object in this example. The ObjectCloningExample class demonstrates how to clone a Person object by serialising it to a byte stream and then deserialising it to create a deep copy. The cloneObject method handles the serialisation and deserialisation process. After cloning, modifications to the original object do not affect the cloned object, demonstrating a deep copy.

Remote Method Invocation (RMI) :
#

Java Remote Method Invocation (RMI) is a mechanism that allows objects to invoke methods on objects in another JVM. Serialisation is used to marshal and unmarshal parameters and return values during remote method calls.

JavaBeans :
#

Serialisation is commonly used in JavaBeans, reusable software components for Java. JavaBeans often need to be serialised to be easily stored or transmitted.

Heads Up
#

Despite these advantages, it’s important to note that not all classes can or should be serialised. For a class to be serialisable, it must implement the Serializable interface. Additionally, care should be taken when serialising objects to ensure security and proper handling of changes to class definitions over time (versioning).

What are the security issues?
#

Java Serialization can introduce security issues, particularly when deserialising data from untrusted sources. Here are some of the security concerns associated with Java Serialization:

Remote Code Execution:
#

One of the most significant security risks with Java Serialization is the potential for remote code execution. When deserialising an object, the Java runtime system can execute arbitrary code within the serialised data. Attackers can exploit this to execute malicious code on the target system. This vulnerability can lead to serious security breaches.

Denial of Service (DoS):
#

An attacker can create a serialised object with a large size, causing excessive memory consumption and potentially leading to a denial of service attack. Deserialising large objects can consume significant CPU and memory resources, slowing down or crashing the application.

Data Tampering:
#

Serialised data can be tampered with during transmission or storage. Attackers can modify the serialised byte stream to alter the state of the deserialised object or introduce vulnerabilities.

Insecure Deserialization:
#

Deserialising untrusted data without proper validation can lead to security issues. For example, if a class that performs sensitive operations is deserialised from untrusted input, an attacker can manipulate the object’s state to perform unauthorised actions.

Information Disclosure:
#

When objects are serialised, sensitive information may be included in the serialised form. An attacker may gain access to sensitive information if this data needs to be adequately protected or encrypted.

How to Mitigate Serialization Issues
#

To mitigate these security issues, consider the following best practices:

Avoid Deserializing Untrusted Data:
#

Avoid deserialising data from untrusted sources altogether. Instead, use safer data interchange formats like JSON or XML for untrusted data.

Implement Input Validation:
#

When deserialising data, validate and sanitise the input to ensure it adheres to expected data structures and doesn’t contain unexpected or malicious data.

Use Security Managers:
#

Java’s Security Manager can be used to restrict the permissions and actions of deserialised code. However, it’s important to note that security managers have been removed in newer versions of Java.

Whitelist Classes:
#

Limit the classes that can be deserialised to a predefined set of trusted classes. This can help prevent the deserialisation of arbitrary and potentially malicious classes.

Versioning and Compatibility:
#

Be cautious when making changes to serialised classes. Use serialVersionUID to manage versioning and compatibility between different versions of serialised objects.

What is serialVersionUID in Java, and how does it work?
#

In Java, the serialVersionUID is a unique identifier associated with a serialisable class. It is used during object serialisation to verify that the sender and receiver of a serialised object have loaded classes for that object that are compatible with serialisation. If the receiver has loaded a class for the object with a different serialVersionUID than the corresponding class on the sender side, deserialisation will result in an InvalidClassException.

Here’s how it works:

Automatic Serialization:
#

When you mark a class as Serializable in Java, the serialisation mechanism automatically generates a serialVersionUID for that class unless you provide one explicitly. This autogenerated serialVersionUID is based on various aspects of the class, including its name, implemented interfaces, fields, and methods.

import java.io.Serializable;
   public class MyClass implements Serializable {
       // class code
   }
Explicit serialVersionUID:
#

You can also explicitly declare a serialVersionUID in your class to have more control over versioning. This can be useful to avoid unexpected serialisation compatibility issues when the class structure changes.

import java.io.Serializable;
   public class MyClass implements Serializable {
       private static final long serialVersionUID = 123456789L;
       // class code
   }

It’s important to note that if you don’t provide an explicit serialVersionUID, the Java runtime will automatically generate one based on the class’s structure, and changes to the class may result in a different autogenerated ID.

Versioning:
#

When an object is serialised, the serialVersionUID is also stored in the serialised form. During deserialisation, the receiving end compares the serialVersionUID of the loaded class with the one stored in the serialised data. If they match, deserialisation proceeds; otherwise, an InvalidClassException is thrown.

This mechanism helps ensure that the serialised data is compatible with the current class version. If you make changes to the class that are not backwards-compatible, you should update the serialVersionUID to indicate that the new class is incompatible with the previous version.

In summary, serialVersionUID in Java is a version control mechanism for serialised objects, and it plays a crucial role in maintaining compatibility between different versions of a class during object serialisation and deserialisation.

SerialVersionUID Example
#

Imagine you have a Person class that represents a person with a name and an age, and you want to serialise instances of this class.

import java.io.*;
public class Person implements Serializable {

    private static final long serialVersionUID = 1L;
    private String name;
    private int age;
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // getters and setters
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }

    public static void main(String[] args) {
        // Serialization
        serializePerson();
        // Deserialization
        Person deserializedPerson = deserializePerson();
        System.out.println("Deserialized Person: " + deserializedPerson);
    }

    private static void serializePerson() {
        try (ObjectOutputStream oos 
               = new ObjectOutputStream(new FileOutputStream("person.ser"))) {
            Person person = new Person("John", 25);
            oos.writeObject(person);
            System.out.println("Person serialized successfully.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static Person deserializePerson() {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.ser"))) {
            return (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

In this example:

- The Person class implements the Serializable interface.

- It has a serialVersionUID field explicitly set to 1L.

- The serializePerson method creates a Person object, serialises it, and writes it to a file called person.ser.

- The deserializePerson method reads and returns the serialised Person object from the file.

Now, let’s see how versioning works:

1. Run the program to serialise a Person object and write it to the file.

2. Change the Person class by adding a new field, for example, private boolean isStudent;.

3. rerun the program to deserialise the Person object.

If you don’t update the serialVersionUID when you add the new field, you will likely encounter an InvalidClassException during deserialisation. To avoid this, you should update the serialVersionUID to indicate that the new version of the class is not compatible with the previous one:

**private static final long serialVersionUID = 2L;**

When you rerun the program, it should deserialise the object successfully, and the isStudent field will have its default value (false). Remember that this is a simple example; in a real-world scenario, you might need to implement more sophisticated versioning strategies based on your application’s requirements.

Security Libraries:
#

Consider using third-party libraries like Apache Commons Collections or OWASP Java Serialization Security (Java-Serial-Killer) to help mitigate known vulnerabilities and prevent common attacks.

Lessons Learned
#

In summary, Java Serialization can introduce serious security risks, especially when dealing with untrusted data. It’s essential to take precautions, validate inputs, and consider alternative serialisation methods or libraries to enhance security. Additionally, keeping your Java runtime environment up to date is crucial, as newer versions of Java may include security improvements and fixes for known vulnerabilities.

Conclusion:
#

Overall, serialisation is both a powerful and dangerous tool. We must balance usage and comfort at each location against our security needs. If you look at open-source projects, you can see a lot of activity to minimise the risk and increase the abstraction of this topic for everyday use.

Related

Secure Coding Practices - Input Validation

What is - Input Validation? # Input validation is a process used to ensure that the data provided to a system or application meets specific criteria or constraints before it is accepted and processed. The primary goal of input validation is to improve the reliability and security of a system by preventing invalid or malicious data from causing errors or compromising the system’s integrity.

What is a Common Weakness Enumeration - CWE

CWE stands for Common Weakness Enumeration. It is a community-developed list of software and hardware weakness types that can serve as a common language for describing, sharing, and identifying security vulnerabilities in software systems. CWE aims to provide a standardized way of identifying and categorizing vulnerabilities, making it easier for software developers, testers, and security professionals to discuss and address security issues.

EclipseStore High-Performance-Serializer

I will introduce you to the serializer from the EclipseStore project and show you how to use it to take advantage of a new type of serialization. Since I learned Java over 20 years ago, I wanted to have a simple solution to serialize Java-Object-Graphs, but without the serialization security and performance issues Java brought us. It should be doable like the following…

Contextual Analysis in Cybersecurity

Contextual analysis in cybersecurity involves examining events, actions, or data within the broader context of an organization’s IT environment. It is a critical component of a proactive cybersecurity strategy, aiming to understand the significance of activities by considering various factors surrounding them. This multifaceted approach helps cybersecurity professionals identify and respond to potential threats effectively.