NASA Rakete

Stellt euch vor, ihr seid für die Steuerungssoftware der Rakete für eine bemannte Mondmission zuständig und verantwortlich. Kein Problem! Was soll da schon schief gehen? Schließlich habt ihr ja ein Team aus hochqualifizierten Softwareentwicklern, die perfekten und fehlerfreien Code schreiben. Fehlerbehandlung wird also gar nicht benötigt. Oder?

Nun, vielleicht läuft euer Programm auch zuverlässig und absolut fehlerfrei. Doch was passiert, wenn zum Beispiel ausversehen ein anderer Programmzweig gestartet wird und den grade laufenden Prozess blockiert oder sogar beendet? Das könnte ganz schön ungemütlich werden, wenn sich das Astronautenteam grade im Landeanflug in mehreren tausend Metern Höhe befindet. Hört sich erstmal unwahrscheinlich an, ist allerdings genau so bei der Apollo-8 Mission passiert.

Fehlerbehandlung kann Leben retten

Margaret Hamilton, eine Softwarepionierin, war die Chefprogrammiererin der Apollo-11 Mission. Als sie damals damit beschäftigt war, die besagte Steuerungssoftware zu entwickeln, brachte sie oft ihre kleine Tochter mit zur Arbeit. Bei einem Testlauf der Mission, kam das Mädchen auf eine Taste, die eigentlich für die Startvorbereitung gedacht war. Das System stürzte ab und Margaret erkannte, dass sie hier vorbeugen muss indem sie eine Fehlerbehandlung einbauen muss.
Okay, vielleicht muss unsere Software nicht direkt eine bemannte Mondmission steuern. Trotzdem sind Fehler lästig, nervig und führen zu unzufriedenen Kunden. Wie ihr eine saubere Fehlerbehandlung in eurer Java-Applikation einrichtet, möchte ich euch im Folgenden ein wenig näherbringen.

Was sind Exceptions?

Exceptions sind ungewollte und unerwartete Events, welche zum Beispiel zur Laufzeit eines Programms auftreten und den normalen Ablauf des Programms unterbrechen. Im Java-Umfeld wird zwischen den Checked und Unchecked Exceptions unterschieden. Der Unterschied besteht darin, dass der Java-Compiler für die Checked Exceptions überprüft ob sie behandelt werden und die Unchecked Exceptions ignoriert. Unchecked kann zum Beispiel eine NullPointerException oder eine ArrayIndexOutOfBoundsException sein. In der Regel treten diese Exceptions durch Programmierfehler auf.
Checked Exceptions müssen wie gesagt behandelt werden. Das bedeutet, dass entweder das throws-Keyword an der Methodensignatur stehen muss, in der die Exception auftreten kann oder die Exception mittels try-catch Block abgefangen werden muss. Schauen wir ins zunächst das try-catch an.

Try-Catch

Beim Try-Catch wird die auftretende Exception sofort abgefangen. Da der Compiler uns ja dazu zwingt, kann dies auch nicht ausversehen vergessen werden. Der Teil, der die Exception werfen kann muss dabei im try-Block stehen. Direkt daran angeschlossen befindet sich der catch-Block. Hier wird implementiert, wie sich das Programm verhalten soll wenn uns die Exception um die Ohren schallert. Zum Veranschaulichen schauen wir uns ein sehr häufigen Use-Case an.

public String ladeDateiHerunter(final URL url)
{
	try
	{
		InputStream inputStream = new BufferedInputStream(url.openConnection().getInputStream());
		return CharStreams.toString(new InputStreamReader(inputStream, "Encoding"));
	}
	catch (final IOException e)
	{
		//TODO: Fehlerbehandlung
	}
	return "";
}

Wir wollen eine Datei runterladen und den Inhalt in Form eines Strings zurückgeben. Die URL wird vom Aufrufer übergeben. Im try-Block sorgt unteranderem die Methode getInputStream() dafür, dass eine IOException fliegen kann. IO steht für Input/Output. Sie definiert in unserem Fall also einen Fehler der beim Input des Dateiinhaltes über die Connection auftreten kann.
Wir haben also den Teil des Codes, in dem potenziell die Exception fliegt, mit einem try-Block ummantelt und fangen diese anschließend direkt im catch-Block auf. Bevor wir uns jetzt der Fehlerbehandlung widmen, gibt es noch eine kleine aber sehr wichtige Verbesserung des Codes.

Try-with-resources

Direkt zu Beginn des try-Blocks deklarieren und initialisieren wir den InputStream. Ein InputStream muss allerdings immer nach Abarbeitung geschlossen werden. Aber was passiert, wenn beim Öffnen der Connection oder beim Aufruf der Methode getInputStream() ein Fehler auftritt? Richtig, unser Programm springt in den catch-Block und beginnt mit der Fehlerbehandlung. Selbst wenn es keinen Fehler gibt wird unser Stream aktuell noch nicht geschlossen. Das ist nicht nur unschön, dass ist sogar ein richtig fetter Bug.
Bestimmt denkst du jetzt, dass wir uns doch einfach im catch darum kümmern können, den Stream zu schließen. Keine schlechte Idee! Aber leider auch nicht wirklich zielführend. Wenn uns nämlich eine andere Exception um die Ohren fliegt, für die sich der catch-Block gar nicht interessiert, dann wird auch dann der Stream nicht geschlossen. Und genau jetzt kommt das try-with-resources ins Spiel.

public String ladeDateiHerunter(final URL url)
{
	try(InputStream inputStream = new BufferedInputStream(url.openConnection().getInputStream()))
	{
		return CharStreams.toString(new InputStreamReader(inputStream, "Encoding"));
	}
	catch (final IOException e)
	{
		//TODO: Fehlerbehandlung
	}
	return "";
}

Im Code sehen wir, dass der InputStream jetzt hinter dem try(...) in runden Klammern steht. Dadurch wird im Falle einer Exception der Stream noch zuverlässig geschlossen.

Die Fehlerbehandlung

Als nächstes schauen wir uns den catch-Block an. Hier wird der Fehler wie gesagt aufgefangen und behandelt. Wie genau mit einem Fehler umgegangen wird bzw. was das Programm zur Behandlung machen soll, entscheidet man je nach Fehler und Programm individuell. Eine Möglichkeit in unserem Beispiel wäre ein erneuter Anlauf des Downloads nach einer kurzen Zeit. So würden wir schonmal auf ein temporäres Verbindungsproblem reagieren.
Ein catch-Block kann übrigens mehrere Exceptions von verschiedenen Typen oder auch einfach alle Exceptions fangen. Dafür einfach die einzelnen Exceptions mit einer Pipe aneinanderreihen oder die Klasse Exception in die Klammern des catches schreiben.
Es gibt allerdings noch zwei Todos auf die ich an dieser Stelle unbedingt eingehen möchte.

Logging

Während wir an unserer Software noch entwickeln greifen wohl einige Entwickler auf System.out.println(); zurück um ein Problem oder ein Fehler im Code ausfindig zu machen. Aber was machen wir auf einer produktiven Umgebung wo es gar keine Konsolenausgabe gibt? Wir möchten schließlich bei Fehlverhalten wissen was los ist und den Fehler natürlich auch beheben. Wir brauchen also Logging um Abstürze oder Fehler aufzunehmen und später auszuwerten.
Schauen wir uns das Framwork Log4j an. Es gibt verschiedene Log-Level anhand denen wir in den Properties festlegen können, was alles geloggt werden soll. In ihrer Priorität absteigend geordnet gibt es folgende:
1. Debug
2. Info
3. Warn
4. Error
5. Fatal

In unserem Beispiel könnten wir das Logging folgendermaßen einsetzen:

try(InputStream inputStream = new BufferedInputStream(url.openConnection().getInputStream()))
{
	String dateiInhalt = CharStreams.toString(new InputStreamReader(inputStream, "Encoding"));
	log.info("Dateidownload von " + url.toString() + "erfolgreich.");
	return dateiInhalt;
}

Ist der Download erfolgreich gewesen, erzeugen wir ein Info-Log, der einen kurzen Text und die URL enthält.

if (anzahlDownloadVersuche > 0)
{
	log.warn("Download nicht erfolgreich. Neuer Versuch wird gestartet.", e);
	Thread.sleep(Timeout);
	return ladeDateiHerunter(url);
}

Ist der Download fehlgeschlagen und ein neuer Versuch soll gestartet werden, erzeugen wir uns ein Warn-Log und geben die Exception mit.

else
{
	final IOException e1 = new IOException(
	String.format("Der Inhalt auf %s konnte nicht runtergeladen werden!", url.toString()));
	log.error(e1.getMessage(), e);
	throw e1;
}

Ist die Anzahl unserer Versuche die wir starten wollen abgelaufen, dann gibt es ein Error-Log mit einer Nachricht der Exception die wir uns zuvor erstellt haben und geben auch hier wieder die ganze Exception mit. Anschließend wird die Exception noch zum Aufrufer weitergeworfen.

System.exit()

Wenn ein Fehler dazu führt, dass das Programm nicht weiter ausgeführt werden kann und beendet wird, ist es wichtig, dass wir dem Aufrufer mitteilen, dass es zu einem Fehler gekommen ist. Wenn ein Programm erfolgreich ausgeführt und beendet wird, dann steht im Exitcode die Ziffer 0. Fangen wir jetzt eine Exception die nicht weiter behandelt werden kann, müssen wir das Programm beenden und mit einem Exitcode != 0 rausgehen.

catch (final IOException e)
{
	log.error("Das Programm wird abgebrochen", e);
	System.exit(1);
}

Das Re-throw

Es gibt Fälle, in denen können wir nicht direkt auf die Exception reagieren bzw. nicht entscheiden wie gehandelt werden soll. In so einem Fall können wir die Exception einfach zum Aufrufer werfen. Dieser kann dann die Exception behandeln oder sogar ebenfalls weiterwerfen. Spätestens in der Mainmethode sollten die Exceptions aber gecatcht werden.

public String ladeDateiHerunter(final URL url) throws IOException
{
	InputStream inputStream = new BufferedInputStream(url.openConnection().getInputStream());
	String dateiInhalt = CharStreams.toString(new InputStreamReader(inputStream, "Encoding"));
	log.info("Dateidownload von " + url.toString() + "erfolgreich.");
	return dateiInhalt;
}

Fazit

Fehlerbehandlung ist wichtig und kann uns eine menge Zeit beim Debugging ersparen. Fehler sollten immer ordentlich weggeloggt werden und das Programm mit einem Fehlercode sauber beendet werden. Nutzen wir Resourcen, greifen wir auf ein try-with-resources zurück. Je nach Situation behandeln wir die Exceptions direkt beim Auftreten im catch-Block oder werfen sie durch zum Aufrufer.
In meinem Fachartikel konnte ich hoffentlich einen guten Einstieg in die Fehlerbehandlung in Java geben. Falls du Fragen oder Anmerkungen hast, lass mir gerne ein Kommentar da oder schreib mir eine E-Mail 😊

Du kannst auch gerne mal bei den anderen Fachartikeln vorbeischauen!

Fehlerbehandlung in Java
Markiert in:                     

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.