<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sebastian Aburto</title>
    <description>The latest articles on DEV Community by Sebastian Aburto (@saburto).</description>
    <link>https://dev.to/saburto</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F391316%2F6d3dd4f0-9e6a-43a7-bd1d-b4c144f8dab3.jpeg</url>
      <title>DEV Community: Sebastian Aburto</title>
      <link>https://dev.to/saburto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/saburto"/>
    <language>en</language>
    <item>
      <title>Java Command-Line Debugging with AI Agent</title>
      <dc:creator>Sebastian Aburto</dc:creator>
      <pubDate>Sun, 07 Jun 2026 10:36:46 +0000</pubDate>
      <link>https://dev.to/saburto/java-command-line-debugging-with-ai-agent-4c4m</link>
      <guid>https://dev.to/saburto/java-command-line-debugging-with-ai-agent-4c4m</guid>
      <description>&lt;p&gt;When you need to track down a complex bug, your first instinct is to reach for a visual debugger. You set a breakpoint, step through the logic, and inspect the stack trace. But in an era of agentic coding, IDEs and visual debuggers are a bottleneck.&lt;/p&gt;

&lt;p&gt;IDEs and their visual debuggers in the agentic coding era are not an option anymore 🙅 &lt;/p&gt;

&lt;p&gt;The best next option is adding some logs as breadcrumbs to try to understand what is happening&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;processPayment&lt;/span&gt;&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Entering processPayment()"&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;isValid&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;System&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing valid payment: "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;paymentId&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is a natural approach, but the feedback loop is slow in &lt;code&gt;Java&lt;/code&gt;. Each change means compiling, packaging, and running again, or at best, a hot-reload. You make many code modifications just to understand what happens, but those print statements only work in your own code.&lt;/p&gt;

&lt;p&gt;I need something my agent can use, the same way I use a visual debugger.&lt;/p&gt;

&lt;h2&gt;
  
  
  JDB: The built-in command-line debugger for Java
&lt;/h2&gt;

&lt;p&gt;I gave &lt;code&gt;jdb&lt;/code&gt; a try, the command-line debugger that was always there (since jdk 1).&lt;/p&gt;

&lt;p&gt;You can connect to a jvm that is running with the debugger options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;java &lt;span class="nt"&gt;-agentlib&lt;/span&gt;:jdwp&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;transport&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dt_socket,server&lt;span class="o"&gt;=&lt;/span&gt;y,suspend&lt;span class="o"&gt;=&lt;/span&gt;n,address&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;:5005 MyClass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you can connect using that port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jdb &lt;span class="nt"&gt;-attach&lt;/span&gt; 5005
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an interactive tool where you send commands like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add breakpoints: &lt;code&gt;stop at com.saburto.Bar:46&lt;/code&gt; or by method &lt;code&gt;stop at com.saburto.Bar.getAllLedgers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;steps: &lt;code&gt;step&lt;/code&gt;, &lt;code&gt;step up&lt;/code&gt;,  &lt;code&gt;stepi&lt;/code&gt;, &lt;code&gt;next&lt;/code&gt;, &lt;code&gt;cont&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;get variables info: &lt;code&gt;print&lt;/code&gt;, &lt;code&gt;dump&lt;/code&gt;, &lt;code&gt;eval&lt;/code&gt;, &lt;code&gt;locals&lt;/code&gt;, &lt;code&gt;set&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;threads: &lt;code&gt;where&lt;/code&gt;, &lt;code&gt;threadgroups&lt;/code&gt;, &lt;code&gt;up&lt;/code&gt;, &lt;code&gt;down&lt;/code&gt;, &lt;code&gt;kill&lt;/code&gt;, &lt;code&gt;interrupt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;show source code: &lt;code&gt;use&lt;/code&gt; to add the source code path, &lt;code&gt;list&lt;/code&gt; to show the code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The source code looks clean in the output of the coding agent&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;  &lt;span class="mi"&gt;42&lt;/span&gt;    &lt;span class="nd"&gt;@GetMapping&lt;/span&gt;
  &lt;span class="mi"&gt;43&lt;/span&gt;    &lt;span class="kd"&gt;public&lt;/span&gt; &lt;span class="nc"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;LedgerResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;getAllLedgers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="mi"&gt;44&lt;/span&gt;            &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;@RequestParam&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;defaultValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"20"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="mi"&gt;45&lt;/span&gt;
  &lt;span class="mi"&gt;46&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;     &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ledgers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ledgerService&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getAllLedgers&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;);&lt;/span&gt;
  &lt;span class="mi"&gt;47&lt;/span&gt;
  &lt;span class="mi"&gt;48&lt;/span&gt;        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="mi"&gt;49&lt;/span&gt;                &lt;span class="n"&gt;ledgers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getContent&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;().&lt;/span&gt;&lt;span class="na"&gt;map&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nl"&gt;mapper:&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="n"&gt;toLedgerResponse&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="na"&gt;toList&lt;/span&gt;&lt;span class="o"&gt;();&lt;/span&gt;
  &lt;span class="mi"&gt;50&lt;/span&gt;
  &lt;span class="mi"&gt;51&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;PageResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ledgers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalElements&lt;/span&gt;&lt;span class="o"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;ledgers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getTotalPages&lt;/span&gt;&lt;span class="o"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Let your agent know the tools
&lt;/h3&gt;

&lt;p&gt;Frontier models are intelligent enough to figure out how to call &lt;code&gt;jdb&lt;/code&gt;, but if you want to lend a hand, create a skill using &lt;code&gt;man jdb&lt;/code&gt; to document the key commands.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scenarios
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Dump a request payload&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Attach jdb to my app at port 5005, set a breakpoint at &lt;code&gt;LedgerController.getAllLedgers&lt;/code&gt;, trigger &lt;code&gt;curl localhost:8080/api/ledgers&lt;/code&gt;, and dump every field of the &lt;code&gt;ledgers&lt;/code&gt; Page object. I want to see the actual database records returned.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Trace a variable's mutations&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Set a breakpoint in &lt;code&gt;PaymentService.process&lt;/code&gt;, step through line by line with &lt;code&gt;next&lt;/code&gt;, and dump the &lt;code&gt;payment&lt;/code&gt; variable after each line. I want to see how the status field changes as it moves through validation, enrichment, and persistence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Inspect concurrency at a checkpoint&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Break at the top of my scheduled job in &lt;code&gt;ReconciliationScheduler.run&lt;/code&gt;, trigger a full thread dump with &lt;code&gt;where all&lt;/code&gt;, and show me what every other thread is doing at that moment. Any thread blocked on a lock?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Evaluate an expression in place&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Set a breakpoint at line 89 of &lt;code&gt;InvoiceCalculator&lt;/code&gt;, and when it hits, run &lt;code&gt;eval invoice.getLineItems().stream().mapToDouble(LineItem::getTotal).sum()&lt;/code&gt;. I want to verify the line-item math without adding a log line, rebuilding, and waiting for Maven.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Debug exception state without re-deploying&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Break inside the catch block of &lt;code&gt;PaymentGateway.submit&lt;/code&gt; (line 203), send a payment request with a bad card number, and when the breakpoint fires, dump the exception's message, cause chain, and the local variables. I want to see exactly what the gateway rejected and what state the request was in.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These prompts share a common shape: pick a breakpoint, trigger the code path, and let the agent extract the data. The agent handles the timing, the jdb commands, and the output parsing, and you get the answer in your terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Simple examples
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Checking variables in a request
&lt;/h3&gt;

&lt;p&gt;For a Java application, you need to start the system with the proper arguments: &lt;code&gt;java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;spring-boot&lt;/strong&gt; application using &lt;code&gt;mvn&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mvn spring-boot:run &lt;span class="nt"&gt;-Dspring-boot&lt;/span&gt;.run.jvmArguments&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:5005"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then connect by attaching to the port: &lt;code&gt;jdb -attach 5005&lt;/code&gt;, and that is it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jdb&lt;/code&gt; is interactive by design, but in an agentic workflow the session must run hands-off, with the HTTP request firing while the debugger is attached and waiting.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;[!WARNING]&lt;br&gt;
This is only an example of how my agent generated the script to use the debugger. Depending on your case it may be different; let your agent create the right script.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Because the agent cannot type commands interactively into a debugger prompt, we pipeline the commands through standard input instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/projects/my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /tmp/jdb-output.txt &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"stop in com.saburto.ledger.controller.LedgerController.getAllLedgers"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cont"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;5
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"next"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;1
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"dump ledgers.content.elementData"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"dump ledgers.content.elementData[0]"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;2
&lt;span class="o"&gt;)&lt;/span&gt; | &lt;span class="nb"&gt;timeout &lt;/span&gt;30 jdb &lt;span class="nt"&gt;-attach&lt;/span&gt; 5005 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/jdb-output.txt 2&amp;gt;&amp;amp;1 &amp;amp;
&lt;span class="nv"&gt;JDB_PID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;
&lt;span class="nb"&gt;sleep &lt;/span&gt;2
curl &lt;span class="nt"&gt;-s&lt;/span&gt; localhost:8080/api/v1/ledgers &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;./scripts/get-token.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null
&lt;span class="nb"&gt;wait&lt;/span&gt; &lt;span class="nv"&gt;$JDB_PID&lt;/span&gt; 2&amp;gt;/dev/null
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"JDB_EXIT=&lt;/span&gt;&lt;span class="nv"&gt;$?&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's break this down:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The &lt;code&gt;( echo ... echo ... )&lt;/code&gt; subshell pipes a sequence of &lt;code&gt;jdb&lt;/code&gt; commands into the debugger. Each command goes in at the right time: set the breakpoint, continue, wait 5 seconds for the HTTP request to arrive and hit the breakpoint, then step over the assignment and dump the ledger data.&lt;/li&gt;
&lt;li&gt;The whole thing runs in the &lt;strong&gt;background&lt;/strong&gt; (&lt;code&gt;&amp;amp;&lt;/code&gt;), with its PID captured.&lt;/li&gt;
&lt;li&gt;After a 2-second head start, &lt;code&gt;curl&lt;/code&gt; triggers the REST endpoint that runs &lt;code&gt;getAllLedgers()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The script then waits for jdb to finish and reports the exit code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The agent reads &lt;code&gt;/tmp/jdb-output.txt&lt;/code&gt; afterward.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;next&lt;/code&gt; command steps past the &lt;code&gt;var ledgers = ...&lt;/code&gt; assignment before the dumps run. The &lt;code&gt;sleep&lt;/code&gt; calls pace the commands so they arrive after the breakpoint fires. The agent reads the output file and reconstructs the data.&lt;/p&gt;

&lt;p&gt;Now we see each &lt;code&gt;LedgerEntity&lt;/code&gt; instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ledgers.content.elementData[0] = {
    id: instance of java.util.UUID(id=15654)
    name: "Basic Transactions Ledger"
    createdAt: instance of java.time.Instant(id=15670)
    isNew: false
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;[!TIP]&lt;br&gt;
Use &lt;strong&gt;&lt;code&gt;tmux&lt;/code&gt;&lt;/strong&gt; as the bridge between agent and debugger. Tell the agent to open a new panel or window in tmux and send all debugging commands there while keeping your coding agent open in the original pane. This is the pattern we use throughout this post, including the SQL extraction later, to keep the agent and &lt;code&gt;jdb&lt;/code&gt; running in parallel.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Printing the Header of the Http Request
&lt;/h3&gt;

&lt;p&gt;Here is an example. Without touching a single line of code, I asked the agent to show me the HTTP request headers hitting a controller method. After a few tries, it delivered exactly what I needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Request Headers at LedgerController.getAllLedgers() (line 46, bci=0)

 ┌───┬───────────────┬─────────────────────────────────────────────────────────────┐
 │ # │ Header        │ Value                                                       │
 ├───┼───────────────┼─────────────────────────────────────────────────────────────┤
 │ 0 │ Host          │ localhost:8080                                              │
 ├───┼───────────────┼─────────────────────────────────────────────────────────────┤
 │ 1 │ User-Agent    │ curl/8.5.0                                                  │
 ├───┼───────────────┼─────────────────────────────────────────────────────────────┤
 │ 2 │ Accept        │ */*                                                         │
 ├───┼───────────────┼─────────────────────────────────────────────────────────────┤
 │ 3 │ Authorization │ Bearer                                                      │
 │   │               │ eyJraWQiOiJkZWZhdWx0IiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ. │
 │   │               │ ..                                                          │
 └───┴───────────────┴─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  How I got there (wrapper chain)
&lt;/h4&gt;

&lt;p&gt;The request object at &lt;code&gt;DispatcherServlet.doDispatch&lt;/code&gt; frame 15 goes through 5 nested &lt;code&gt;.request&lt;/code&gt; fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SecurityContextHolderAwareRequestWrapper
  → HeaderWriterFilter$HeaderWriterRequest
    → (firewall wrapper)
      → RequestFacade
        → Request
          → coyoteRequest.headers  (MimeHeaders, count=4)
            → headers[0..3]        (MimeHeaderField with nameB/valueB MessageBytes)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No code changes, no rebuilds, no restart. Just the debugger, the agent, and a &lt;code&gt;tmux&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Advanced debugging: chasing the SQL
&lt;/h2&gt;

&lt;p&gt;Sometimes you need to dig deeper. I wanted to see the exact SQL that Spring Data JDBC sends, straight from the PostgreSQL wire. I started at the high level (&lt;code&gt;LedgerController.getAllLedgers&lt;/code&gt;) and worked down through the stack, trying breakpoints until the right one stuck:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌──────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────┬────────┐
│ Layer                                                │ Breakpoint tried                                                                   │ Result │
├──────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┼────────┤
│ java.sql.Connection.prepareStatement(String)         │ Failed — Connection is an interface, jdb can't break on interfaces                 │        │
├──────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┼────────┤
│ org.postgresql.jdbc.PgConnection.prepareStatement    │ Never hit — HikariCP proxies the connection                                        │        │
├──────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────┼────────┤
│ com.zaxxer.hikari.pool.ProxyPreparedStatement.&amp;lt;init&amp;gt; │ ✅ Hit — the Hikari wrapper constructor receives the real statement as an argument │        │
└──────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────┴────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The key breakpoint
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stop in com.zaxxer.hikari.pool.ProxyPreparedStatement.&amp;lt;init&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fires every time HikariCP creates a prepared statement. The constructor signature accepts a statement parameter, the real &lt;code&gt;PgPreparedStatement&lt;/code&gt; from the PostgreSQL driver.&lt;/p&gt;

&lt;h3&gt;
  
  
  Extract the SQL from the driver internals
&lt;/h3&gt;

&lt;p&gt;At the breakpoint, the statement variable is a &lt;code&gt;PgPreparedStatement&lt;/code&gt; which has a &lt;code&gt;preparedQuery&lt;/code&gt; field of type &lt;code&gt;CachedQuery&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight java"&gt;&lt;code&gt;&lt;span class="n"&gt;print&lt;/span&gt; &lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="na"&gt;preparedQuery&lt;/span&gt;
&lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nc"&gt;CachedQuery&lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="n"&gt;executeCount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="no"&gt;SELECT&lt;/span&gt; &lt;span class="s"&gt;"ledger_ledgers"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="s"&gt;"id"&lt;/span&gt; &lt;span class="no"&gt;AS&lt;/span&gt; &lt;span class="s"&gt;"id"&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="no"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isFunction&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;CachedQuery.toString()&lt;/code&gt; conveniently includes the full SQL string.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending commands to a tmux pane
&lt;/h3&gt;

&lt;p&gt;Since jdb ran interactively in a tmux pane, I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux send-keys &lt;span class="nt"&gt;-t&lt;/span&gt; 3 &lt;span class="s1"&gt;'print statement.preparedQuery'&lt;/span&gt; Enter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Summary of the chain
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl → Tomcat → Spring MVC → LedgerController.getAllLedgers()
  → LedgerService → PagingAndSortingRepository.findAll(Pageable)
    → Spring Data JDBC → JdbcTemplate
      → HikariCP → ProxyPreparedStatement.&amp;lt;init&amp;gt;(conn, PgPreparedStatement)
                                                   ↑
                                         statement.preparedQuery = the SQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Final Response
&lt;/h4&gt;

&lt;p&gt;This is what it looks like the final response:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;SQL Queries Executed for &lt;code&gt;GET /api/v1/ledgers&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;From the &lt;code&gt;ProxyPreparedStatement.&amp;lt;init&amp;gt;&lt;/code&gt; breakpoint on &lt;code&gt;PgPreparedStatement&lt;/code&gt;:&lt;/p&gt;
&lt;h5&gt;
  
  
  Query 1: COUNT (for pagination metadata)
&lt;/h5&gt;


&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"ledger_ledgers"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;em&gt;(Earlier runs captured this: Spring Data JDBC executes the count first to populate &lt;code&gt;PageImpl.totalElements&lt;/code&gt;)&lt;/em&gt;&lt;/p&gt;
&lt;h5&gt;
  
  
  Query 2: SELECT data (with LIMIT)
&lt;/h5&gt;


&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="nv"&gt;"ledger_ledgers"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"id"&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nv"&gt;"ledger_ledgers"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"name"&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="nv"&gt;"ledger_ledgers"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;"created_at"&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="nv"&gt;"created_at"&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="nv"&gt;"ledger_ledgers"&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Agentic debugging is powerful. You get deep insights into your Java system's state in seconds, entirely hands-off. The days of manually hitting F10 to step through line by line are behind us.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;jdb&lt;/code&gt; is just for &lt;code&gt;Java&lt;/code&gt;, the same pattern applies to &lt;code&gt;gdb&lt;/code&gt; for C/C++, &lt;code&gt;pdb&lt;/code&gt; for Python, &lt;code&gt;dlv&lt;/code&gt; for Go, or any debugger that exposes a command interface. If you can use in a terminal, the agent can use it.&lt;/p&gt;

&lt;p&gt;So, do you still prefer hitting F9, F10, F11 to step through your code?&lt;/p&gt;

</description>
      <category>java</category>
      <category>ai</category>
      <category>code</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
