How to run Java JShell with local maven dependencies

How to run Java JShell with local maven dependencies

March 22, 2024
Java
JShell, Maven

Introduction #

There are cases when we want to quickly debug some part of java code, play around with different versions of an isolated snippet or simply to sharpen our knowledge regarding some new libraries and concepts.

Since Java 9 there is a REPL (Read–Eval–Print Loop) for an interactive evaluation within Java.

No need anymore for the “long” feedback loop: code -> compile -> run -> code!

This, of course, does not replace using tests for a quick/correct feedback loop.

Yet, having an interpreter is sometimes a time saver.

Environment #

All examples were run under the following environment:

  • OS: Mac OS
  • Java: OpenJDK - Temurin-21.0.1
  • Maven: 3.9.6
  • Shell: /bin/zsh

Main classes #

The JShell tool was added in Java 9 under the jdk.jshell package.

The main entry point of the REPL is located at jdk.internal.jshell.tool.JShellTool#start.

But the central class in the JShell API is actually jdk.jshell.JShell.

When a JShell session is started a JVM instance is actually used in the background:

zsh> jshell
|  Welcome to JShell -- Version 21.0.1
|  For an introduction type: /help intro
jshell> 
zsh> ps -a | grep java

 /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=localhost:59955 -Djdk.console=jdk.jshell jdk.jshell.execution.RemoteExecutionControl 59954

Importing Maven dependencies #

A subset of dependencies #

Let’s try jackson library under JShell:

When we run the jShell command without any context, and we try to import the object mapper class we get:

jshell> import com.fasterxml.jackson.databind.ObjectMapper;
|  Error:
|  package com.fasterxml.jackson.databind does not exist
|  import com.fasterxml.jackson.databind.ObjectMapper;
|         ^-----------------------------------------^

JShell accepts an extra option, --class-path, that sets up the classpath for the remote JVM where the evaluation will run.

This classpath plays the same role as the standard Java classpath and abides by the same rules and restrictions.

Let’s set the classpath to the jackson-databind jar:

zsh> jshell --class-path /path/to/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.15.3/jackson-databind-2.15.3.jar

Now the import works!

jshell> import com.fasterxml.jackson.databind.ObjectMapper;
jshell>

But nothing else!

jshell> ObjectMapper objectMapper=new ObjectMapper();
|  Exception java.lang.NoClassDefFoundError: com/fasterxml/jackson/core/Versioned
|        at ClassLoader.defineClass1 (Native Method)
|        at ClassLoader.defineClass (ClassLoader.java:1027)
|        at SecureClassLoader.defineClass (SecureClassLoader.java:150)
|        at BuiltinClassLoader.defineClass (BuiltinClassLoader.java:862)
|        at BuiltinClassLoader.findClassOnClassPathOrNull (BuiltinClassLoader.java:760)
|        at BuiltinClassLoader.loadClassOrNull (BuiltinClassLoader.java:681)
|        at BuiltinClassLoader.loadClass (BuiltinClassLoader.java:639)
|        at ClassLoaders$AppClassLoader.loadClass (ClassLoaders.java:188)
|        at ClassLoader.loadClass (ClassLoader.java:580)
|        at ClassLoader.loadClass (ClassLoader.java:526)
|        at (#2:1)
|  Caused by: java.lang.ClassNotFoundException: com.fasterxml.jackson.core.Versioned
|        at BuiltinClassLoader.loadClass (BuiltinClassLoader.java:641)
|        at ClassLoaders$AppClassLoader.loadClass (ClassLoaders.java:188)
|        at ClassLoader.loadClass (ClassLoader.java:526)
|        ...

Other necessary dependencies are missing!

Remember! wildcards support in java classpaths is limited.

The only way to get around this is to explicitly add folders of other dependencies to the path.

zsh> jshell --class-path /path/to/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.15.3/\*:/path/to/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.15.3/\*:/path/to/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.15.3/\*
zsh> ps -a | grep java
zsh> 42988 ttys022    0:00.43 /Library/Java/JavaVirtualMachines/temurin-21.jdk/Contents/Home/bin/java -agentlib:jdwp=transport=dt_socket,address=localhost:62065 --class-path /path/to/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.15.3/jackson-databind-2.15.3-sources.jar:/path/to/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.15.3/jackson-databind-2.15.3.jar:/path/to/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.15.3/jackson-core-2.15.3.jar:/path/to/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.15.3/jackson-annotations-2.15.3.jar -Djdk.console=jdk.jshell jdk.jshell.execution.RemoteExecutionControl 62064

Now we can use jackson API inside JShell:

jshell> import com.fasterxml.jackson.databind.ObjectMapper;

jshell> ObjectMapper objectMapper=new ObjectMapper();
objectMapper ==> com.fasterxml.jackson.databind.ObjectMapper@4facf68f

jshell> String json = """
   ...>        {"text":"some text"}
   ...>        """
json ==> "{\"text\":\"some text\"}\n"

jshell> record dummyRecord(String text){};
|  created record dummyRecord

jshell> objectMapper.readValue(json, dummyRecord.class);
$10 ==> dummyRecord[text=some text]

All project dependencies #

Sometimes you need to run the REPL in the context of a specific project, say a Spring Boot application.

In such a case the only way is really to let Maven prepare the classpath for you and use it for your JShell session:

zsh> jshell --class-path $(mvn -q exec:exec -Dexec.executable=echo -Dexec.args="%classpath")

This makes sense as long as isolated pieces of code are under investigation. Testing classes which depend on Spring and Spring Boot infrastructure would be almost of no help under REPL.