Java Stream Handling

Letzte Woche war es wieder so weit: Die Stream-Falle hat zugeschlagen.

Was ist passiert?

Der Betreiber einer Server-Anwendung beschwert sich, dass sein Anwendung nicht läuft. Das Log-File zeigt eine Menge von IOException, frei nach dem Motto: To many open files.

Bei einer Quelltextgröße von über 1000 Klassen wird es schwer das vergessene close zu finden. Also werfen wir den Yourkit an und siehe da ein kurzer Blick auf die Probs und schon haben wir unsere Kandidaten.

  • in einem Fall wurde der Stream nicht geschlossen
  • in einem Fall wurde der Stream zwar geschlossen, nur leider nicht in einem finally — dummerweise hat die Anwendung genau in diesem Fall eine Exception geworfen (Pferde suchen sich auch immer Apotheken zum …)
  • ein XMLStreamReader wurde mit einem File gefüttert — ganz übler Fall.

Stream-Spielregeln

Grundsätzlich sollten die folgenden Regeln beim Umgang mit Streams befolgt werden:

  • ein Stream wird immer von dem geschlossen, der ihn geöffnet hat
  • ein Stream wird immer in einem finally-Block geschlossen
  • ein Stream wird sofort nach dem er nicht mehr benötigt wird geschlossen
  • alle Team-Mitglieder verarbeiten Streams in der gleichen Art und Weise

Wenn diese Regeln vom Team gemeinsam befolgt werden und jeder auch mal rechts und links schaut, dann kann in Sachen Streams nicht mehr viel schief gehen.

Für häufig benutzte Stream-Operation können entsprechend Hilfsklassen erstellt werden. Für diese trivialen Methoden muss man nicht die komplette commons-io Bibliothek einbinden (die dann noch die commons-logging mit bringt). Insbesondere dann nicht, wenn man im Team beschlossen hat, Exceptions bei einem close wenigstens zu loggen.

XMLStreamReader-Problem

[code language=“java“ wraplines=“false“]
File file = …;
XMLInputFactory xmlInputFactory = XMLInputFactory.newInstance();
StreamSource source = new StreamSource(file);
try {
XMLStreamReader xmlStreamReader = xmlInputFactory.createXMLStreamReader(source);
try {
// … do some work on xmlStreamReader
} finally {
xmlStreamReader.close();
}
} catch (XMLStreamException e) {
throw new RuntimeException("Error while reading file " + file.getAbsolutePath() + " Error message: " + e.getMessage(), e);
}
[/code]

Ganz unschöne Sache. Der Programmierer sieht keine Streams, denn die StreamSource erzeugt den Stream für den Programmier, also kann man keinen Stream schließen, ganz einfache Schlussfolgerung. Und genau hier ist der Hund begraben. Intern wird ein Stream von der StreamSource geöffnet, dies geschieht im inneren der StreamSource. Leider bietet die StreamSource keine close-Methode, sodass der XMLStreamReader den geöffneten Stream nicht wieder schließen kann. Von außen kann man den Stream auf Grund der fehlenden close-Methode auch nicht schließen — ein Teufelskreis.

Ganz böse Falle! Besonders weil dieser Konstruktor sich quer durch die ganze XML-Api zieht:

  • SchemaFactory.newSchema(File) (der Stream für das Schema-File wird nie geschlossen)
  • JAXB.unmarshal(File, Class) (jede mit dieser Methode eingelesen XML-Datei wird nicht geschlossen)

Beide Methoden sind so schön einfach aufzurufen, denn man muss sich nicht um den Stream, das close und das try…catch…finally kümmern. Ein Irrglaube. Der geneigte Leser kann sich ja zum Spaß mal die close-Methode vom XMLStreamReaderImpl anschauen, er wird erstaunt sein.

Gebräuchliche Variante (bis Java7)

[code language=“java“ wraplines=“false“]
File file = …
InputStream in = null;
try {
in = new FileInputStream(file);
// … do some work on in
// … do some other work
} catch (IOException e) {
throw new RuntimeException("Error while reading file " + file.getAbsolutePath() + " Error message: " + e.getMessage(), e);
} finally {
try {
if (in != null) {
in.close();
}
} catch (IOException e) {
// ignore
}
}
[/code]

  • Im finally ist eine Null-Check nötig, weil new FileInputStream eine FileNotFoundException werfen kann.
  • In einigen Fällen wird der Stream erst am Ende einer Methode geschlossen lange, nachdem das letzte Byte gelesen wurde.
  • Der finally-Block wird in vielen Fällen durch ein IOUtils.close(in) ersetzt — das spart Platz.
  • Der Nebeneffekt bei dieser Lösung ist, durch den Catch-Block für das close werden potentielle Probleme beim Schließen des Streams nicht bemerkt. Das kann insb. bei Server-Anwendungen zu Probleme führen, wenn File-Handles z.B. nicht korrekt wieder freigegeben werden können.

Üblicher Variante (ab Java7)

[code language=“java“ wraplines=“false“]
try (InputStream in = new FileInputStream(file)) {
// … do some work on in
} catch (IOException e) {
throw new RuntimeException("Error while processing file " + file.getAbsolutePath() + " Error message: " + e.getMessage(), e);
}
[/code]

  • Ein saubere elegante Lösung mit dem neuen Java-Sprach-Feature.
  • Diese Lösung funktioniert leider nicht XMLStreamReader oder XMLEventReader

Zusammenfassung

Egal welche von den Varianten genutzt wird, Streams müssen in Java immer explizit geschlossen werden! Dabei ist es egal ob Produktions-, Prototyp- oder
Testcode geschrieben wird. Welcher Variante man sich bedient ist letztendlich zweitrangig.