A debugger is one of the most powerful tools available to the developer and can assist immensely for both tracking down defects and helping to understand an application's core behavior. Despite the benefits debugging tools offer, my experience is that few developers take advantage of these tools and so miss out on one of the biggest productivity gains an IDE can offer. In many cases, the common debugging practice is to adorn the application with strategically placed log messages and examine the output at the console as the application executes.
This method of fault-finding and discovery is workable but is a slow and ponderous means of analyzing code. With a suitable debugging tool, you can set breakpoints within the application and then step through the code from that point on, line by line. With the debugger invoked, a wealth of information is available, the state of threads can be viewed, the contents of variables observed, and the call stack of the application monitored. The ability to replicate this level of information through log messages at the console is highly unlikely. Armed with a full array of information, it is possible to get to the root of a problem quickly and easily.
To understand the full benefits a debugger can offer, this section walks through how this useful tool can be put to best effect on a J2EE solution. First, let's look at the technology in the JVM that makes debugging tools possible.
Java Platform Debugger Architecture
The Java Platform Debugger Architecture (JPDA) was introduced as of JDK 1.3, enabling a debugging tool to attach to a JPDA-compliant JVM. The JPDA has the debugging tool execute in a separate JVM from that of the application being debugged. This architecture has significant advantages over running a debugging tool in the same JVM as that of the application being debugged. For optimal debugging, it is often necessary to suspend the JVM of the debugged application, a process that is not possible if both debugger and debuggee are collocated in the same JVM.
The JPDA defines two interfaces and a protocol:
Implementations of the JDI and JVMDI interfaces interoperate via two software components, which are imaginatively called the front end and back end in the JPDA documentation. Figure 13-7 illustrates the JPDA debugging model.
Figure 13-7. The Java platform debugger architecture.
A debugging tool submits requests to an attached JVM through the JDI. The front-end component forwards all JDI requests over the communications channel in accordance with the JDWP. The receiving end of the communications channel is the back-end component, which translates JDWP packets and submits them to the JVMDI of the JVM. The entire process is bidirectional because the JVM being debugged must be able to report events back to the debug tool, for example, when a breakpoint is triggered.
The function of the back-end and front-end components is similar to the approach used for marshaling RMI calls and so should be familiar.
RMI is discussed in Chapter 4.
Local and Remote Debugging
JDWP is independent of the underlying transport mechanism used to move requests between both ends of the conversation. This independence allows a JVM to be debugged on a machine separate from the JVM running the debugger. This process is known as remote debugging and is achieved by specifying a suitable transport mechanism, known as a transport in JPDA terminology, for carrying traffic between the two machines.
Sun Microsystems' reference implementation for JDPA provides two transport types: a socket transport and a shared memory transport. The socket transport uses TCP/IP for supporting remote debugging between machines. The shared memory transport supports interprocess communication between JVMs running on the same box.
JVM Debug Configuration
The details of configuring a JPDA-compliant JVM are mostly taken care of for us by Eclipse. However, to take advantage of remote debugging, you must understand how a JVM is configured to support debug tools attaching remotely to the JVM.
JVM configuration is proprietary to the target VM, and you should refer to the documentation for your specific implementation. The following instructions refer to the Sun Java HotSpot VM 1.4.2.
Debug support is enabled in the JVM by providing the arguments -Xdebug and -Xrunjdwp. The -Xdebug argument instructs the JVM to listen for debugging connections. The -Xrunjdwp argument is a little more complicated and is used to configure the transport for incoming debug connections. To start the JVM so that it can accept remote connections, this argument is set as follows:
The suboptions require some explanation:
Options are dt_socket for TCP/IP or st_shmem for shared memory.
These settings also need to be provided to the debugger so it can attach to the target VM. Under Eclipse, a remote debugging configuration is created for this purpose. Figure 13-8 shows the Eclipse dialog for configuring remote debugging for an application.
Figure 13-8. Eclipse configuration dialog for remote debugging.
The Eclipse dialog makes it easy to configure remote debugging once the target VM is set up correctly. The dialog shown offers a single connection type of socket for the transport. The connection properties specify the host machine name, and the port the target VM listens for incoming connections.
Now that we've covered the basics of the JPDA, let's look at the features Eclipse, with some help from MyEclipse, provides for debugging J2EE solutions.
Debugging a J2EE Application
With JPDA support, Eclipse lets us set breakpoints in J2EE components that are under the control of the J2EE server. Thus, we can break in on the execution of components such as servlets and EJBs to observe their behavior in an executing state. Moreover, if the source is available, we can debug the implementation of the J2EE server itself.
JPDA-compliant debugging tools remove the need to scatter log messages, shotgun style, throughout the code, and provide a superior level of diagnostic reporting than is possible with the use of logging and console messages.
Logging messages still have their uses, as it is highly unlikely you'll get the opportunity to use a debugger for tracing faults in a production environment.
To debug a servlet under Eclipse involves setting a breakpoint in the editor, then starting the servlet engine and deploying the application. Deployment of the application and the startup of the server can all be performed from within the IDE using the services MyEclipse provides.
With the application deployed and the target server running, accessing the page associated with the servlet from a browser causes the breakpoint to be hit. This triggers Eclipse to switch to the debug perspective, displaying the debugger's editor and views. Figure 13-9 shows the Eclipse debug perspective with an active breakpoint for a servlet running under Tomcat 5.0.
Figure 13-9. The Eclipse debug perspective.
The different views provided in the debug perspective allow the state of variables to be inspected and the current instruction pointer in the code to be advanced line by line. Figure 13-9 shows the debug, variables, code, and outline views. The debug view displays the processes, threads, and stack frames. The variables view allows the state of all object instances to be inspected. The source being debugged is displayed in the editor, while the outline view shows the code structure. Each represents the state of the code as at the current line of execution.
The inclusion of the Java code editor in the debug perspective allows us to take advantage of another JPDA featurehot swapping.
The hot swapping feature was introduced with the release of JDK 1.4 and enables code to be modified from within a debugger while executing under a JVM.
Hot swapping technology makes it possible to apply changes quickly, bypassing the normal build-and-deployment cycle, and thereby allowing for a swift resolution to defects within the code.
There are limits, however, to the extent to which changes can be made. The main rule is that the structure of the method cannot be changed, so the method signature, including the return type, is immutable if hot swapping is to work. This type of functionality is also called fix-and-continue debugging.
Hot swapping is implicitly supported by Eclipse and can be used to fix-and-continue code running under a J2EE server that is executing under a JPDA-complaint JVM with dynamic class-loading support. Hot swapping is a feature of the JVM, not the J2EE server.
Following from the example shown in Figure 13-9, where a servlet running under Tomcat is being debugged, changes can be made to the servlet using the open code editor in the debug perspective.
Using the MyEclipse plug-in, the Tomcat server is launched as a remote Java application, and the debugger is attached. Having deployed a Web application containing a simple servlet, a breakpoint can be set in the Java editor. Note the breakpoint does not need to be set in the debug perspective. It can be set in any perspective with the source open in the Java editor. Eclipse automatically switches to the debug perspective when the breakpoint is triggered.
Figure 13-10 shows the presence of the breakpoint annotation in the code editor. The breakpoint can be set by double-clicking on the left edge of the editor, on the line where execution is to be interrupted. Note a mouse-over tool tip is visible in the screenshot to denote the location of the breakpoint.
Figure 13-10. Servlet code with breakpoint set.
With the Tomcat server started from the Eclipse workbench, the Web application deployed, and the breakpoint set, the page associated with the servlet can be accessed from a Web browser.
Loading the page in the browser triggers the breakpoint, and the Eclipse debugger is activated. Within the body of the method, any line of code under or ahead of the breakpoint can be changed. Once a modification has been made, the action of saving the file has Eclipse load the changed class into the target VM. Resuming the execution of the application from the debugger executes the changed code and the changes are immediately visible in the browser.
Hot swapping should not be confused with hot deployment. Hot deployment refers to the ability of a J2EE server to reload updated parts of an application dynamically, without the need to shutdown or interrupt the running application. Hot swapping is a feature of a JPDA-compliant JVM, not the J2EE server.
Earlier, we discussed the need for an IDE to support various file types in order to be suitable for J2EE development. The ability to work with a variety of file types applies equally to the debugger as to the code editors.
Under JDK 1.4, JPDA has been extended to support the debugging of languages other than Java. This ability is addressed by JSR-045, Debugging Support for Other Languages.
JSPs benefit greatly from this advancement, making it possible to set breakpoints and inspect the running state of a JSP as if it were Java code.
A plug-in such as MyEclipse is required to support this functionality on the Eclipse workbench. However, the process of running a JSP under the debugger is identical to that of debugging Java components such as servlets and Enterprise JavaBeans.
This section offers some suggestions to help you get the most out of the debugger. The techniques described all rely on standard JPDA features found in most debugging tools.
Check All New Code with the Debugger
A debugger isn't just a diagnostic tool for locating defects; it can also help improve the quality of your software. In his book Code Complete [McConnell, 2004], Steve McConnell recommends the approach of debugging all new code before it is placed under source control.
This practice allows the developer to confirm the code is operating as designed, rather than passing tests on pure luck. For example, a common code error is to have a loop statement either perform an extra iteration or skip a final iteration. You can verify the correct termination of the loop by using the debugger to step through the loop construct while monitoring the state of the count variable.
Set Conditional Breakpoints
A conditional breakpoint is triggered when an expression associated with the breakpoint evaluates to true. This is a common feature of most debuggers and is especially useful when tracking down problems that do not occur on the first run through of the code.
In the example of an error you suspect is occurring on exiting a loop, rather than stepping through every iteration of the loop, have the breakpoint triggered by providing an expression for the breakpoint that sees the debugger interrupt execution when the loop's exit condition is reached.
Conditional breakpoints are set in Eclipse by right-clicking on the breakpoint and selecting its properties from the context menu. You can set a condition for the breakpoint in the resulting properties window.
Use the Watch Functionality to Monitor Specific Variables
The debugger can display so much information about the state of the executing program, it becomes difficult to monitor those variables that are of interest. In this situation, using the watch functionality enables the placing of specific variables in a separate view where they are easily observed.
To do this in Eclipse, with execution of the program suspended in the debugger, highlight the variable and right-click to bring up its context menu. Selecting Watch from the menu adds the variable to the Expressions View.
Modify the State of Variables for Testing Purposes
With execution of the program suspended at a breakpoint, you can use the debugger to change the value of a variable. You can use this feature to turn the debugger into a testing tool by setting a breakpoint on entry into a method and then populating the method's parameters with test data.
Modifying the program's internal state at runtime is useful for test scenarios for which test data is not readily available.
To modify a variable in the Eclipse debugger, double-click on the variable in the Variables View. This displays a dialog box from which you can enter a new value.
Explore the Code by Stepping Into and Out of Methods
In addition to stepping through code line by line, you can step into a method. This approach allows you to burrow down into the depths of the application to determine its inner workings. The approach is particularly useful when exploring other people's code, as would be the case on an XP project where code ownership is discouraged.
When you have gone deep enough into the code base, to step back out, use the Step Return debug menu command. This returns you to the calling method and places you at the line of code immediately following the call into which you initially stepped. This is a handy feature when you don't want to step through to the end of a method before returning.