Sehr leicht übersieht man nicht-serialisierbare Felder in serialisierbaren Klassen mit Folgen, die erst später auftauchen.
In einer JSF (JavaServerFaces) Webanwendung entdeckte ich einen kleinen, aber folgenreichen Fehler.

Managed Beans

JSF verwendet sogenannte Managed Beans, welche eine Art Controller und Model repräsentieren, in denen der aktuelle Zustand der Darstellung aufbewahrt werden. Insbesondere bei Formularen mit mehreren Seiten oder Tabs werden die bearbeiteten Daten als Zwischenergebnis in der Managed Bean vorgehalten. Wählt dann der Benutzer z. B. einen Speichern-Knopf, wird der aktuelle Zustand aus der Managed Bean in eine Datenbank persistiert.

import java.util.Base64.Encoder;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
 
@ManagedBean(name = "MyManagedBean")
@ViewScoped
public class MyManagedBean implements Serializable {
  private MyDto aDto;
  private List<String> infos = new ArrayList<>();
  private Encoder base64Encoder = new Encoder();
  ...

Die meisten solcher Managed Beans sind View oder Session bezogen. Dass heißt, die in den Managed Beans gespeicherten Daten müssen zwischen Seitenaufrufen oder während der ganzen Session des Benutzers auch auf dem Server zwischen gespeichert werden.
Wie viele Webframeworks verwendet JSF hierzu die Usersession, in der die entsprechenden Managed Beans serverseitig abgelegt werden.

Usersession im Container

Die Usersession ihrerseits wird üblicherweise vom Webapplication-Container verwaltet, z. B. einer Tomcat-Instanz.
Der Applikationscontainer muss viele Sessions verwalten und bei hoher Last z. B. sicherstellen, dass gerade nicht benötigte Sessions aus dem Hauptspeicher auf die Festplatte ausgelagert werden. Auch beim Neustarten des Containers versucht dieser alle Sessions auf die Festplatte zu serialisieren, um sie nach dem Neustart wieder laden zu können.
Deswegen ist es sinnvoll, wenn auch durch die API nicht vorgeschrieben, dass alle Daten, die einer Session hinzugefügt werden, auch serialisierbar sind.

An irgend einem Zeitpunkt wird also der Container versuchen, die Daten der Session, und damit auch die Managed Bean, per Serialisierung auf die Festplatte zu schreiben. Aus Sicht des Entwicklers ist dieses Verhalten eher undeterministisch, die Fehler während der Serialisierung treten eher erst nach der Entwicklungsphase auf.
Meist hat der Entwickler auf seinem lokalen Entwicklungssystem nur ein-zwei Sessions aktiv und der Container sieht vorerst keinen Bedarf, die Usersessions auf die Festplatte auszulagern. Und erst in der Produktion, wenn sehr viel mehr Benutzer auf das System zugreifen, muss der Container die Serialisierung durchführen.

Nicht serialisierbare Objekte als Fehler

Die Java Serialisierung setzt voraus, dass alle Properties einer serialisierbaren Klasse ebenfalls serialisierbar sind, also auch das Serializable-Interface erben. Ist diese Erbstruktur an einer Stelle unterbrochen, weil eine der Klassen nicht serialisierbar ist, gibt es eine Exception und die Serialisierung schlägt fehlt.

Im obigen Beispiel sieht dies wie folgt aus:
Die Klasse MyDto ist Serializable. DTOs (Data Transfer Objects) und Entities sollten stets Serializable sein, da insbesondere DTOs über verschiedene Übertragungswege (als XML, JSON, Binärprotokolle etc.) transportiert werden können.

Die Collections aus der JDK, wie die ArrayList, sind ebenfalls Serializable.

Der Import java.util.Base64.Encoder ist nicht Serializable.

Also wird irgend wann, wahrscheinlich auf einem System mit mehr Last, zum Zeitpunkt der Serialisierung der Usersession durch den Container eine Exception auftreten.

Behandlung von nicht serialisierbaren Objekten

Der Grund, warum der Entwickler den Encoder in der Managed Bean als Instanzvariable definierte, war es sicherlich zu vermeiden, bei jedem Gebrauch eine neue Instanz erzeugen zu müssen.
Das ist auch legitim, bedarf aber bei Serializable Klassen einer besonderen Behandlung.

Als erstes muss sichergestellt werden, dass die Variable beim Serialisieren ignoriert wird. Dies wird über das Java-Schlüsselwort transient geregelt.

import java.util.Base64.Encoder;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;
 
@ManagedBean(name = "MyManagedBean")
@ViewScoped
public class MyManagedBean implements Serializable {
  private MyDto aDto;
  private List<String> infos = new ArrayList<>();
  private transient Encoder base64Encoder;
  ...

Nun wird beim Serialisieren und Deserialisieren die Variable übersprungen, es gibt keine Exception mehr.
Dafür müssen wir aber sicherstellen, dass das Objekt bei bedarf auch wirklich vorhanden ist. Denn nach einem Deserialisieren ist der Wert einer transienten Variable null. Das macht man dann am besten, in dem man eine prüfende verzögerte Initialisierungsmethode für den Zugriff auf die nicht serialisierbare Instanz erstellt.

public void myMethod() {
  Encoder encoder = getEncoder();
  // Do some fancy stuff
  ...
}
 
public Encoder getEncoder() {
  if (encoder == null) {
    encoder = new Encoder();
  }
  return encoder;
}

Diese Art Fehler kommt übrigens auch häufig im Zusammenhang mit Servlets vor, deren Session ebenfalls vom Container verwaltet wird und daher serialisierbar sein muss.

Referenzen