Debugging a problem remotely (for example, when you simply don't have direct access to the system where your program is running) is often difficult. Even the simplest problems can be impossible to understand when all you get is a message like this:
Exception in thread "main" java.lang.StringIndexOutOfBoundsException
Exception in thread "main" java.lang.NullPointerException
at nodebugger.DebugExample2.findFirst(DebugExample2.java:20)
at nodebugger.DebugExample2.addHighlight(DebugExample2.java:27)
at nodebugger.DebugExample2.main(DebugExample2.java:45)
Or worse:
Execution failed
You have to do a lot of thinking, using techniques that I describe earlier, to sort out what might have gone wrong.
Logging to the rescue
However, you can make your job significantly easier by judicious use of logging (i.e. Writing to the console). Here's the example from the previous post, with more logging statements added:
package nodebugger;
import java.util.ArrayList;
import java.util.List;
public class DebugExample2 {
private static final String JIM = "Jim";
private static final String JAVA = "Java";
public String removeComment(String line) {
System.out.println("Removing comment, line = " + line);
if (line.charAt(0) != '*') {
return line;
}
// Return empty line
return "";
}
private static String addHighlight(String line,
String str,
String hilite) {
final int pos = line.indexOf(str);
String newLine = line.substring(0, pos);
newLine += "<" + hilite + ">" + str +
"" + hilite + ">";
newLine += line.substring(pos + str.length());
return newLine;
}
public static void main(String[] args) {
DebugExample2 de = new DebugExample2();
ListnewLines = new ArrayList ();
// Simplification - imagine that these lines
// are being read from a file.
for (String line : DebugExample.LINES) {
String newLine = line;
System.out.println("newLine = " + newLine);
if (line.contains(JIM)) {
System.out.println("Adding bold highlight");
newLine = addHighlight(line, JIM, "B");
}
else if (line.contains(JAVA)) {
System.out.println("Adding italic highlight");
newLine = addHighlight(line, JAVA, "I");
}
else if (line.startsWith("*")) {
System.out.println("Line is a comment - skipping");
newLine = "";
}
System.out.println("Removing comment");
newLines.add(de.removeComment(newLine));
}
System.out.println("newLines = " + newLines);
}
}
When you run this, you get the following:
newLine = * Comment 1
Line is a comment - skipping
Removing comment
Removing comment, line =
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 0
at java.lang.String.charAt(String.java:686)
at nodebugger.DebugExample2.removeComment(DebugExample2.java:12)
at nodebugger.DebugExample2.main(DebugExample2.java:51)
Note how much easier this is. You immediately see that there are two places where you are "removing comment", which immediately leads you to the problem (that comment lines are being removed twice).
Logging is expensive and ugly
This is a great technique when you are in the middle of developing new code, or in any situation where you can reproduce the problem yourself. You can add System.out.println calls wherever you want, reproduce the problem, look at the logs, and figure out where the problem is. However, you then need to remove all those ugly log lines before you give the code to anyone else. This means that if someone else encounters a bug in the same code, you need to get the code back, recreate the problem to ensure you can, add all the logging back in, then recreate the problem again with the logging, fix the problem, verify the fix, then remember to remove all the logging again before you give the code back to your customer.
Even worse, what if you can't reproduce the problem? You would have to get the code, add all this logging, send it to the customer who can reproduce the problem, ask them to install it and recreate the problem, then send you the log output so you can deduce what's happening. Now imagine you have a customer who wants a fix immediately - you will not be popular with this customer if you propose all that.
java.util.logging to the rescue
You can avoid these issues with the use of classes in the java.util.logging package (found at http://java.sun.com/javase/6/docs/api/java/util/logging/package-summary.html). They allow you to keep your logging statements around all the time, but only turn them on when the user wants them turned on.
Now look at this example with modified logging:
package nodebugger;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.*;
public class DebugExample2 {
private static final String JIM = "Jim";
private static final String JAVA = "Java";
private static final Logger LOG =
Logger.getLogger("DebugExample2");
static {
File file = new File("logging.properties");
try {
LogManager.getLogManager()
.readConfiguration(new FileInputStream(file));
} catch (IOException e) {
e.printStackTrace();
}
ConsoleHandler consoleHandler =
new ConsoleHandler();
consoleHandler.setLevel(Level.FINE);
LOG.addHandler(consoleHandler);
}
public String removeComment(String line) {
LOG.fine("Removing comment, line = " + line);
if (line.charAt(0) != '*') {
return line;
}
// Return empty line
return "";
}
private static String addHighlight(String line,
String str,
String hilite) {
final int pos = line.indexOf(str);
String newLine = line.substring(0, pos);
newLine += "<" + hilite + ">" + str +
"" + hilite + ">";
newLine += line.substring(pos + str.length());
return newLine;
}
public static void main(String[] args) {
DebugExample2 de = new DebugExample2();
ListnewLines = new ArrayList ();
// Simplification - imagine that these lines
// are being read from a file.
for (String line : DebugExample.LINES) {
String newLine = line;
LOG.fine("newLine = " + newLine);
if (line.contains(JIM)) {
LOG.fine("Adding bold highlight");
newLine = addHighlight(line, JIM, "B");
}
else if (line.contains(JAVA)) {
LOG.fine("Adding italic highlight");
newLine = addHighlight(line, JAVA, "I");
}
else if (line.startsWith("*")) {
LOG.fine("Line is a comment - skipping");
newLine = "";
}
LOG.fine("Removing comment");
newLines.add(de.removeComment(newLine));
}
System.out.println("newLines = " + newLines);
}
}
There are 7 logging levels:
- SEVERE
- WARNING
- INFO
- CONFIG
- FINE
- FINER
- FINEST
The things to note are line 13 (where a java.util.logging.Logger is obtained), the static initializer starting at line 15 (where the logger is configured), and lines like line 30 (where logging is actually performed).
By default, anything logged at INFO level or higher will be written to the console. Any call to logging methods that log at a level lower than that will be ignored by the logger itself. However, the user can change this behavior by editing the logging configuration file. In this case, it is called "logging.properties" and is in the root directory of the application, but you can put it anywhere.
With this, you can now put in "FINE" log calls instead of System.out.println calls. When you are debugging your code initially, you can ensure that your logging configuration is set to FINE level or higher by having this line in your configuration file:
.level=FINE
Then when you run your code yourself, you will see something like the following:
May 31, 2010 6:20:37 PM nodebugger.DebugExample2 main
FINE: newLine = * Comment 1
May 31, 2010 6:20:37 PM nodebugger.DebugExample2 main
FINE: Line is a comment - skipping
May 31, 2010 6:20:37 PM nodebugger.DebugExample2 main
FINE: Removing comment
May 31, 2010 6:20:37 PM nodebugger.DebugExample2 removeComment
FINE: Removing comment, line =
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 0
at java.lang.String.charAt(String.java:686)
at nodebugger.DebugExample2.removeComment(DebugExample2.java:31)
at nodebugger.DebugExample2.main(DebugExample2.java:70)
Notice that on lines 1, 3, 5, and 7, the logging has included additional information for you:
- The date and time the line was logged
- The full package and class name where the log entry was written
- The method name in which the log was written
This is followed on the next line by the level at which the log was written, then the actual log line itself.
If, however, the logging configuration is not set to FINE or higher, then this is all you will see:
Exception in thread "main" java.lang.StringIndexOutOfBoundsException: String index out of range: 0
at java.lang.String.charAt(String.java:686)
at nodebugger.DebugExample2.removeComment(DebugExample2.java:31)
at nodebugger.DebugExample2.main(DebugExample2.java:70)
This way, your code still appears professional, but you don't have to give up on the diagnosis abilities logging gives you.
Expensive logging parameters
This is fine for most situations, but what if the code is very sensitive for performance? Imagine a line like the following:
LOG.fine("My " + foo " "has value " + foo.getValue() +
" and " + bar + " has value " + bar.getValue());There's a whole lot of concatenation going on here. Also, there are two method calls, which themselves may be quite expensive. All this concatenation and these method calls must take place before the call to the fine() method is made. If that method is going to turn around and throw everything away, you've wasted a lot of time and effort for nothing.
This can be avoided, though, by use of the isLoggable() method. Here's an example where I've modified the previous example code:
if (LOG.isLoggable(Level.FINE)) {
LOG.fine("Removing comment, line = " + line);
}
By using isLoggable(), you only pay the cost of building up the parameter list if the log will actually be written.
A note about J2EE containers
Many J2EE containers (e.g. WebSphere Application Server) configure Java logging for you, and provide users with a simple interface for modifying the logging configuration (instead of forcing users to find and edit a configuration file). If this is the case, then the static initializer that configures the logger is probably not necessary.
Also, note that J2EE containers may even allow dynamic changes to take immediate effect. For this reason, it is not a good idea to cache the logging state at startup - your code should make the calls repeatedly.
Conclusion
Java logging is very useful for debugging. It is a good habit to use this logging instead of System.out.println for debug logging, because you can then leave the logs in place for use later if/when a problem occurs.
