Cet article fait suite à mon précédent article sur la gestion des erreurs en 3 étapes.
Dans cet article, je souhaite montrer quelques mauvaises pratiques en matière de gestion des erreurs et peut-être quelques solutions rapides, si vous trébuchez
à travers eux dans votre base de code.
Avertissement, cet article de blog n'est qu'une opinion personnelle et vous devrez ajuster les recommandations à votre cas d'utilisation spécifique.
Je tombe régulièrement sur des bases de code où les exceptions sont silencieusement ignorées. Peut-être que le développeur était pressé ou voulait gérer les cas d'erreur plus tard ? Quoi qu'il en soit, c'est une mauvaise pratique car si l'erreur se produit :
/** * 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"); } }
Dans cet exemple, nous pourrions nous demander pourquoi certains utilisateurs se plaignent de ne pas recevoir le bon message de bonjour, mais nous ne voyons aucune erreur, nous pourrions penser que les utilisateurs sont simplement confus.
Gérez l'erreur là où elle se produit, par ex. déjà dans le référentiel si possible.
Si vous devez le gérer ici, enregistrez au moins l'erreur et renvoyez un message d'erreur approprié.
Refactoring minimal
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)); } }
REMARQUE :
Cela pourrait casser le code en amont puisque vous renvoyez maintenant une erreur au lieu d'une valeur par défaut.
Il serait préférable de gérer l'erreur dans l'API du flux réactif
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)); }
Le modèle de l'exemple 1 apparaît également dans les flux réactifs :
public Mono<String> getHello() { return helloRepository.getHello() .map("Hello %s"::formatted) // do nothing on error, just return a default value .onErrorReturn("Hello world"); }
Ça a l'air très joli et propre, non ? Mais nous ne pourrons pas détecter que l'erreur est levée dans le référentiel !
S'il existe une valeur par défaut, au moins un journal des erreurs doit être rédigé.
Comme dans l'exemple précédent, nous encapsulons l'exception dans une autre exception, cette fois dans une exception personnalisée qui facilite même la détection de l'endroit spécifique où cette exception est levée
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)); }
L'opposé de la suppression silencieuse des exceptions consiste à enregistrer la même exception plusieurs fois. C'est une mauvaise pratique car elle confond les journaux avec des centaines ou des milliers de lignes de stacktraces sans apporter de signification supplémentaire.
Dans mon pire exemple, j'ai trouvé la même trace de pile cinq fois dans les journaux sans aucun message significatif.
Le contrôleur :
@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); }); } }
Et dans le 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); }); } }
... et probablement dans d'autres endroits...
Ceci est expliqué dans mon article précédent Gestion des erreurs en 3 étapes, je ne montrerai donc pas le code ici, mais plutôt une recommandation :
La détection d'exceptions génériques telles que Exception ou Throwable peut entraîner un comportement involontaire et rendre le débogage assez difficile. Il est préférable d'attraper des exceptions spécifiques.
public Mono<String> getHello() { try { return helloRepository.getHello(); } catch (Exception e) { log.error("Error while fetching hello data", e); return Mono.empty(); } }
Détectez des exceptions spécifiques pour gérer différents scénarios d'erreur de manière appropriée.
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)); } }
et l'équivalent en utilisant l'API de flux réactif
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)); }
À mon avis, les exceptions vérifiées sont une erreur en Java, elles ne sont pas très utiles et conduisent souvent à de mauvaises pratiques et à un code encombré.
public void someMethod() throws IOException, SQLException { // some code that might throw an exception }
Avec les exceptions vérifiées, vous devez les gérer dans le code appelant, ce qui rend plus difficile l'utilisation d'autres modèles, par ex. programmation fonctionnelle ou flux réactifs ou au printemps le gestionnaire d'erreurs global.
Exception :
Les exceptions cochées sont utiles, par ex. dans le code de la bibliothèque, où vous souhaitez forcer l'utilisateur à gérer l'exception.
Utilisez des exceptions non contrôlées pour les scénarios dans lesquels on ne peut raisonnablement pas s'attendre à ce que l'appelant récupère.
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.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!