Java Remote Debugging with IntelliJ
Java is one of the most widely used programming languages because of its principle of “compile once, run anywhere.” Its many syntax constraints, though, mean that writing code in a basic text editor can become tedious. You can use an integrated development environment (IDE) like IntelliJ IDEA to improve your output, with features like compile-time error suggestions and intelligent code completion. One of the most helpful functions of IntelliJ is its debugging capacity.
IntelliJ offers multiple features to make Java debugging easier. You can use out-of-the-box or custom configurations, run multiple debugging sessions at once, and fix and reload problematic code while your session is running. As more organizations move their software development to the cloud, though, remote debugging has become increasingly necessary.
In this article, you will learn how to configure a Spring Boot application for remote debugging with IntelliJ.
About Remote Debugging
While traditional debugging works with software hosted on an on-premise system, remote debugging enables you to debug cloud-hosted code by setting up a connection between your local environment and the remote server.
To debug remote Java applications, the Java Debug Wire protocol (JDWP) is used. This protocol defines the format of the communication between the JVM and the debugger. JDWP, however, is only one piece of the puzzle. The JVM and the debugger both implement other specifications that complete the big picture. The JVM implements JVM Tool Interface (JVMTI), which is a low-level specification that provides the debugging abilities to the JVM, for example, the ability to inspect the current object, and to set breakpoints. The debugger on the other hand, implements the Java Debug Interface (JDI). JDI is a pure Java interface that provides a high level way to pass debugging requests from the debugger to the JVM using JDWP.
When connecting a debugger to a remote JVM, either the debugger or the JVM can act as the server and the other can attach to it. In this article, the JVM will act as the server and the debugger will connect to it.
Why Do You Need Remote Debugging?
An increasing number of applications use microservices-based architecture, meaning pieces of the codebase run on different servers but work as a single application in production. Since the application does not have access to the resources required for debugging, remote debugging is a good solution.
Another reason to use remote debugging is that you can’t run Java applications in debug mode inside of a production environment, since it’s disabled by default and you can’t switch it on. In order to debug such an app, you would have to do reproduce a list of specific conditions. That means 1) the environment, 2) the individual steps, and 3) the issue itself, all either in your local environment or on dev servers. In this situation, remote debugging would come in handy, but that by no means implies remote debugging is the ideal tactic. As this tutorial will demonstrate, there are some drawbacks to remote debugging that may and sometimes may not be overcome
Setting Up Remote Debugging for a Spring Boot Application
In this tutorial, you’re going to set up and run a remote debug configuration for a Spring Boot REST application. While this example is written in Java, remote debugging can be performed on applications written in almost any language or framework.
To see the full code for the application, visit the GitHub repository.
Prerequisites
- IntelliJ IDEA Community Edition (this article uses version 2021.3.2)
- Java 9+
- Any Java project. This article uses a Spring Boot Maven application with Spring Boot Tools just to demonstrate the process, but any Java project would work.
Step 1: Create a Project
To create a Spring Boot project, head to Spring Initializr and set up a project. Save the project and open it with IntelliJ.
Step 2: Create Host App Configuration
In order to debug the Spring Boot app, first add one endpoint so that it can be run and tested.
In IntelliJ, open remotedebuggingapplication.java
file from the following location:
src/main/java/com/example/remotedebugging/RemotedebuggingApplication.java
Next, add the following code:
<span class="hljs-keyword">package</span> com.example.remotedebugging;
<span class="hljs-keyword">import</span> org.springframework.boot.SpringApplication;
<span class="hljs-keyword">import</span> org.springframework.boot.autoconfigure.SpringBootApplication;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.GetMapping;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.RequestParam;
<span class="hljs-keyword">import</span> org.springframework.web.bind.annotation.RestController;
<span class="hljs-annotation">@SpringBootApplication</span>
<span class="hljs-annotation">@RestController</span>
<span class="hljs-keyword">public</span> <span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">RemotedebuggingApplication</span> </span>{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">main</span><span class="hljs-params">(String[] args)</span> </span>{
SpringApplication.run(RemotedebuggingApplication.class, args);
}
<span class="hljs-annotation">@GetMapping</span>(<span class="hljs-string">"/hello"</span>)
<span class="hljs-function"><span class="hljs-keyword">public</span> String <span class="hljs-title">hello</span><span class="hljs-params">(@RequestParam(value = <span class="hljs-string">"name"</span>, defaultValue = <span class="hljs-string">"World"</span>)</span> String name) </span>{
<span class="hljs-keyword">return</span> String.format(<span class="hljs-string">"Hello %s!"</span>, name);
}
}
In order to start the host app with remote debugging enabled, you need to pass the following options when launching the app:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
There is no limitation on how you pass these options. In this tutorial, you’ll use a run/debug configuration to achieve this.
Right-click anywhere in the file and select Modify Run Configuration. In the dialog box that opens, click Modify options and select Add VM options:
Paste the following into the VM options field:
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
The above code passes the -agentlib
option to the JVM with the following sub-options:
- transport=dt_socket: Specifies that the connection to the debugger is made through UNIX sockets.
- server=y: Listen for a debugger application to attach at the address supplied by the
address
argument. Ifserver=n
is used, the JVM will connect to a debugger in the specifiedaddress
.
address=:5005*: Tells the JVM to listen for a debugger at port 5005. - suspend=n: This tells the VM to start executing without waiting for the debugger to attach. If
suspend=y
is used, the VM will suspend execution until the debugger is attached and issues a JDWP command to resume the VM.
This will start the VM with the necessary settings for remote debugging.
Click Apply.
Step 3: Create Remote Debug Configuration
Select Edit Configurations from the configuration menu.
Click the Add (+) button and select Remote JVM Debug. This option may also be called Remote depending on your OS and version of IntelliJ.
You can give the configuration a name if you want. In the Host field, enter the hostname of the remote application. In this case, it is localhost
since the Spring Boot app is running on the same machine.
Step 4: Run the Application
Select the host app configuration created in step two and start it by clicking the green Run button.
The first line of the output should be the following, which denotes that remote debugging is ready:
Listening for transport dt_socket at address: 5005
Step 5: Attach the Debugger
Now you can set breakpoints in your code as you would do with normal debugging:
When you’re ready to start debugging, select the remote debug configuration you created in step three and click the Debug icon. This will attach the debugger to the running Spring Boot process.
The following message in the console indicates that the debugger has been attached to the application:
Connected to the target VM, address: 'localhost:5005', transport: 'socket'
Depending on where you put the breakpoint, you’ll need to invoke that function to see the debugging in action. If you put the breakpoint in the same place as in the example screenshot, send a request to localhost:8080/hello
and the breakpoint should turn on.
To close the debugger, click the red square at the side of the debug window.
As you can see, the process is not entirely straightforward. Also, there are a few problems you may encounter when remote debugging an application. For example:
- You need admin access to the server to apply remote debug settings, which may not always be possible in a production environment.
- Remote debugging an application may expose sensitive data like passwords or tokens to developers, which is a significant security breach.
- The speed of the debugging process may suffer due to latency issues. Issues such as poor network connections might inhibit using more advanced features of otherwise powerful platforms to conduct remote debugging. For instance, IntelliJ recommends avoiding use of method breakpoints in favor of regular line breakpoints.
- In a multithreaded or microservices application, it may be challenging to get to the root cause of the issue.
- Setting up remote debugging in an application deployed to Kubernetes means you need to change the Dockerfile and rebuild and redeploy the Docker image every time remote debugging is required. That isn’t necessarily possible in a production environment.
- The data and insights required to get to the root cause of the issue are still hard to get from remote debugging.
Conclusion
Now that we understand how to set up a remote debugging configuration – in this case for a Spring Boot application in IntelliJ IDEA – you can also see there are a number of limitations to remote debugging itself.
Those issues can run the gamut, such as the need for admin access and the risk to sensitive data. You also need a strong connection to the remote server; otherwise, you will have to try debugging without a lot of more advanced features. On top of that have to keep your source code in sync with your IDE or debugging platform, which high latency could undermine (especially with more complicated apps).
In addition, there are certain situations when standard remote debugging is problematic: Microservice applications that have already been deployed are far more difficult to debug once on the cloud, and adding breakpoints for debugging purposes can cause app failure.
For these and other use cases, something that goes beyond merely remote debugging comes into play, live debugging, such as Rookout. For more about how Rookout can help you with an easier and faster debugging process, check out the documentation or sign up for a freemium account.