Dieser Artikel ist eine Fortsetzung meines vorherigen Artikels zur Fehlerbehandlung in drei Schritten.
In diesem Artikel möchte ich einige schlechte Praktiken bei der Fehlerbehandlung und vielleicht ein paar schnelle Lösungen zeigen, falls Sie stolpern
über sie in Ihrer Codebasis.
Haftungsausschluss, dieser Blog-Beitrag ist nur eine persönliche Meinung und Sie müssen die Empfehlungen an Ihren spezifischen Anwendungsfall anpassen.
Ich stolpere regelmäßig über Codebasen, in denen Ausnahmen stillschweigend ignoriert werden. Vielleicht hatte der Entwickler es eilig oder wollte die Fehlerfälle später bearbeiten? Auf jeden Fall ist dies eine schlechte Vorgehensweise, denn wenn der Fehler auftritt:
/** * Reads some data from another service or repo and returns it. * The repo might throw an exception, but we don't care. */ public Mono<String> getHello() { try { return helloRepository.getHello() .map("Hello %s"::formatted); } catch (Exception e) { // do nothing return Mono.just("Hello World"); } }
In diesem Beispiel fragen wir uns vielleicht, warum sich einige Benutzer darüber beschweren, dass sie nicht die richtige Hallo-Nachricht erhalten, aber wir sehen keine Fehler, wir denken vielleicht, dass die Benutzer nur verwirrt sind.
Behandeln Sie den Fehler dort, wo er auftritt, z. B. wenn möglich bereits im Repository.
Wenn Sie das Problem hier beheben müssen, protokollieren Sie zumindest den Fehler und geben Sie eine ordnungsgemäße Fehlermeldung zurück.
Minimales Refactoring
public Mono<String> getHello() { try { return helloRepository.getHello() .map("Hello %s"::formatted); } catch (Exception e) { log.error("Error reading hello data from repository - {}", e.getMessage(), e); return Mono.error(new RuntimeException("Internal error reading hello data %s".formatted(e.getMessage()), e)); } }
HINWEIS:
Dadurch könnte der Upstream-Code beschädigt werden, da Sie jetzt einen Fehler anstelle eines Standardwerts zurückgeben.
Besser wäre es, den Fehler innerhalb der reaktiven Stream-API zu behandeln
public Mono<String> getHelloHandling() { return helloRepository.getHello() .map("Hello %s"::formatted) // wrap the exception in a RuntimeException with a meaningful message .onErrorMap(e -> new RuntimeException("Internal error reading hello data from HelloRepository` %s". formatted(e.getMessage()), e)); }
Das Muster aus Beispiel eins erscheint auch in reaktiven Streams:
public Mono<String> getHello() { return helloRepository.getHello() .map("Hello %s"::formatted) // do nothing on error, just return a default value .onErrorReturn("Hello world"); }
Das sieht sehr schön und sauber aus, oder? Aber wir werden nicht erkennen können, dass der Fehler im Repository ausgegeben wird!
Wenn es einen Standardwert gibt, sollte zumindest ein Fehlerprotokoll geschrieben werden.
Wie im vorherigen Beispiel verpacken wir die Ausnahme in eine andere Ausnahme, dieses Mal in eine benutzerdefinierte Ausnahme, die es sogar einfacher macht, den spezifischen Ort zu erkennen, an dem diese Ausnahme ausgelöst wird
public Mono<String> getHello2() { return helloRepository.getHello() .map("Hello %s"::formatted) .onErrorMap( e -> new CustomException("Error reading hello-data from repository - %s".formatted(e.getMessage()), e)); }
Das genaue Gegenteil des stillschweigenden Löschens von Ausnahmen besteht darin, dieselbe Ausnahme mehrmals zu protokollieren. Dies ist eine schlechte Vorgehensweise, da dadurch die Protokolle mit Hunderten oder Tausenden Zeilen von Stacktraces verwechselt werden, ohne dass eine zusätzliche Bedeutung entsteht.
In meinem schlimmsten Beispiel habe ich den gleichen Stacktrace fünfmal in den Protokollen gefunden, ohne überhaupt eine sinnvolle Nachricht.
Der Controller:
@RestController @AllArgsConstructor @Slf4j public class HelloController { private final HelloService helloService; @GetMapping("/hello") public Mono<ResponseEntity<String>> hello() { return helloService.getHello() .map(ResponseEntity::ok) .defaultIfEmpty(ResponseEntity.notFound().build()) .onErrorResume(e -> { log.error("Error:", e); return Mono.error(e); }); } }
Und im Service:
@Service @AllArgsConstructor @Slf4j public class HelloService { private final HelloRepository helloRepository; /** * Reads some data from another service or repo and returns it. */ public Mono<String> getHello() { return helloRepository.getHello() .map("Hello %s"::formatted) .onErrorResume(e -> { log.error("Error:", e); return Mono.error(e); }); } }
... und wahrscheinlich noch an einigen weiteren Orten...
Dies ist in meinem vorherigen Artikel 3-Schritte-Fehlerbehandlung erläutert, daher werde ich hier nicht noch einmal den Code zeigen, sondern eher eine Empfehlung:
Das Abfangen allgemeiner Ausnahmen wie Exception oder Throwable kann zu unbeabsichtigtem Verhalten führen und das Debuggen erheblich erschweren. Es ist besser, bestimmte Ausnahmen abzufangen.
public Mono<String> getHello() { try { return helloRepository.getHello(); } catch (Exception e) { log.error("Error while fetching hello data", e); return Mono.empty(); } }
Bestimmte Ausnahmen abfangen, um verschiedene Fehlerszenarien angemessen zu behandeln.
public Mono<String> getHello() { try { return helloRepository.getHello(); } catch (SQLException e) { return Mono.error(new HelloDataException("Database error while getting hello-data - %s".formatted(e.getMessage()), e)); } catch (IOException e) { // maybe perform a retry? return Mono.error(new HelloDataException("IO error while getting hello-data - %s".formatted(e.getMessage()), e)); } }
und das Äquivalent unter Verwendung der reaktiven Stream-API
public Mono<String> getHello() { return helloRepository.getHello() .onErrorMap(SQLException.class, e -> new HelloDataException("Database error while getting hello-data - %s".formatted(e.getMessage()), e)) .onErrorMap(IOException.class, e -> new HelloDataException("IO error while getting hello-data - %s".formatted(e.getMessage()), e)); }
Meiner Meinung nach sind überprüfte Ausnahmen ein Fehler in Java, sie sind nicht sehr nützlich und führen oft zu schlechten Praktiken und überladenem Code.
public void someMethod() throws IOException, SQLException { // some code that might throw an exception }
Bei aktivierten Ausnahmen müssen Sie diese im aufrufenden Code behandeln, was die Verwendung anderer Muster, z. B. funktionale Programmierung oder reaktive Streams oder im Frühjahr der globale Fehlerhandler.
Ausnahme:
Geprüfte Ausnahmen sind nützlich, z.B. im Bibliothekscode, wo Sie den Benutzer zwingen möchten, die Ausnahme zu behandeln.
Verwenden Sie ungeprüfte Ausnahmen für Szenarien, in denen vernünftigerweise nicht erwartet werden kann, dass sich der Anrufer erholt.
public void someMethod() { try { // some code that might throw an exception } catch (IOException | SQLException e) { throw new RuntimeException("An error occurred - %s".formatted(e.getMessage()), e); } }
Using exceptions for control flow makes the code hard to understand and can lead to performance issues.
try { int value = Integer.parseInt("abc"); } catch (NumberFormatException e) { // handle the case where the string is not a number }
Use a regular flow control mechanism like an if-statement.
String value = "abc"; if (value.matches("\\d+")) { int number = Integer.parseInt(value); } else { // handle the case where the string is not a number }
In this article I showed some bad practices in error handling and how to mitigate them.
I hope you found this article helpful and maybe you can use some of the recommendations in your codebase and in your next refactoring.
Das obige ist der detaillierte Inhalt vonSchlechte Praktiken bei der Fehlerbehandlung. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!