Debugging with the JPDA

 < Free Open Study > 



Let's turn our attention to the Java Platform Debugger Architecture (JPDA). The JPDA offers an alternative method that we can use to capture debugging information. We need the JPDA to write a debugger that's not limited to web applications and that can debug remotely across a network.

start sidebar

The Java Platform Debugger Architecture provides a standard set of protocols and APIs at three levels that facilitate the construction of (remote) debugging and profiling tools.

end sidebar

The JPDA consists of three parts:

  • The Java Virtual Machine Debug Interface (JVMDI) - the entry point to the target Java Virtual Machine (JVM)

  • The Java Debug Wire Protocol (JDWP) - an inter-process communication protocol

  • The Java Debug Interface (JDI) - the front-end API

Any application that we want to debug must be running in a JVM that supports the JVMDI (such a JVM has been available since J2SE version 1.3 on all platforms).

Interesting events such as method invocations and variable modifications are propagated, via the JDWP, from the JVMDI to any front-end debugger that is listening. The JDI hides the details of the JVMDI and JDWP behind a set of Java interfaces that simplify the task of writing a debugger front end.

We could actually use the JDWP to write a debugger in a language other than Java; and as a wire protocol it supports remote debugging across a network.

The following diagram shows how the various parts of the JPDA fit together:

Implementing the JPDA Debugger

We'll implement a front-end to the JPDA that we can attach to any application. The JVM that the application is running within does not have to be the same JVM as the debugger is running on. The JVMs could even be running on different networks or operating systems.

We'll implement a single class, JPDADebugger, which will listen for method entry events from the remote JVM. JPDADebugger will perform a similar function to DebugFilter, but is not limited to only trapping the service methods of a servlet.

This is the implementation of JPDADebugger. To compile this class we need to include %JAVA_HOME%/lib/tools.jar in the classpath:

    package debugging;    import com.sun.jdi.*;    import com.sun.jdi.event.*;    import com.sun.jdi.connect.*;    import com.sun.jdi.request.*;    import java.util.*;    public class JPDADebugger {      private static int SOCKET_ATTACH = 1;      private VirtualMachine vm = null;      private boolean running; 

Our entry point for this class will be the main() method, which takes two command-line arguments. The first allows the location of the target VM to be specified:

    -attach targetMachine:targetPort 

The second allows us to specify the subset of classes that we want to watch, so that we're not overwhelmed by method entry events:

    -include apackage.*:apackage.apackage.* 

The main() method parses these command-line arguments, creates an instance of JPDADebugger, and sets it running with the supplied arguments:

      public static void main(String args[]) {        String attachAddress = null;        Vector includeClasses = new Vector();        for (int i = 0; i < args.length; i++) {          String thisArg = args[i];          if (thisArg.equals("-attach")) {            attachAddress = args[++i];          } else if (thisArg.equals("-include")) {            String incString = args[++i];            StringTokenizer st = new StringTokenizer(incString,":",false);            while (st.hasMoreTokens()) {              String thisOne = st.nextToken();              System.out.println("including " + thisOne);              includeClasses.addElement(thisOne);            }          }        }        JPDADebugger thisDebugger = new JPDADebugger();        try {          thisDebugger.execute(attachAddress, includeClasses);        } catch (Exception ex) {          ex.printStackTrace();        }      } 

Next we create an execute() method, in which we we attach to the remote VM and go into a loop while we wait for debug events:

    public void execute(String attachAddress, Vector includeClasses) {      if (includeClasses == null) {        includeClasses = new Vector();      } 

Our connection to the remote virtual machine will be defined by four values. Here we initialize them to default values that will be overwritten by the values supplied on the command line:

      String connectorName = null;      int connectType = -1;      String connectHost = null;      String connectPort = null; 

The JPDA supports various techniques for connecting to an application running in a target VM, so the following if statement could be extended with else clauses that correspond to the different connection techniques. For example, the user could provide the name of the main class of an application to be debugged (and launched automatically) rather than the network address of an already-running virtual machine. If our class did support various connection methods we'd want to know which one had succeeded, which is why the connectType variable is set to the constant SOCKET_ATTACH:

      if (attachAddress != null) {        connectorName = "com.sun.jdi.SocketAttach";        connectType = SOCKET_ATTACH;        int index = attachAddress.indexOf(":");        connectHost = attachAddress.substring(0,index);        connectPort = attachAddress.substring(index + 1);      } else {        throw new Exception ("ERROR: No attach address specified");      } 

Our target application will be the servlet container, which will most likely be running on a remote machine, possibly on a different operating system, and with no possibility of us launching it on demand. This leaves the socket attaching connector as the only suitable connector:

      Connector connector = null;      List connectors = Bootstrap.virtualMachineManager().allConnectors();      Iterator iter = connectors.iterator();      while (iter.hasNext()) {        Connector thisConnector = (Connector)iter.next();        if (thisConnector.name().equals(connectorName))          connector = thisConnector;      } 

If we didn't find a connector we throw an exception:

      if (connector == null) {        throw new Exception("ERROR: No connector with name " + connectorName);      } 

Set the host and port arguments of the connector:

      Map arguments = connector.defaultArguments();      Connector.Argument hostname = (Connector.Argument)arguments.get("hostname");      Connector.Argument port = (Connector.Argument)arguments.get("port");      hostname.setValue(connectHost);      port.setValue(connectPort); 

Cast the connector to an AttachingConnector and try to attach to the remote VM:

      AttachingConnector attacher = (AttachingConnector) connector;      vm = null;      try {        vm = attacher.attach(arguments);      } catch (Exception e) {        e.printStackTrace();        throw new Exception ("ERROR: " + e + "@ attempting socket attach.");      } 

Throw an exception if we can't get hold of the remote VM:

      if (vm == null) {        throw new Exception("ERROR: No VM process connected.");      } 

Now that we've connected to the remote JVM we can register our interest in certain method invocations. We get the JVM's EventRequestManager and add a MethodEntryRequest with a filter for each included class. We'll be trapping events relating to a subset of the classes that comprise the target application. Not only does this aid our comprehension of the results, but it also helps not to overload the target JVM:

      EventRequestManager em = v m.eventRequestManager();      for (Enumeration e = includeClasses.elements(); e.hasMoreElements();) {        MethodEntryRequest meR = em.createMethodEntryRequest();        String pattern = (String) e.nextElement();        meR.addClassFilter(pattern);        meR.enable();      } 

We get hold of the JVM event queue. The target JVM places events corresponding with our method entry requests onto a queue as they occur. We repeatedly poll this queue in order to obtain sets of events that we've not yet processed:

      EventQueue eventQ = vm.eventQueue();      running = true;      while (running) {        EventSet eventSet = null;        try {          eventSet = eventQ.remove();        } catch (Exception e) {          System.err.println("ERROR: Interrupted Event Loop");          e.printStackTrace();        } 

We step through the events and process each one. For each set of events that we pop from the queue, we call our processMethodEntryEvent() method to log them:

        EventIterator eventIterator = eventSet.eventIterator();        while (eventIterator.hasNext()) {          Event event = eventIterator.nextEvent();          if (event instanceof MethodEntryEvent) {            processMethodEntryEvent((MethodEntryEvent)event);          }        } 

Finally, we tell the target JVM to resume. Not only do we listen in to the remote JVM, but we can also control it. So, once we've processed each set of events, we restart the suspended JVM:

        vm.resume();      }    } 

For each event that we receive, the processMethodEntryEvent() method is invoked. For each MethodEntryEvent, we discover and record the name of the method that was called, the thread on which it occurred, and the names of the caller and called objects:

    private void processMethodEntryEvent(MethodEntryEvent event) {      String methodString = event.method().toString();      ThreadReference thread = event.thread(); 

Get hold of the stack frames for this thread:

      List stackList = null;      try { stackList = thread.frames(); }      catch (Exception e) { return; } 

Initialize the caller and callee information:

      String calleeID = "?";      String calleeClass = "?";      String callerID = "?";      String callerClass = "?"; 

To discover the sender and receiver of the message, the caller and called objects, we'll be looking on the thread's stack frame. The topmost item will be the object that received the message, and the next item will be the object that sent the message:

      int level = 0;      for (Iterator it = stackList.iterator(); it.hasNext();) {        StackFrame stackFrame = (StackFrame) it.next();        ObjectReference thisObject = stackFrame.thisObject();        if (thisObject == null) {          continue;        }        if (level == 0)          calleeID = String.valueOf(thisObject.uniqueID());          String classString = thisObject.referenceType().toString();          StringTokenizer st = new StringTokenizer(classString," ");          calleeClass = st.nextToken();          calleeClass = st.nextToken();        } else if (level == 1) {          callerID=String.valueOf(thisObject.uniqueID());          String classString = thisObject.referenceType().toString();          StringTokenizer st = new StringTokenizer(classString," ");          callerClass = st.nextToken();          callerClass = st.nextToken();        }        level++;        if (level > 1) {          break;        }      } 

To be consitent with the output from our filter and the event listener we print the result with tags:

      System.out.println("<invocation><sender>" + callerID + ":" + callerClass +                         "</sender><message>" + methodString +                         "</message><receiver>" +                         calleeID + ":" + calleeClass + "</receiver><thread>" +                         thread.name() + "</thread></invocation>");    } 

That completes JPDADebugger, which we can now use to trace the progress of any Java application. However, before we can run our debugger we need to give it something to attach to.

Running the Server in Debug Mode

We'll be using JPDADebugger to inspect the servlet container as it runs. Before we can do this we need to make the container available to our debugger by running the server in debug mode.

Whatever servlet container you're using, there will be a file that starts the server via a Java command. For Tomcat, you should make the following addition to the catalina script in the %CATALINA_HOME%\bin directory:

    %_STARTJAVA%    -Xdebug -Xnoagent -Xrunjdwp:transport=dt_socket,server=y,address=8124,suspend=n    %CATALINA_OPTS% -Dcatalina.home="%CATALINA_HOME%"    org.apache.catalina.startup.Bootstrap %2 %3 %4 %5 %6 %7 %8 %9 start 

If you're using a different servlet container, you'll need to make a similar change to the command that sets it running. So that you know what you're doing, here's an explanation of the options we use with Tomcat:

  • -Xdebug and -Xnoagent

    Tells the JVM to run in debug mode but not to use the debugging agent for the legacy Java debugger, jdb

  • -Xrunjdwp:transport=dt_socket,server=y,address=8124,suspend=n

    Tells the JVM to listen for socket connections on port 8124 (which was chosen at random), to act as a server for events, and not to suspend its execution before a connection is made

You can now start Tomcat using the catalina script:

    catalina start 

With the target server running, we can run our JPDA debugger.

Using the JPDA Debugger

Remember to include %JAVA_HOME%/lib/tools.jar in your classpath. Then run the debugger with the following command:

    java debugging.JPDADebugger -attach localhost:8124 -include    SessionExample:javax.servlet.http.* > debug.txt 

Recall the command-line arguments processed by the main() method:

  • The first instructs our debugger to attach to the appropriate port on the relevant machine. We set this port number when we modified the catalina script.

  • The second tells the debugger to listen in on events relating to certain classes, in this case the SessionExample servlet and all classes in the javax.servlet.http.* package. If you want to include all classes in the debug trace, you can specify '*' on the command line. (The quotes are needed to prevent the command interpreter from replacing the * symbol before passing it as a parameter to the Java program.)

The output is redirected to a text file (debug.txt) because as we're running our debugger outside the servlet container, we have no server log file to write to. If you want to see the events in real-time simply remove the file redirection from the command line and watch the console output.

Output from the JPDA Debugger

Once we've started Tomcat in debug mode, and started the debugger, we can test it by running one of the example servlets. Once again, I accessed http://localhost:8080/examples/servlet/SessionExample and saw the following output from the debugger:

    <invocation>      <sender>10:org.apache.catalina.connector.http.HttpProcessor</sender>      <message>        javax.servlet.http.Cookie.<init>(java.lang.String, java.lang.String)      </message>      <receiver>7:javax.servlet.http.Cookie</receiver>      <thread>HttpProcessor[8080][4]      </thread>    </invocation>    <invocation>      <sender>7:javax.servlet.http.Cookie</sender>      <message>javax.servlet.http.Cookie.isToken(java.lang.String)</message>      <receiver>7:javax.servlet.http.Cookie</receiver>      <thread>HttpProcessor[8080][4]</thread>    </invocation>    <invocation>      <sender>10:org.apache.catalina.connector.http.HttpProcessor</sender>      <message>javax.servlet.http.Cookie.getName()</message>      <receiver>7:javax.servlet.http.Cookie</receiver>      <thread>HttpProcessor[8080][4]</thread>    </invocation>    <invocation>      <sender>10:org.apache.catalina.connector.http.HttpProcessor</sender>      <message>javax.servlet.http.Cookie.getValue()</message>      <receiver>7:javax.servlet.http.Cookie</receiver>      <thread>HttpProcessor[8080][4]</thread>    </invocation>    <invocation>      <sender>30:org.apache.catalina.core.ApplicationFilterChain</sender>      <message>        javax.servlet.http.HttpServlet.service(javax.servlet.ServletRequest,        javax.servlet.ServletResponse)      </message>      <receiver>26:SessionExample</receiver>      <thread>HttpProcessor[8080][4]</thread>    </invocation> 

This output contains a lot of useful information but it's not too pleasant to look at. We'll do something about that now.



 < Free Open Study > 



Professional Java Servlets 2.3
Professional Java Servlets 2.3
ISBN: 186100561X
EAN: 2147483647
Year: 2006
Pages: 130

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net