The problem of exception handling is a frequent issue in Java code reviews. As mentioned in this article, the basic rules to follow in Java exception handling are not that complicated. Unfortunately, even Java programmers with many years of experience in their work experience are often unable to implement proper exception handling. Not to mention for Java programmers with less experience.
Why is improper exception handling so widespread? There are three major factors that can be considered. First, there is historical confusion in the exception mechanism (especially checked exceptions) in the Java language specification, giving programmers excessive freedom. Secondly, unless you not only develop the application but also actually operate it, you should not notice the harmful effects of inappropriate exception handling. Finally, there is no compact collection of resources to learn proper exception handling. In beginner's books, ad hoc exception handling that causes ʻe.printStackTrace () `is conspicuous, and appropriate exception handling methods are scattered in books and framework documents for intermediate users and above.
Therefore, in this article, we will divide it into three stages, for beginners, beginners, and intermediates, how to avoid the trap of excessive freedom of exception handling, and what is appropriate exception handling from the viewpoint of operation. I would like to make it compact. The target application is mainly intended for the server side (although many items should be applicable not only to the server side).
Here are some things to understand before you go to the scene.
There is a big problem with this code, which expresses an abnormal system of method calls with a special return value. First, the caller of createItem ()
may forget to implement anomalous processing. Also, the caller does not know what was actually wrong, just knowing that the return value is empty.
Item item = createItem();
if (item == null) {
//Abnormal system
...
} else {
//Normal system
...
}
With the introduction of the exception mechanism, the code can be rewritten as follows: As a result, the caller can be guided to explicitly implement the abnormal processing. In addition, the exception message and stack trace caught in the catch clause allow the caller to know the details of the anomaly.
try {
Item item = createItem();
//Normal system
...
} catch (ItemCreationException e) {
//Abnormal system
...
}
If the exception mechanism is used, it is possible to simply deal with abnormal systems even in a multi-stage call hierarchy. For example, applications with a typical layered architecture tend to have this multi-tiered call hierarchy (at the beginner level, you don't have to worry about the details if you find something complicated).
-(Presentation layer) Pre-processing and post-processing of web framework -(Presentation layer) Controller entry method -(Presentation layer) Controller private method -(Domain layer) Facade-like Service method -(Domain layer) Service method that is responsible for the actual business logic -(Data Source layer) Repository method responsible for data access logic
The problem here is how to convey the abnormal system in the lower layer to the upper layer. For example, suppose that when an error occurs in the data access of the lowest layer, it is necessary to return an appropriate error response in the post-processing of the highest layer. To meet this requirement with a special return value, an abnormal return value must be returned in a bucket-relay manner from the lowest layer to the highest layer.
On the other hand, by using an exception mechanism instead of a special return value, it is possible to handle an abnormality in any lower layer in any upper layer while keeping the input / output of the method simple. The lower layer just throws an exception like throw new IllegalStateException ("Something is wrong")
without thinking, and some upper layer just catches the exception in the catch
clause.
Below is a basic exception class inheritance tree (note that all packages are java.lang
). Exceptions that inherit from ʻExceptionare __checked exceptions__, and exceptions that inherit from
RuntimeExceptionare __unchecked exceptions__. Note that
Throwable and ʻError
do not require a detailed understanding at the beginner level (rather, it is better not to touch them).
Checked exceptions are exceptions that force the caller to take some action. For example, if you write the code that reads the contents of the file as shown below, a compile error will be output.
List<String> lines = Files.readAllLines(Paths.get("items.csv"));
Error:(x, x) java:Exception java.io.No IOException is reported. Must be captured or declared to throw
Here, there are two steps you can take to get the compilation through from a beginner's point of view. First, there is a method of catching with try-catch. Note that there are many traps in the processing after capture here (appropriate measures will be described later).
try {
List<String> lines = Files.readAllLines(Paths.get("items.csv"));
//Normal system
...
} catch (IOException e) {
//Abnormal system
...
}
Alternatively, you can explicitly leave it to the caller in the throws clause of the method. However, there are not many situations where this method can be used. Details will be described in the section for intermediate users.
public void processItems() throws IOException {
List<String> lines = Files.readAllLines(Paths.get("items.csv"));
//Normal system
...
}
An unchecked exception is an exception, such as a checked exception, that the caller is not forced to handle. For example, the ʻInteger.parseInt (String) method may throw an unchecked exception
NumberFormatException` (reference: Javadoc /api/java/lang/Integer.html#parseInt-java.lang.String-)), but no compilation error is output. Instead, an error occurs at run time.
Integer value = Integer.parseInt("ABC"); //Compile without doing anything
java.lang.NumberFormatException: For input string: "ABC"
Now, remember what to do if you catch a checked exception for compilation, just how to wrap it in this RuntimeException
and send it at the beginner level. You don't need to understand in detail what happens to the unchecked exceptions you throw at the beginner level. Don't do anything extra.
try {
List<String> lines = Files.readAllLines(Paths.get("items.csv"));
//Normal system
...
} catch (IOException e) {
throw new RuntimeException(e); //Wrap in unchecked exception and send
}
RuntimeException
It would be better if you could wrap it in an appropriate unchecked exception and send it instead of just focusing on it. Below are some commonly used unchecked exceptions and their very rough uses.
So, in fact, the code wrapped in the above RuntimeException
should be rewritten to use ʻUncheckedIOException`.
try {
List<String> lines = Files.readAllLines(Paths.get("items.csv"));
//Normal system
...
} catch (IOException e) {
throw new UncheckedIOException(e); //Wrap and send in more appropriate unchecked exceptions
}
Here, we will describe what you need to understand when developing and operating under the direction of someone in the field.
As mentioned above, in most cases you can write minimally safe code, just remembering to wrap it in an unchecked exception and send it out. But for some reason, Java programmers tend to deviate from the minimum line by doing extra things or forgetting what's important. The following are common improper exception handling. Please do not imitate it.
The first forbidden hand is crushing. As a result of squeezing, a problem occurs in the subsequent processing that expected the correct result of the processing that was originally executed. A typical problem is NullPointerException
. In general, there are occasions when NullPointerException
is treated as a bad guy in Java, but nothing here is that your own code is bad, not NullPointerException
.
List<String> lines = null;
try {
lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
//crush
}
lines.stream().forEach(...); //I get a NullPointerException because lines are still null here
However, there are few cases that should be crushed with will. In that case, it is better to clearly indicate the intention in a log or a comment.
Often seen in the primer is the code that calls ʻe.printStackTrace ()in the
catch clause, which is fine, but this is almost always unacceptable in real-world Java applications. I try to print a message with ʻe.printStackTrace ()
, but if I continue processing, it's not much different from squeezing exceptions. Also, the output destination of ʻe.printStackTrace ()` is generally standard error output, and unlike logs, meta information such as date and time and request ID that is useful for investigating the cause is missing. If you leave such code unattended, you'll be plagued by a mysterious stack trace that doesn't give you clues to solve it during operation.
List<String> lines = null;
try {
lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
e.printStackTrace(); //Sorry to output the stack trace
}
lines.stream().forEach(...); //I end up getting a NullPointerException here
Even if replacing ʻe.printStackTrace ()above with log output like
log.warn ("Could not read items: "+ e, e)` solves the output meta information problem, It is the same in that if the processing is continued, it is not much different from crushing the exception.
It is this forbidden hand that a well-mannered beginner tends to do. After the own log is output individually, the log caused by the same problem is output again by the global exception handler described later. These look like two types of exception logs for different issues in operation, causing confusion. In the adult world, being careful is not unconditionally good.
List<String> lines = null;
try {
lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
log.warn("Could not read items: " + e, e); //The meticulousness backfires
throw new UncheckedIOException(e);
}
lines.stream().forEach(...);
If you want to include a message, send it as an exception message.
List<String> lines = null;
try {
lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
throw new UncheckedIOException("Could not read items", e); //Appeal casually
}
lines.stream().forEach(...);
There are some mistakes in which you forget the "wrap" of "wrap and send in unchecked exception" and simply throw another unchecked exception. The downside in this case is that you don't know the root cause of the exception during operation. For example, various events such as the fact that the file does not exist and the right to access the file cannot be considered as the cause of the exception that occurs when accessing the file. If you forget to wrap the exception that indicates the root cause and send another exception, failure analysis during operation becomes difficult.
List<String> lines = null;
try {
lines = Files.readAllLines(Paths.get("items.csv"));
} catch (IOException e) {
throw new IllegalStateException("Could not read items"); //Information that should have been in the original exception is lost
}
lines.stream().forEach(...);
When the open processing of a resource such as a file or database connection is executed, the close processing must be performed after that in principle (whether or not the close processing is strictly required is determined by referring to the Javadoc of the open processing method. thing). A common negative effect of forgetting to close is a so-called memory leak. However, in many cases, the problem is not discovered immediately after the release, and it becomes apparent like a time bomb with the subsequent operation. Once discovered, the site will be exhausted by provisional operations such as searching for the criminal and restarting regularly.
try {
BufferedReader in = Files.newBufferedReader(Paths.get("items.csv"));
in.lines().forEach(...);
//in is not closed
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Use the try-with-resources syntax instead.
try (BufferedReader in = Files.newBufferedReader(Paths.get("items.csv"))) {
in.lines().forEach(...);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
Before the introduction of the try-with-resources syntax, closing processing was performed in the finally
clause, but it is redundant and difficult to read, and there are many complicated things to consider, such as what to do if an exception occurs in the finally clause. It is safe to avoid it at the beginner's stage.
BufferedReader in = null;
try {
in = Files.newBufferedReader(Paths.get("items.csv"));
in.lines().forEach(...);
} catch (IOException e) {
throw new UncheckedIOException(e);
} finally {
try {
in.close();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
For reference, try-with-resources can only be used if the target class implements the java.io.Closeable
interface or the java.lang.AutoCloseable
interface. For details, refer to Official Documents.
This forbidden person has almost no operational harm, but it's still bad that the code isn't readable.
try {
Item item = createItem();
//Normal system
...
} catch (ItemCreationException e) {
throw new IllegalStateException(e);
} catch (InvalidProcessingException e) {
throw new IllegalStateException(e);
}
Use multi-catch instead.
try {
Item item = createItem();
//Normal system
...
} catch (ItemCreationException | InvalidProcessingException e) {
throw new IllegalStateException(e);
}
Unchecked exceptions can also be used to defensively express conditions such as "only these parameters should come here" and "this process should not be executed".
A common application is the throwing of ʻIllegalArgumentException` for invalid parameters in the guard clause at the beginning of the method.
public void processItem(Item item) {
if (!item.isValid()) {
//Invalid parameter
throw new IllegalArgumentException("Invalid item: " + item);
}
//Normal system
...
}
The above code is as follows using Preconditions of Guava You can write in one line. I can't say it out loud, but if you don't like the "just in case" level branching that reduces unit test coverage, this is a good option.
public void processItem(Item item) {
Preconditions.checkArgument(item.isValid(), "Invalid item: %s", item);
//Normal system
...
}
Similarly, when checking for null parameters, it is better to use the standard class library java.util.Objects.requireNonNull (T)
instead of your own if statement. By the way, the exception thrown here is the infamous NullPointerException
. Guava's similar feature, Preconditions.checkNotNull (T)
, also throws a NullPointerException
, albeit with some resistance. Let's wrap it around a long object.
public void processItem(Item item) {
Objects.requireNonNull(item, "Item is null");
//Normal system
...
}
In addition, it may throw an exception in the normally unreachable default part of a switch statement.
Result result = processItem(item);
switch (result) {
case SUCCEEDED:
...
break;
case FAILED:
...
break;
...
default:
throw new IllegalStateException("This would not happen");
}
So far, the description has focused mainly on the side that throws the exception, but next we will describe how to handle the thrown exception.
The simplest and most straightforward way to catch exceptions is to write your own last bastion of try-catch processing at the top of the call hierarchy (main method for command line applications and Presentation layer Controller for web applications). However, in reality, there are few opportunities to use this method. In real-world Java application development, there are quite a few cases where the main method is implemented in full scratch, and there is a better alternative to the web framework for the Presentation layer Controller.
Modern web frameworks provide a mechanism to execute exception handlers according to the exception class that occurs. The advantage of such a mechanism is that the common processing of multiple Controllers can be flexibly combined without using the inheritance structure of the Controller class. The exception handling mechanism in a typical Web framework is shown below.
-JAX-RS (Jersey) Exception Handling Mechanism --For the exception handler class, the exception class is specified using the type parameter. -Spring MVC Exception Handling Mechanism --Annotations are used to specify exception classes for exception handler classes and exception handler methods.
A more general mechanism is the DI container Interceptor mechanism. You can use the Interceptor mechanism to insert pre-processing and post-processing for any method call of a class under the control of a DI container. The Interceptor mechanism in a typical DI container is shown below.
-CDI (Weld) Interceptor Mechanism -Spring Interceptor mechanism
Transaction control (automatic rollback when an exception occurs under a specific method) is a typical process realized by using the Interceptor mechanism for exceptions. The transaction control mechanism in a typical DI container is shown below.
--CDI Transaction Control Mechanism -Spring transaction control mechanism
When you can handle exceptions globally according to class, you want to create your own exceptions. For example, you may want to return a personalized error response specific to a particular business logic or external API access logic. To create your own exception, you can inherit the existing exception class and override the constructor.
public class ItemCreationException extends RuntimeException {
public ItemCreationException() {
}
public ItemCreationException(String message) {
super(message);
}
public ItemCreationException(Throwable cause) {
super(cause);
}
public ItemCreationException(String message, Throwable cause) {
super(message, cause);
}
public ItemCreationException(String message, Throwable cause, boolean enableSuppression,
boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
At first glance, the amount of code looks large, but it's actually generated by the IDE, so the work is minor. For example, in the case of Eclipse, in the new class generation dialog, just specify the superclass name and check the constructor generation.
Here, we will describe what you should understand when developing and operating in the field at your own discretion.
For exceptions handled by the global exception handler, it is necessary to output a log at an appropriate level and perform daily operations. The following is an example of the policy regarding the proper use of levels and operation. This is just an example, and each team has different policies.
However, it is generally difficult to strictly apply the rules immediately after the release, and in reality, the above operations will be developed step by step by cutting the phase.
In real-world Java applications, exception handling that is specific to a certain function may be required instead of global exception handling. The following cases are typical examples.
When it is not appropriate by design for the lower layer to throw an exception, it may catch the exception and return another return value. For example, the following code catches an exception indicating that the record does not exist when accessing the database, and instead returns a return value of ʻOptional`.
public Optional<Item> findByName(String name) {
try {
Item item = entityManager.createQuery("...", Item.class).setParameter(1, name).getSingleResult();
return Optional.of(item);
} catch (NoResultException e) {
return Optional.empty();
}
}
Note that you should not choose a design that returns null instead of Optional. See Twitter search results for "null kill" for more details. ..
In processing such as an external API call, an exception indicating an accidental communication error may be caught and retry processing may be executed. However, when trying to implement retry processing seriously, there are surprisingly many things to consider such as timeout time setting, retry count setting, backoff algorithm, coping with simultaneous retry occurrence, monitoring of retry occurrence status, and so on. Rather than implementing it yourself, Microprofile Retry and [Spring Retry](https:: It would be safer to use a library like //github.com/spring-projects/spring-retry). In the future, it is expected that such logic will be taken up by Service Mesh mechanisms such as Istio outside the application.
In addition, alternative processing may be performed according to the requirements. Since the processing content is case by case, details are omitted.
Finally, I will explain how to use the checked exceptions that I have avoided so far.
First, for low-level libraries and frameworks, there are occasions when you should make good use of checked exceptions, as is the case with the java.io
package.
Also, for business logic, if you want to force the caller to process abnormal systems, using a check exception is an option. Below are examples of methods that use checked exceptions and methods that return enums. The method using checked exceptions has a simple method signature, and similar processing can be shared by utilizing the exception inheritance structure. On the other hand, in the method of returning with enum, it is easy to understand that the caller handles the return value comprehensively. Which one to choose is on a case-by-case basis.
public void sendNotification(Member member, Notification notification) throws InvalidMemberException {
//Method using check exception
...
}
try {
sendNotification(member, notification);
//Normal system
...
} catch (InvalidMemberException e) {
//Alternative processing
...
}
public NotificationResult sendNotification(Member member, Notification notification) {
//Method to return with enum
...
}
NotificationResult result = sendNotification(member, notification);
switch (result) {
case SUCCEEDED:
//Normal system
...
break;
case INVALID_MEMBER:
//Alternative processing
...
break;
...
}
So far, we have shown what you need to understand about exception handling in real-world Java applications in three stages: beginners, beginners, and intermediates. We hope this article will reduce unproductive code reviews and painful application operations as much as possible.
Recommended Posts