Using Clojure To Debug Java

Problem

Suppose you have a Java web server running on a machine. The normal way to debug this is to attach an IDE such as Eclipse or IntelliJ to it.

The goals of debugging are to get into the code, find the values of variables, create objects, and execute methods on objects.

Generally the IDEs make it awkward to drill down into deeply nested data structures. They require UI interactions to drill down into objects. Also running methods is difficult. Generally interactions are hard to scale and automate.

The debugging environment also does not let you add new methods. Adding methods to classes that have already been loaded changes the signature of objects and requires a restart of the app server. This is because Java does not support hot-swapping classes.

Restarting the app server takes 3 minutes. So it slows down the feedback loop.

Solution

Clojure provides an elegant solution to these problems. Clojure provides a prompt using which Java objects can be created, and their methods executed, all from an interactive prompt.

Here is how this works. First, you embed a Clojure socket server in your application. Then you open a telnet connection to this server. Using this telnet connection you can create Java objects and invoke methods on them, passing in other objects. Also you can define new Clojure functions without restarting the app server.

How To Connect

The technique for connecting is this.

  1. Start the server.
  2. Visit a test JSP page which enables running Clojure in the process of the Java server.
  3. This causes Clojure classes to get loaded and for the Clojure runtime to bootstrap.
  4. Once it is up it starts reading on a special port for Clojure commands.
  5. At this point start a telnet session on the same port. The telnet session can be wrapped in rlwrap to provide command line editing using Emacs or Vi key-bindings.
  6. The telnet session brings up a Clojure prompt which can be used to run Clojure commands directly into Java server process.

Use Cases

Here are some use cases for this feature.

  1. Testing. It can be used to run specific parts of the code for testing.
  2. Exploring object behavior. Objects can be tested interactively. This really shortens the feedback cycle. If an issue is discovered the object can be tweaked to make it work.
  3. Performance testing. It is easy to write a loop on some complex sequence of logic and get statistically significant performance data.
  4. API exploration. Since Clojure gives full access to Java APIs this approach can be used to poke around a legacy API in the system. Frequently these APIs are not usable when the server is down. They only come alive when the server is running. They require the server context.
  5. Ad Hoc Exploratory Testing. This can be used for exploratory testing. Once a good test scenario has been defined the Clojure can be easily translated to.

These use cases are supported by the following features that the Clojure prompt provides:

  • Access to the runtime environment
  • Can create objects
  • Can invoke methods on objects
  • Can try out different scenarios
  • Can measure performance of specific methods
  • Can trap exceptions and observe traces
  • Can interactively drill down into methods

All of these features are enabled by the Clojure REPL. The REPL or the real-eval-print loop give you the Clojure prompt which provides a short fast feedback loop. In this loop you can interact with a system, understand it, measure it, and improve it.

Security Considerations

The Clojure JSP and the code should only be enabled in the test build. This way there is no risk of exposing the system to the world.

How To Do It

Step 1. Add the Clojure 1.3 jar and the Clojure Contrib jars to your system’s classpath.

Step 2. Add the following file as interactive.jsp to your system.

<%@ page import="test.ClojureServer, java.io.PrintWriter, java.io.StringWriter" %><%
    if (ClojureServer.SERVER == null) { };   // Force it to load the class.
    String input = request.getParameter("input");
    StringBuffer result = new StringBuffer();
    if (input != null) {
        try {
            result.append(ClojureServer.SERVER.evalString(input));
        } catch (Exception e) {
            StringWriter stringWriter = new StringWriter();
            e.printStackTrace(new PrintWriter(stringWriter));
            result.append(stringWriter.toString());
        }
    } else {
        input = "";
    }
%>

<form action="" method="POST">
<%--suppress HtmlFormInputWithoutLabel --%>
    <textarea name="input" rows="20" cols="120"><%= input%></textarea>
    <br>
    <input value="Eval" type="submit" />
</form>
<div>
    <h3>Result:</h3>
    <pre><%= result%></pre>
</div>

Step 3: Add the following code as ClojureServer.java to your system.

package test;
/**
 * ClojureServer
 */
public class ClojureServer {
    public static final String PORT = "18081";
    public static final String NS = "user";

    private final String initResult;
    public String getInitResult() { return initResult; }

    public ClojureServer() {
        String result;
        try {
            String create_repl_server =
                    "(do " +
                    "(use '[clojure.contrib.server-socket :only [create-repl-server]])" +
                    "(create-repl-server " + PORT + ")" + ")";
            result = evalString(create_repl_server);
        } catch (Exception e) {
            result = e.toString();
        }
        initResult = result;
    }

    private clojure.lang.Var eval = clojure.lang.RT.var("clojure.core", "eval");
    private clojure.lang.Var read = clojure.lang.RT.var("clojure.core", "read-string");

    public String evalString(String create_repl_server) throws Exception {
        String result = eval.invoke(read.invoke(create_repl_server)).toString();
        return result;
    }

    public static final ClojureServer SERVER = new ClojureServer();
}

Conclusion

Clojure can be used for detailed debugging and investigation of running systems.

Leave your comments...

Using Clojure To Debug Java