jdb is the Java source debugger. It is not the most powerful source debugger around, but it is certainly better than nothing. Here is a trace of a jdb session with annotations:
First, make sure that you compile all your code with the -g option. This ensures that debugging information will be put in the .class files.
Next invoke the debugger on the program you want to debug. Notice that in this case I start on the Acme server, although I want to debug my own servlet.
gaelic 3/home/keane/cs336/edu/rutgers/keane/cs336>javac -g *.java
gaelic 3/home/keane/cs336/edu/rutgers/keane/cs336>jdb Acme.Serve.Serve
Initializing jdb...
0xb0:class(Acme.Serve.Serve)
The ? command will list everything jdb will do. The commands you will use most are:
> ?
** command list **
threads [threadgroup]     -- list threads
thread <thread id>        -- set default thread
suspend [thread id(s)]    -- suspend threads (default: all)
resume [thread id(s)]     -- resume threads (default: all)
where [thread id] | all   -- dump a thread's stack
wherei [thread id] | all  -- dump a thread's stack, with pc info
threadgroups              -- list threadgroups
threadgroup <name>        -- set current threadgroup

print <id> [id(s)]        -- print object or field
dump <id> [id(s)]         -- print all object information

locals                    -- print all local variables in current stack frame

classes                   -- list currently known classes
methods <class id>        -- list a class's methods

stop in <class id>.<method>[(argument_type,...)] -- set a breakpoint in a method
stop at <class id>:<line> -- set a breakpoint at a line
up [n frames]             -- move up a thread's stack
down [n frames]           -- move down a thread's stack
clear <class id>.<method>[(argument_type,...)]   -- clear a breakpoint in a method
clear <class id>:<line>   -- clear a breakpoint at a line
step                      -- execute current line
step up                   -- execute until the current method returns to its caller
stepi                     -- execute current instruction
next                      -- step one line (step OVER calls)
cont                      -- continue execution from breakpoint

catch <class id>          -- break for the specified exception
ignore <class id>         -- ignore when the specified exception

list [line number|method] -- print source code
use [source file path]    -- display or change the source path

memory                    -- report memory usage
gc                        -- free unused objects

load classname            -- load Java class to be debugged
run <class> [args]        -- start execution of a loaded Java class
!!                        -- repeat last command
help (or ?)               -- list commands
exit (or quit)            -- exit debugger
Let's see what classes were automatically loaded with Acme.Serve.Serve:
> classes
** classes list **
0x7:class(java.lang.NoClassDefFoundError)
0x3:class(java.lang.Class)
0x2:class(java.lang.Object)
0x8:class(java.lang.Throwable)
0x9:interface(java.io.Serializable)
0x4:class(java.lang.String)
0xa:interface(java.lang.Comparable)

   :
   Goes on for pages...
   :

0xd3:class(java.lang.RuntimePermission)
0xb0:class(Acme.Serve.Serve)
0xb2:interface(Acme.Serve.servlet.ServletContext)
We can also find out what methods are known in a class:
> methods edu.rutgers.keane.cs336.Login
void <init>()
void service(Acme.Serve.servlet.http.HttpServletRequest, Acme.Serve.servlet.http.HttpServletResponse)
> methods edu.rutgers.keane.cs336.Session
void <clinit>()
void <init>()
java.util.Random access$0()
java.lang.String getSession(java.lang.String)
java.lang.String makeSession(java.lang.String)
Now I set a breakpoint at the beginning of the Login.service() method. I want the debugger to stop as soon as it starts to process a user login:
> stop in edu.rutgers.keane.cs336.Login.service
Breakpoint set in edu.rutgers.keane.cs336.Login.service
Now I'll start running the server. After I start it, I will bring up the login form on my brower (you'll have to use your imagination here...), enter my userid and password and click enter. As soon as I do, the debugger stops at the beginning of the Login.service() method:
> run Acme.Serve.Serve -p 1234
running ...
main[1] [Thu Mar 01 11:09:31 EST 2001] getting /login.html

Breakpoint hit: edu.rutgers.keane.cs336.Login.service (Login:56)
OK. Let's see what variables are in scope at entry:
Thread-2[1] locals
Method arguments:
Local variables:
  this = edu.rutgers.keane.cs336.Login@5cbc1b49
  req = Acme.Serve.ServeConnection@a2dc1b4e
  res = Acme.Serve.ServeConnection@a2dc1b4e
First I step to the next line. Then I try to list the source, but jdb is confused:
Thread-2[1] next
Thread-2[1] 
Breakpoint hit: edu.rutgers.keane.cs336.Login.service (Login:63)
Thread-2[1] list
Unable to find Login.java
The problem is that jdb wants to use the class name as the path to the source. Since I'm debugging the class edu.rutgers.keane.cs336.Login, it wants to look for the file (starting from the current directory) edu/rutgers/keane/cs336/Login.java. However, I started jdb in edu/rutgers/keane/cs336, so it can't find the file. I need to tell jdb to start its search from the root of the classpath:
Thread-2[1] use /home/keane/cs336
Thread-2[1] list
59	           }
60	   
61	           /* Get the parameters for the request */
62	           try {
63	=>             if ((parms = HTMLHelper.getPostParms(req)) == null) {
64	                   res.sendError( HttpServletResponse.SC_BAD_REQUEST );
65	                   return;
66	               }
67	           } catch (IOException e) {
Step a few more times...
Thread-2[1] next
Thread-2[1] 
Breakpoint hit: edu.rutgers.keane.cs336.Login.service (Login:82)
Thread-2[1] next
Thread-2[1] 
Breakpoint hit: edu.rutgers.keane.cs336.Login.service (Login:83)
Thread-2[1] list
79	   //              System.out.println("Parameter: '" + key + "' has value '" + value + "'");
80	   //          }
81	           
82	           String userid = (String) parms.get("userid");
83	=>         String password = (String) parms.get("password");
84	   
85	   	res.setStatus( HttpServletResponse.SC_OK );
86	   	res.setContentType( "text/html" );
87	   	ServletOutputStream p = res.getOutputStream();
Thread-2[1] next
Thread-2[1] 
Breakpoint hit: edu.rutgers.keane.cs336.Login.service (Login:85)
Now that we've gone a little farther, let's see what new variables have come into scope:
Thread-2[1] locals
Method arguments:
Local variables:
  this = edu.rutgers.keane.cs336.Login@5cbc1b49
  req = Acme.Serve.ServeConnection@a2dc1b4e
  res = Acme.Serve.ServeConnection@a2dc1b4e
  parms = {password=foobar, userid=123456789}
  userid = 123456789
  password = foobar
This is boring. I want to set another breakpoint several lines on, at line 87. Then I cont to resume execution until the breakpoint is hit:
Thread-2[1] stop at edu.rutgers.keane.cs336.Login:87
Breakpoint set at edu.rutgers.keane.cs336.Login:87
Thread-2[1] cont
Thread-2[1] 
Breakpoint hit: edu.rutgers.keane.cs336.Login.service (Login:87)
Thread-2[1] list
83	           String password = (String) parms.get("password");
84	   
85	   	res.setStatus( HttpServletResponse.SC_OK );
86	   	res.setContentType( "text/html" );
87	=> 	ServletOutputStream p = res.getOutputStream();
88	   
89	           /* Read and set database system properties*/
90	           try {
91	               PropInit.readfile("gradedb.javaprop");
I want to leave this function and stop when we get back to the caller:
Thread-2[1] step up
Thread-2[1] 
Breakpoint hit: Acme.Serve.ServeConnection.runServlet (ServeConnection:645)
Let's go up and down the stack and see what lives in each frame:
Thread-2[1] where
  [1] Acme.Serve.ServeConnection.runServlet (ServeConnection:645)
  [2] Acme.Serve.ServeConnection.parseRequest (ServeConnection:629)
  [3] Acme.Serve.ServeConnection.run (ServeConnection:540)
  [4] java.lang.Thread.run (Thread:479)
Thread-2[1] locals
Method arguments:
Local variables:
  this = Acme.Serve.ServeConnection@a2dc1b4e
  servlet = edu.rutgers.keane.cs336.Login@5cbc1b49
Thread-2[1] up
Thread-2[2] locals
Method arguments:
Local variables:
  this = Acme.Serve.ServeConnection@a2dc1b4e
  lineBytes = { 0x43, 0x6f, 0x6e, ... }
  len = 0
  line = Content-length: 32
  tokens = { POST, /Login, HTTP/1.0 }
  qmark = -1
  servlet = edu.rutgers.keane.cs336.Login@5cbc1b49
Thread-2[2] up
Thread-2[3] locals
Method arguments:
Local variables:
  this = Acme.Serve.ServeConnection@a2dc1b4e
We can also show the source at each level as we go back down the stack:
Thread-2[3] list
536	   	    {
537	   	    problem( "Getting streams: " + e.getMessage(), SC_BAD_REQUEST );
538	   	    }
539	   
540	=> 	parseRequest();
541	   
542	   	try
543	   	    {
544	   	    socket.close();
Thread-2[3] down
Thread-2[2] list
625	   	    reqUriPath = decode( reqUriPath );
626	   
627	   	    Servlet servlet = (Servlet) serve.registry.get( reqUriPath );
628	   	    if ( servlet != null )
629	=> 		runServlet( (HttpServlet) servlet );
630	   	    }
631	   	catch ( IOException e )
632	   	    {
633	   	    problem( "Reading request: " + e.getMessage(), SC_BAD_REQUEST );
Thread-2[2] down
Thread-2[1] list
641	   	setDateHeader( "Date", System.currentTimeMillis() );
642	   	setHeader(
643	   	    "Server", ServeUtils.serverName + "/" + ServeUtils.serverVersion );
644	   	setHeader( "Connection", "close" );
645	=> 	try
646	   	    {
647	   	    servlet.service( this, this );
648	   	    }
649	   	catch ( IOException e )
Thread-2[1] down
End of stack.
That's it! Now I just cont and the server returns to running:
Thread-2[1] cont
Thread-2[1]