Beachten Sie die Gefahren generischer Ausnahmen

Während ich an einem kürzlich durchgeführten Projekt arbeitete, fand ich einen Code, der eine Ressourcenbereinigung durchführte. Da es viele verschiedene Anrufe gab, konnte es möglicherweise sechs verschiedene Ausnahmen auslösen. Der ursprüngliche Programmierer erklärte in einem Versuch, den Code zu vereinfachen (oder nur die Eingabe zu speichern), dass die Methode Exceptionanstelle der sechs verschiedenen Ausnahmen, die ausgelöst werden könnten, ausgelöst wird. Dies zwang den aufrufenden Code, in einen abgefangenen Try / Catch-Block eingeschlossen zu werden Exception. Der Programmierer entschied, dass die Fehlerfälle nicht wichtig waren, da der Code zu Bereinigungszwecken diente, sodass der Catch-Block leer blieb, da das System trotzdem heruntergefahren wurde.

Offensichtlich sind dies nicht die besten Programmierpraktiken, aber nichts scheint schrecklich falsch zu sein ... außer einem kleinen logischen Problem in der dritten Zeile des Originalcodes:

Listing 1. Ursprünglicher Bereinigungscode

private void cleanupConnections () löst ExceptionOne, ExceptionTwo {for (int i = 0; i <connection.length; i ++) {connection [i] .release () aus; // Wirft ExceptionOne, ExceptionTwo connection [i] = null; } Verbindungen = null; } protected abstract void cleanupFiles () löst ExceptionThree, ExceptionFour aus; protected abstract void removeListeners () löst ExceptionFive, ExceptionSix aus; public void cleanupEverything () löst eine Ausnahme aus {cleanupConnections (); cleanupFiles (); removeListeners (); } public void done () {try {doStuff (); AufräumenAlles (); doMoreStuff (); } catch (Ausnahme e) {}}

In einem anderen Teil des Codes wird das connectionsArray erst initialisiert, wenn die erste Verbindung hergestellt wurde. Wenn jedoch nie eine Verbindung hergestellt wird, ist das Verbindungsarray null. In einigen Fällen führt der Aufruf zu connections[i].release()a NullPointerException. Dies ist ein relativ einfach zu behebendes Problem. Fügen Sie einfach einen Scheck für hinzu connections != null.

Die Ausnahme wird jedoch nie gemeldet. Es wird von cleanupConnections()geworfen, wieder von geworfen cleanupEverything()und schließlich gefangen done(). Die done()Methode macht mit der Ausnahme nichts, sie protokolliert sie nicht einmal. Und weil cleanupEverything()nur durchgerufen wird done(), wird die Ausnahme nie gesehen. Der Code wird also nie repariert.

Daher werden im Fehlerszenario die Methoden cleanupFiles()und removeListeners()niemals aufgerufen (sodass ihre Ressourcen niemals freigegeben werden) und doMoreStuff()niemals aufgerufen. Daher wird die endgültige Verarbeitung in done()nie abgeschlossen. Um die Sache noch schlimmer zu machen, done()wird nicht aufgerufen, wenn das System heruntergefahren wird. Stattdessen wird es aufgerufen, um jede Transaktion abzuschließen. Bei jeder Transaktion gehen also Ressourcen verloren.

Dieses Problem ist eindeutig ein großes: Fehler werden nicht gemeldet und Ressourcen lecken. Aber der Code selbst scheint ziemlich unschuldig zu sein, und nach der Art und Weise, wie der Code geschrieben wurde, ist dieses Problem schwer nachzuvollziehen. Durch Anwendung einiger einfacher Richtlinien kann das Problem jedoch gefunden und behoben werden:

  • Ausnahmen nicht ignorieren
  • Fangen Sie keine generischen Exceptions
  • Werfen Sie keine generischen Exceptions

Ausnahmen nicht ignorieren

Das offensichtlichste Problem mit dem Code von Listing 1 ist, dass ein Fehler im Programm vollständig ignoriert wird. Eine unerwartete Ausnahme (Ausnahmen sind naturgemäß unerwartet) wird ausgelöst, und der Code ist nicht darauf vorbereitet, diese Ausnahme zu behandeln. Die Ausnahme wird nicht einmal gemeldet, da der Code davon ausgeht, dass die erwarteten Ausnahmen keine Konsequenzen haben.

In den meisten Fällen sollte zumindest eine Ausnahme protokolliert werden. Mehrere Protokollierungspakete (siehe Seitenleiste "Protokollierungsausnahmen") können Systemfehler und Ausnahmen protokollieren, ohne die Systemleistung wesentlich zu beeinträchtigen. Die meisten Protokollierungssysteme ermöglichen auch das Drucken von Stapelspuren und liefern so wertvolle Informationen darüber, wo und warum die Ausnahme aufgetreten ist. Da die Protokolle normalerweise in Dateien geschrieben werden, kann eine Aufzeichnung von Ausnahmen überprüft und analysiert werden. In Listing 11 in der Seitenleiste finden Sie ein Beispiel für die Protokollierung von Stack-Traces.

Das Protokollieren von Ausnahmen ist in bestimmten Situationen nicht kritisch. Eine davon ist das Reinigen von Ressourcen in einer finally-Klausel.

Ausnahmen in endlich

In Listing 2 werden einige Daten aus einer Datei gelesen. Die Datei muss geschlossen werden, unabhängig davon, ob eine Ausnahme die Daten liest. Daher wird die close()Methode in eine finally-Klausel eingeschlossen. Wenn jedoch ein Fehler die Datei schließt, kann nicht viel dagegen unternommen werden:

Listing 2:

public void loadFile (String fileName) löst eine IOException aus {InputStream in = null; try {in = new FileInputStream (Dateiname); readSomeData (in); } endlich {if (in! = null) {try {in.close (); } catch (IOException ioe) {// ignoriert}}}}

Beachten Sie, dass loadFile()weiterhin eine IOExceptionan die aufrufende Methode gemeldet wird, wenn das Laden der Daten aufgrund eines E / A-Problems (Eingabe / Ausgabe) fehlschlägt. Beachten Sie auch, dass close()der Code , obwohl eine Ausnahme von ignoriert wird, dies explizit in einem Kommentar angibt, um es jedem klar zu machen, der an dem Code arbeitet. Sie können dasselbe Verfahren anwenden, um alle E / A-Streams zu bereinigen, Sockets und JDBC-Verbindungen zu schließen usw.

Das Ignorieren von Ausnahmen ist wichtig, um sicherzustellen, dass nur eine einzige Methode in den ignorierenden Try / Catch-Block eingeschlossen ist (andere Methoden im umschließenden Block werden weiterhin aufgerufen) und dass eine bestimmte Ausnahme abgefangen wird. Dieser besondere Umstand unterscheidet sich deutlich vom Fang eines Generikums Exception. In allen anderen Fällen sollte die Ausnahme (zumindest) protokolliert werden, vorzugsweise mit einem Stack-Trace.

Fang keine generischen Ausnahmen

In komplexer Software führt ein bestimmter Codeblock häufig Methoden aus, die eine Vielzahl von Ausnahmen auslösen. Dynamisch Laden eine Klasse und verschiedene Ausnahmen, darunter werfen kann ein Objekt instanziieren ClassNotFoundException, InstantiationException, IllegalAccessException, und ClassCastException.

Anstatt die vier verschiedenen catch-Blöcke zum try-Block hinzuzufügen, kann ein beschäftigter Programmierer die Methodenaufrufe einfach in einen try / catch-Block einschließen, der generische Exceptions abfängt (siehe Listing 3 unten). Dies scheint zwar harmlos zu sein, es können jedoch einige unbeabsichtigte Nebenwirkungen auftreten. Wenn beispielsweise className()null ist, Class.forName()wird ein a ausgelöst NullPointerException, das in der Methode abgefangen wird.

In diesem Fall fängt der catch-Block Ausnahmen ab, die er nie abfangen wollte, da a NullPointerExceptioneine Unterklasse von ist RuntimeException, die wiederum eine Unterklasse von ist Exception. So ist die generischen catch (Exception e)Fänge alle Subklassen von RuntimeException, einschließlich NullPointerException, IndexOutOfBoundsExceptionund ArrayStoreException. Normalerweise beabsichtigt ein Programmierer nicht, diese Ausnahmen abzufangen.

In Listing 3 null classNameergibt sich a NullPointerException, was der aufrufenden Methode anzeigt, dass der Klassenname ungültig ist:

Listing 3:

public SomeInterface buildInstance (String className) {SomeInterface impl = null; try {Class clazz = Class.forName (className); impl = (SomeInterface) clazz.newInstance (); } catch (Ausnahme e) {log.error ("Fehler beim Erstellen der Klasse:" + className); } return impl; }}

Eine weitere Konsequenz der generischen catch-Klausel ist, dass die Protokollierung begrenzt ist, da catchdie spezifische Ausnahme, die abgefangen wird, nicht bekannt ist. Einige Programmierer greifen bei diesem Problem auf das Hinzufügen einer Prüfung zum Ausnahmetyp zurück (siehe Listing 4), was dem Zweck der Verwendung von Catch-Blöcken widerspricht:

Listing 4:

catch (Ausnahme e) {if (e Instanz von ClassNotFoundException) {log.error ("Ungültiger Klassenname:" + className + "," + e.toString ()); } else {log.error ("Klasse kann nicht erstellt werden:" + className + "," + e.toString ()); }}

Listing 5 provides a complete example of catching specific exceptions a programmer might be interested in. The instanceof operator is not required because the specific exceptions are caught. Each of the checked exceptions (ClassNotFoundException, InstantiationException, IllegalAccessException) is caught and dealt with. The special case that would produce a ClassCastException (the class loads properly, but does not implement the SomeInterface interface) is also verified by checking for that exception:

Listing 5

public SomeInterface buildInstance(String className) { SomeInterface impl = null; try { Class clazz = Class.forName(className); impl = (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); } catch (InstantiationException e) { log.error("Cannot create class: " + className + ", " + e.toString()); } catch (IllegalAccessException e) { log.error("Cannot create class: " + className + ", " + e.toString()); } catch (ClassCastException e) { log.error("Invalid class type, " + className + " does not implement " + SomeInterface.class.getName()); } return impl; } 

In some cases, it is preferable to rethrow a known exception (or perhaps create a new exception) than try to deal with it in the method. This allows the calling method to handle the error condition by putting the exception into a known context.

Listing 6 below provides an alternate version of the buildInterface() method, which throws a ClassNotFoundException if a problem occurs while loading and instantiating the class. In this example, the calling method is assured to receive either a properly instantiated object or an exception. Thus, the calling method does not need to check if the returned object is null.

Note that this example uses the Java 1.4 method of creating a new exception wrapped around another exception to preserve the original stack trace information. Otherwise, the stack trace would indicate the method buildInstance() as the method where the exception originated, instead of the underlying exception thrown by newInstance():

Listing 6

public SomeInterface buildInstance(String className) throws ClassNotFoundException { try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.error("Invalid class name: " + className + ", " + e.toString()); throw e; } catch (InstantiationException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (IllegalAccessException e) { throw new ClassNotFoundException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new ClassNotFoundException(className + " does not implement " + SomeInterface.class.getName(), e); } } 

In some cases, the code may be able to recover from certain error conditions. In these cases, catching specific exceptions is important so the code can figure out whether a condition is recoverable. Look at the class instantiation example in Listing 6 with this in mind.

In Listing 7, the code returns a default object for an invalid className, but throws an exception for illegal operations, like an invalid cast or a security violation.

Note:IllegalClassException is a domain exception class mentioned here for demonstration purposes.

Listing 7

public SomeInterface buildInstance(String className) throws IllegalClassException { SomeInterface impl = null; try { Class clazz = Class.forName(className); return (SomeInterface)clazz.newInstance(); } catch (ClassNotFoundException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (InstantiationException e) { log.warn("Invalid class name: " + className + ", using default"); } catch (IllegalAccessException e) { throw new IllegalClassException("Cannot create class: " + className, e); } catch (ClassCastException e) { throw new IllegalClassException(className + " does not implement " + SomeInterface.class.getName(), e); } if (impl == null) { impl = new DefaultImplemantation(); } return impl; } 

When generic Exceptions should be caught

Certain cases justify when it is handy, and required, to catch generic Exceptions. These cases are very specific, but important to large, failure-tolerant systems. In Listing 8, requests are read from a queue of requests and processed in order. But if any exceptions occur while the request is being processed (either a BadRequestException or any subclass of RuntimeException, including NullPointerException), then that exception will be caught outside the processing while loop. So any error causes the processing loop to stop, and any remaining requests will not be processed. That represents a poor way of handling an error during request processing:

Listing 8

public void processAllRequests () {Request req = null; try {while (true) {req = getNextRequest (); if (req! = null) {processRequest (req); // löst BadRequestException aus} else {// Anforderungswarteschlange ist leer, muss erledigt werden break; }}} catch (BadRequestException e) {log.error ("Ungültige Anfrage:" + req, e); }}