This article reprints errors and lessons learned about Java exception handling

Error 1, abnormal choice

Figure 1. Anomaly Classification Figure 1 describes the structure of anomalies. In fact, we all know that anomalies are classified into detected anomalies and undetected anomalies, but the application of these two kinds of anomalies is confused in practice. Because undetected exceptions are easy to use, many developers consider detecting exceptions useless. In fact, the application scenarios of exceptions can be summarized as follows:

The calling code cannot continue execution and needs to be terminated immediately. There are so many possibilities for this to happen, such as the server being disconnected, the parameters being incorrect, etc. In these cases, undetected exceptions apply, no explicit catching and handling of the calling code is required, and the code is straightforward.

Second, the calling code needs further processing and recovery. If you define SQLException as a non-detection exception, so that when manipulating data, the developer can take it for granted that SQLException does not require explicit capture and processing of the calling code, This can lead to serious Connection failures, Transaction failures, dirty data in the DB, etc. Because SQLException is defined to detect an exception, it drives developers to explicitly catch and clean up resources after the code generates an exception. Of course, after cleaning up resources, you can continue to throw undetected exceptions to prevent the execution of the program. Based on observation and understanding, detecting exceptions can mostly be applied to tool classes.

Error 2. Displaying exceptions directly on the page or client.

It is common to see exceptions printed directly to the client. In the case of JSP, the container prints the exception stack information directly to the page by default once the code runs abnormally. In fact, from the customer’s point of view, any exception has no practical significance, the vast majority of customers do not understand the exception information, software development should try to avoid the exception directly presented to the user.

package com.ibm.dw.sample.exception;
/** * Custom RuntimeException * add error code attribute */
public class RuntimeException extends java.lang.RuntimeException { 
     // Default error code
    public static final Integer GENERIC = 1000000; 
    // Error code
    private Integer errorCode; 
     public RuntimeException(Integer errorCode, Throwable cause) {
            this(errorCode, null, cause);
     }
     public RuntimeException(String message, Throwable cause) {
            // Use a generic error code
            this(GENERIC, message, cause);
     }
     public RuntimeException(Integer errorCode, String message, Throwable cause) {
            super(message, cause);
            this.errorCode = errorCode;
     }
     public Integer getErrorCode(a) {
            returnerrorCode; }}Copy the code

As shown in the sample code, we introduce the error code in the exception, and once an exception occurs, we simply present the error code of the exception to the user or convert the error code into a more readable prompt. In fact, the error code contains another function, the developer can also know exactly what type of exception occurred from the error code.

Contamination of code hierarchies

We often divide our code into hierarchies like Service, Business Logic, and DAO. The DAO layer contains methods to throw exceptions, as shown in Listing 2:

public Customer retrieveCustomerById(Long id) throw SQLException {
 // Query database by ID
}
Copy the code

The above code looks fine at first glance, but if you think about it from a design coupling perspective, the SQLException here taints the upper level of the calling code, which either explicitly uses try-catch or throws further up the hierarchy. According to the design isolation principle, we can appropriately modify it to:

public Customer retrieveCustomerById(Long id) {
     try{
            // Query database by ID
     }catch(SQLException e){
            // Use undetected exception encapsulation to detect exceptions and reduce hierarchical coupling
            throw new RuntimeException(SQLErrorCode, e);
     }finally{
            // Close the connection and clean up the resources}}Copy the code

Mistake four: Ignoring exceptions

The following exception handling simply prints the exception to the console and has no meaning. And there is an exception that does not interrupt the program, which then calls the code to continue executing, leading to more exceptions.

public void retrieveObjectById(Long id){
   try{
       / /.. some code that throws SQLException
    }catch(SQLException ex){
     /** * Anyone who knows knows that the exception print here is meaningless and simply prints the error stack to the console. * In a Production environment, the error stack needs to be printed to a log. * And the continuation of the program after the catch will cause further problems */ex.printStacktrace(); }}Copy the code

Can be reconstituted:

public void retrieveObjectById(Long id){
 try{
    / /.. some code that throws SQLException
 }
 catch(SQLException ex){
    throw newRuntimeException (" Exception in retieveObjectById ", the ex); }finally{
    //clean up resultset, statement, connection etc}}Copy the code

This error is more basic, generally will not make this low-level error .

Error # 5: Include exceptions in a loop block

As shown in the code below, the exception is contained in the for loop block.

for(int i=0; i<100; i++){
    try{}catch(XXXException e){
         / /... .}}Copy the code

We all know that exception handling consumes system resources. At first glance, everyone thought they would not make such a mistake. To put it another way, class A executes A loop that calls A method of class B, but the method called in class B contains A try-catch block. Strip out the class hierarchy and the code looks exactly the same.

Catch all potential exceptions with Exception

A method execution throws several different types of exceptions. For code brevity, use the base class Exception to catch all potential exceptions, as shown in the following example:

public void retrieveObjectById(Long id){
    try{
        / /... A code call that throws IOException
        / /... A code call that throws an SQLException
    }catch(Exception e){
        // All potential exceptions caught by the base Exception class are used here. If multiple levels are caught this way, valid information about the original Exception will be lost
        throw newRuntimeException (" Exception in retieveObjectById ", e); }}Copy the code

Can be reconstituted

public void retrieveObjectById(Long id){
    try{
        / /.. some code that throws RuntimeException, IOException, SQLException
    }catch(IOException e){
        // Only IOException is caught
        throw new RuntimeException(/* Specifies the error code for IOException */Code, "Exception in retieveObjectById", e); }catch(SQLException e){
        // Just catch the SQLException
        throw new RuntimeException(/* Specify the error code for this SQLException */Code, "Exception in retieveObjectById", e); }}Copy the code

Multilevel encapsulation throws undetected exceptions

If we insist that different types of exceptions must be captured differently, we can bypass this section for the most part. However, if a single code call throws more than one Exception, it is often not necessary to write a catch statement for each different type of Exception. For development purposes, any Exception is sufficient to explain the specific problem of the program.

try{
    // May throw a RuntimeException, IOExeption, or other exceptions;
    // Note the difference between this and myth 6, where a single piece of code throws multiple exceptions. These are multiple pieces of code, each throwing a different exception
}catch(Exception e){
    // Convert Exception to RuntimeException as usual, but here e is an instance of RuntimeException, wrapped in the previous code
    throw new RuntimeException(/**/code, /**/, e);
}
Copy the code

If we convert all exceptions to RuntimeExceptions as shown in the previous example, we do another wrapper when the Exception type is already RuntimeException. The RuntimeException is reencapsulated again, thus losing the valid information carried by the original RuntimeException.

The solution is to add a check to the RuntimeException class to verify that the Throwable parameter is not an instance of RuntimeException. If so, the corresponding properties are copied to the newly created instance. Or use different catch blocks to catch RuntimeException and other exceptions. Personal preference one, the benefits are self-evident.

Eight, multi-level printing abnormal

Let’s take A look at the following example, which defines two classes A and B. The code of class B is called in class A, and exceptions are caught and printed in both classes.

public class A {
 private static Logger logger = LoggerFactory.getLogger(A.class);
 public void process(a){
     try{
     // instantiate class B
     B b = new B();
     b.process();
     //other code might cause exception
    } catch(XXXException e){
       // If the class B process method throws an exception, the exception is printed in class B, and here, twice
       logger.error(e);
       throw new RuntimeException(/* Error code */ errorCode, /* Exception message */msg, e); }}}public class B{
 private static Logger logger = LoggerFactory.getLogger(B.class);
    public void process(a){
        try{
            // Code that might throw an exception
        }
        catch(XXXException e){
            logger.error(e);
            throw new RuntimeException(/* Error code */ errorCode, /* Exception message */msg, e); }}}Copy the code

The same exception will be printed twice. If the hierarchy were a little more complex, it would be hard enough just to locate the specific problem in the exception log, regardless of the system performance cost of printing the log.

In fact, logging only needs to be captured and printed in the outermost layer of the code. Exception printing can also be written as AOP and woven into the outermost layer of the framework.

Error nine, the information contained in the exception is not sufficient to locate the problem

Exceptions need to be more than just useful to developers

We know that java.lang. Exception has a constructor for a string type parameter that can be customized to be an easy-to-understand prompt.

With simple custom information developers can only know where an exception is occurring, but in many cases developers need to know what parameters are causing the exception. At this point we need to append the parameter information of the method call to the custom information. The following example lists only one parameter. In the case of multiple parameters, you can write a separate utility class to organize such strings.

public void retieveObjectById(Long id){
    try{
        / /.. some code that throws SQLException
   }catch(SQLException ex){
        // Add parameter information to the exception information
        throw new RuntimeException(“Exception in retieveObjectById with Object Id :”+ id, ex);
   }
}
Copy the code

Failure to anticipate potential anomalies

In the process of code writing, due to the lack of deep understanding of the calling code, we cannot accurately determine whether the called code will generate exceptions, so we ignore the processing. After the Production Bug occurs, I realize that I should add exception catching in some code, and I can’t even pinpoint the reason for the exception. This requires developers not only to know what they are doing, but also to be as aware as possible of what others are doing and what the consequences might be, and to consider the entire application process as a whole. These ideas influence how we write and handle code.

Mixing multiple third-party log libraries

With the increasing variety of Java third-party logging libraries, a large project can introduce a variety of frameworks, which in turn rely on the implementation of different logging libraries. The most troublesome problem is not importing all the required log libraries, but the incompatibility of the introduced log libraries themselves. If it is possible to solve this problem early in the project, you can either reintroduce the logging libraries in all the code as needed, or change the framework. However, such costs are not affordable for every project, and the more the project progresses, the greater the risk.

What can be done to effectively avoid this problem? Most frameworks today already address this problem by configuring Properties or XML files, parameters, or scanning the logging implementation classes in the Lib library at runtime to determine which particular logging library to apply at runtime.

By following the principle that you don’t need multiple levels of logging, we can simplify many classes that call logging code. In many cases, we can use interceptors or filters to print logs, reducing the cost of code maintenance and migration.