Note: The code samples discussed in this post are available at http://www.github.com/bradleyross/tutorials while the documentation can be found at http://bradleyross.github.io/tutorials. This project uses Maven. Although you don’t have to use Maven, the comments in the pom.xml files provide a great deal of useful information. The Javadoc files also contain links to the source code and other Javadoc libraries.
I am going to be revising this a little, but I wanted to get this draft out.
Introduction
After discussing the mental attitudes for debugging, it is time to discuss the tools and techniques that can be used in debugging in Java. Many of the Java open source libraries make use of the various Java logging frameworks (also known as logging services). Using a Logging Framework greatly simplifies verification, testing, and debugging. Not having a working logging service makes it almost impossible to verify the proper functioning of your code. The vast majority of programmers use the Apache logging frameworks (http://logging.apache.org)1.
API versus Implementation
One of the first things that you have to understand is the difference between the API’s and the implementations. The API is the set of class names and method signatures that you will use to actually place items in the log. Examples of code that places messages in the log can be found in the package bradleyross.library.debugging package in the tutorial-commons module.
There are actually multiple implementations of the API’s. There is one set that is based on the log4j 1 logging framework (See the dependencies in the pom.xml file in the tutorials-log4j1 module.). Another is based on the log4j 2 logging framework (See the dependencies in the pom.xml file in the tutorials-log4j2 module.). There are many classes in the log4j 1 implementation that have the same name as those in the log4j 2 implementation. If you use the wrong combination of classes, the logging software will not work correctly and may not work at all. (One of the major dangers is that you may think it is working correctly, but it simply isn’t displaying the error messages.)
The logj4 2 implementation is currently the only API being maintained. It is also capable of supporting all of the API’s supported by the log4j 1 and slf4j implementations. The syntax for the configuration file differs between the log4j 1 and log4j 2 implementations, but creating a new configuration file is not that difficult.
Java Exceptions
I am assuming that the reader is familiar with the nature of Java exceptions. If you feel uncomfortable, the Oracle web site
contains a tutorial on exceptions.2
Java exceptions can be either checked or unchecked exceptions. Unchecked exceptions will be passed to the next higher object in the stack unless a try/catch block captures the exception and processes it. (If you are already at the highest object in the stack, the program will halt and an error message will be generated.) If there is no try/catch block processing an exception in a class and the exception is not listed in the throws clause of the method signature, the program will be halted when the exception is passed to the next higher class in the stack.
Catch and Release
When you are trying to test or debug a program, you want to ensure that your log statements do not alter the flow through the program. The approach that I take to this is what I call the Catch and Release method. When you capture the exception, log it and then throw it so that it goes to the next higher object in the stack.
Consider the following snippet of code.
LineNumberReader input; File file; try { file = new File("reports.txt"); input = new LineNumberReader(new FileReader(file)); String data = input.readLine(); if (data != null) { System.out.println(data); } } catch (FileNotFoundException e) { logger.warn("File not found", e); throw e; } catch (IOException e) { logger.warn("IO exception", e); throw e; } catch (Throwable e) { logger.warn("Unexpected throwable", e); throw e; }
Because I am throwing the exception after logging it, the flow of the program is unaffected. After logging the information, the exception is passed up to the next higher structure, which is what it would have done originally. There are cases where I won’t want to throw the exception, but not when all I am doing is logging it.
Where to Test
The following are some guidelines for inserting logging statements in the code. In inserting these log statements you have a choice of several warning levels. (The most severe is FATAL, with the levels ERROR, WARN, INFO, DEBUG, and TRACE following in order of decreasing severity.4)
- Every catch block should contain a logging statement.
- If there is a throws clause in the method signature, consider placing a try/catch block around the entire contents of the method. This may enable you to get more usable information.
- I should have a try/catch block everywhere I believe that something might go wrong.
- There should be log statements after an operation has taken place successfully. To prevent the size of the log from becoming unmanageable, you can alter the levels of the log statement. By putting these messages at the DEBUG or TRACE they will be available for debugging but won’t clog the system while running normally.
Encapsulating Logging Statements
You want to get as much information passed in the log statements as possible. However, writing all that code can be a burden. The way to do this is to encapsulate the log statement in a helper class such as bradleyross.library.helpers.ExceptionHelper in the tutorials-common module. As an example, SQLException contains some more information than most exception classes, and this code is designed to detect SQLException and print the extra values.
Another possibility would be to write an additional method or class that can process file-related errors. Such a method would have the File object and the file name as parameters and then place additional information in the log such as size of the file, whether the file exists, type of file, and read/write permissions.
Throwing Exceptions
When you throw an exception, you should always consider throwing a subclass of the standard Java exceptions. (Throwable, Exception, Error, RuntimeException, IOException, SqlException, etc.) This is something that you may be able to take advantage of in the future.
Attitude and Philosophy
The term “expect the unexpected” definitely applies to debugging code. The following are some things to keep in mind.
- You don’t know how the code will be used in the future.
- You don’t know who might make changes in the future or what changes they might make.
- There is a very good chance that errors will be introduced into the code. Furthermore, the existence of these errors may not be detected until long after all of the programmers have left the project.
- Descriptions of errors by users will often be incomplete or misleading.
- Your log messages should contain enough information to determine the location of the problem.
References
- ↑1 Apache Organization, Apache Logging Frameworks, http://logging.apache.org
- ↑2 The Java Tutorials, Exceptions, https://docs.oracle.com/javase/tutorial/essential/exceptions/
- 3 Slf4j web site, http://www.slf4j.org
- ↑4 Javadoc for class org.apache.logging.log4j.Level, http://logging.apache.org/log4j/2.x/log4j-api/apidocs/org/apache/logging/log4j/Level.html