How Spring Debugger Enables Agent‑Free Remote Debugging of Java Apps

The article explains how JetBrains' Spring Debugger plugin provides remote debugging for Spring applications without using Java agents, detailing the underlying thread‑model tricks, supported containers, practical features like bean inspection and SQL execution, and step‑by‑step JDWP configuration.

Java Architecture Diary
Java Architecture Diary
Java Architecture Diary
How Spring Debugger Enables Agent‑Free Remote Debugging of Java Apps

Background

JetBrains released the Spring Debugger plugin in September 2025. The community asked whether the plugin could debug remote Spring applications, which led to a design that avoids using Java debug agents.

Java agents

In the Java ecosystem an agent is a piece of code attached to the JVM at startup or runtime, typically via the -javaagent flag:

java -javaagent:/path/to/agent.jar -jar your-app.jar

Agents enable byte‑code manipulation, performance monitoring, etc., but they have drawbacks:

Complex JVM startup parameters and the need to specify the agent JAR path.

Version‑matching requirements between the agent, application, and JVM.

Potential interference with normal debugging because the agent modifies bytecode.

Remote debugging without an agent

Local debugging can attach a breakpoint during Spring context initialization, but in a remote scenario the JVM is already running, so there is no opportunity to inject an agent and capture the ApplicationContext. The solution relies on the thread‑pool models of embedded servlet containers.

Container thread models

Tomcat

Tomcat creates a worker thread pool at server startup. Idle worker threads are always present, allowing Spring Debugger to seize a free worker thread after the application has started and read the ApplicationContext immediately, without waiting for an HTTP request.

Jetty and Undertow

Both Jetty and Undertow separate I/O threads from worker threads. Worker threads are created lazily—only when the first request arrives. Consequently, Spring Debugger cannot load the context until the first HTTP request triggers worker‑thread creation.

Features available once the Spring context is obtained

Access any bean : Enter a bean name (e.g., categoryRepository) in the expression window; the plugin retrieves the bean from the context, allowing direct method invocation.

categoryRepository.deleteAll();
categoryRepository.findAll();

Execute arbitrary SQL : Run JPQL or native queries directly from the debugger.

entityManager.createQuery("SELECT c FROM Category c").getResultList();

Read configuration properties : Query runtime values, e.g.,

environment.getProperty("spring.datasource.url");

Publish events manually : Trigger custom Spring events for testing listeners.

applicationEventPublisher.publishEvent(new CustomEvent("test"));

Transaction inspection : While paused inside a transaction, view isolation level, propagation status, cached entities, and entity states (MANAGED, DETACHED, etc.) via the transaction node.

Runtime bean information : "Show runtime info" reveals which classes inject a bean and its dependencies, useful for navigating legacy code.

Effective property values : The plugin shows the actual runtime value of properties defined in .properties files and links to all definition sources.

server.port=8080  # actual: 8081 (overridden by env)

Remote debugging configuration

The setup follows the standard JDWP remote‑debugging pattern. Add JDWP options to the JVM (for example, in a Docker‑Compose file):

http-server:
  image: 'jb/http-server:latest'
  environment:
    - SPRING_DATASOURCE_URL=jdbc:postgresql://postgresql:5432/db
    - SPRING_DATASOURCE_USERNAME=user
    - SPRING_DATASOURCE_PASSWORD=secret
    - JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
  ports:
    - '8080:8080'
    - '5005:5005'

The crucial line is:

JAVA_TOOL_OPTIONS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
server=y

: JVM acts as a debug server waiting for connections. suspend=n: Do not pause the JVM on start (set to y to wait for a debugger). address=*:5005: Listen on port 5005 on all network interfaces.

In IntelliJ IDEA create a Remote JVM Debug configuration, specify the target IP and port, and set the module classpath so the IDE can locate source files. After connecting:

Tomcat provides the Spring context immediately.

Jetty/Undertow require a first HTTP request to load the context.

Once the context is available you can set breakpoints, evaluate expressions, inspect properties, and analyze transactions exactly as with local debugging.

Limitations

Supported only for embedded containers (Tomcat, Jetty, Undertow). Stand‑alone WAR deployments are not covered.

The remote debug configuration must specify the module classpath; otherwise source code is invisible.

The database structure view is unavailable – connections are shown but table schemas are not.

Key benefits

Zero extra configuration: only a standard JDWP port is required.

No maintenance burden: no agents to version or keep compatible.

Non‑intrusive: the application’s runtime behavior remains unchanged.

Spring Debugger UI
Spring Debugger UI
Bean inspection
Bean inspection
Transaction debugging
Transaction debugging
Runtime bean info
Runtime bean info
Effective property values
Effective property values
Docker Compose JDWP setup
Docker Compose JDWP setup
JavaSpringtomcatremote debuggingIDE plugin
Java Architecture Diary
Written by

Java Architecture Diary

Committed to sharing original, high‑quality technical articles; no fluff or promotional content.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.