Interactive Java: JShell API for Programmatic Execution

JShell, introduced in Java 9 as Java's REPL (Read-Eval-Print Loop), provides a powerful API for programmatically executing Java code snippets. This enables dynamic code evaluation, scripting capabilities, and interactive features within Java applications.


Introduction to JShell API

JShell allows you to execute Java code fragments without compiling complete classes:

// Traditional Java requires full class definition
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
// JShell allows immediate execution
jshell> System.out.println("Hello, World!")
Hello, World!

Core JShell API Components

1. Basic JShell Setup and Execution

import jdk.jshell.*;
import jdk.jshell.spi.ExecutionControlProvider;
import jdk.jshell.execution.*;
import java.util.*;
public class BasicJShellExample {
public static void main(String[] args) {
// Create a JShell instance
try (JShell jshell = JShell.create()) {
// Execute simple code snippets
List<SnippetEvent> events = jshell.eval("int x = 10;");
// Check evaluation results
for (SnippetEvent event : events) {
if (event.value() != null) {
System.out.println("Result: " + event.value());
}
if (event.status() == Snippet.Status.VALID) {
System.out.println("Snippet executed successfully");
}
}
// Use previously declared variables
jshell.eval("int y = x * 2;");
jshell.eval("System.out.println(\"y = \" + y);");
}
}
}

2. Comprehensive JShell Manager

public class JShellManager implements AutoCloseable {
private final JShell jshell;
private final List<SnippetEvent> history;
public JShellManager() {
// Configure JShell with custom execution engine
jshell = JShell.builder()
.executionEngine("local")
.build();
this.history = new ArrayList<>();
}
public ExecutionResult execute(String code) {
System.out.println("Executing: " + code);
List<SnippetEvent> events = jshell.eval(code);
history.addAll(events);
ExecutionResult result = new ExecutionResult();
for (SnippetEvent event : events) {
processSnippetEvent(event, result);
}
return result;
}
private void processSnippetEvent(SnippetEvent event, ExecutionResult result) {
switch (event.status()) {
case VALID:
if (event.value() != null) {
result.addOutput(event.value());
}
result.addSuccessfulSnippet(event.snippet());
break;
case REJECTED:
result.addError("Snippet rejected: " + event.snippet().source());
break;
case RECOVERABLE_DEFINED:
case RECOVERABLE_NOT_DEFINED:
result.addError("Recoverable error in: " + event.snippet().source());
break;
case OVERWRITTEN:
result.addWarning("Snippet overwritten: " + event.snippet().source());
break;
}
// Print any exception
if (event.exception() != null) {
result.addError("Exception: " + event.exception().getMessage());
}
}
public List<String> getDeclaredVariables() {
return jshell.variables()
.map(snippet -> snippet.source())
.collect(Collectors.toList());
}
public List<String> getDeclaredMethods() {
return jshell.methods()
.map(snippet -> snippet.source())
.collect(Collectors.toList());
}
public void reset() {
jshell.dropPersistentState();
history.clear();
}
@Override
public void close() {
jshell.close();
}
public static class ExecutionResult {
private final List<String> outputs = new ArrayList<>();
private final List<String> errors = new ArrayList<>();
private final List<String> warnings = new ArrayList<>();
private final List<Snippet> successfulSnippets = new ArrayList<>();
// Getters and adders...
public void addOutput(String output) { outputs.add(output); }
public void addError(String error) { errors.add(error); }
public void addWarning(String warning) { warnings.add(warning); }
public void addSuccessfulSnippet(Snippet snippet) { successfulSnippets.add(snippet); }
public boolean isSuccess() { return errors.isEmpty(); }
public List<String> getOutputs() { return Collections.unmodifiableList(outputs); }
public List<String> getErrors() { return Collections.unmodifiableList(errors); }
}
}

Advanced JShell Features

1. Method and Class Definition

public class AdvancedJShellExample {
public static void main(String[] args) {
try (JShell jshell = JShell.create()) {
// Define a method
jshell.eval("""
int add(int a, int b) {
return a + b;
}
""");
// Use the method
jshell.eval("int result = add(5, 3);");
jshell.eval("System.out.println(\"5 + 3 = \" + result);");
// Define a class
jshell.eval("""
class Calculator {
int multiply(int a, int b) {
return a * b;
}
double divide(double a, double b) {
return a / b;
}
}
""");
// Use the class
jshell.eval("Calculator calc = new Calculator();");
jshell.eval("System.out.println(calc.multiply(4, 5));");
jshell.eval("System.out.println(calc.divide(10.0, 2.0));");
}
}
}

2. Import Management

public class JShellImportManager {
private final JShell jshell;
public JShellImportManager() {
this.jshell = JShell.create();
setupDefaultImports();
}
private void setupDefaultImports() {
// Add common imports
addImport("java.util.*");
addImport("java.time.*");
addImport("java.math.*");
addImport("java.io.*");
}
public void addImport(String importStatement) {
if (!importStatement.startsWith("import ")) {
importStatement = "import " + importStatement;
}
jshell.eval(importStatement);
}
public List<String> getActiveImports() {
return jshell.imports()
.map(Snippet::source)
.collect(Collectors.toList());
}
public void executeWithImports(String code, String... additionalImports) {
for (String imp : additionalImports) {
addImport(imp);
}
jshell.eval(code);
}
public static void main(String[] args) {
try (JShellImportManager manager = new JShellImportManager()) {
// Use classes from imported packages
manager.executeWithImports("""
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
names.stream()
.filter(name -> name.startsWith("A"))
.forEach(System.out::println);
""");
// Add specific class import
manager.addImport("java.util.concurrent.ThreadLocalRandom");
manager.executeWithImports("""
int randomNum = ThreadLocalRandom.current().nextInt(1, 100);
System.out.println("Random number: " + randomNum);
""");
}
}
}

Real-World Applications

1. Dynamic Expression Evaluator

public class ExpressionEvaluator {
private final JShell jshell;
private final Map<String, Object> context;
public ExpressionEvaluator() {
this.jshell = JShell.create();
this.context = new HashMap<>();
setupMathContext();
}
private void setupMathContext() {
jshell.eval("import java.lang.Math;");
jshell.eval("import java.util.*;");
}
public Object evaluate(String expression) {
try {
// Add context variables to JShell
injectContextVariables();
List<SnippetEvent> events = jshell.eval(expression);
for (SnippetEvent event : events) {
if (event.status() == Snippet.Status.VALID && event.value() != null) {
return parseValue(event.value());
}
if (event.exception() != null) {
throw new EvaluationException("Evaluation failed: " + 
event.exception().getMessage());
}
}
throw new EvaluationException("No result from expression: " + expression);
} catch (Exception e) {
throw new EvaluationException("Failed to evaluate expression: " + expression, e);
}
}
private void injectContextVariables() {
for (Map.Entry<String, Object> entry : context.entrySet()) {
String varDeclaration = String.format("%s %s = %s;",
getTypeDeclaration(entry.getValue()),
entry.getKey(),
formatValue(entry.getValue()));
jshell.eval(varDeclaration);
}
}
private String getTypeDeclaration(Object value) {
if (value instanceof Integer) return "int";
if (value instanceof Double) return "double";
if (value instanceof Boolean) return "boolean";
if (value instanceof String) return "String";
return "Object";
}
private String formatValue(Object value) {
if (value instanceof String) {
return "\"" + value + "\"";
}
return value.toString();
}
private Object parseValue(String stringValue) {
// Parse the string representation back to appropriate type
try {
return Integer.parseInt(stringValue);
} catch (NumberFormatException e1) {
try {
return Double.parseDouble(stringValue);
} catch (NumberFormatException e2) {
if ("true".equalsIgnoreCase(stringValue)) return true;
if ("false".equalsIgnoreCase(stringValue)) return false;
return stringValue;
}
}
}
public void setVariable(String name, Object value) {
context.put(name, value);
}
public static void main(String[] args) {
ExpressionEvaluator evaluator = new ExpressionEvaluator();
// Set context variables
evaluator.setVariable("radius", 5.0);
evaluator.setVariable("price", 100);
evaluator.setVariable("taxRate", 0.08);
// Evaluate expressions
System.out.println("Circle area: " + 
evaluator.evaluate("Math.PI * radius * radius"));
System.out.println("Total price: " + 
evaluator.evaluate("price * (1 + taxRate)"));
System.out.println("Random value: " + 
evaluator.evaluate("Math.random() * 100"));
}
public static class EvaluationException extends RuntimeException {
public EvaluationException(String message) { super(message); }
public EvaluationException(String message, Throwable cause) { super(message, cause); }
}
}

2. Interactive Code Playground

public class InteractivePlayground {
private final JShell jshell;
private final Scanner scanner;
public InteractivePlayground() {
this.jshell = JShell.create();
this.scanner = new Scanner(System.in);
setupEnvironment();
}
private void setupEnvironment() {
// Pre-load common utilities
jshell.eval("""
import java.util.*;
import java.time.*;
import java.math.*;
import java.util.stream.*;
""");
// Add helper methods
jshell.eval("""
void print(Object obj) { System.out.println(obj); }
void printf(String format, Object... args) { System.out.printf(format, args); }
""");
}
public void start() {
System.out.println("Java Interactive Playground");
System.out.println("Type 'exit' to quit, 'reset' to clear context, 'import <pkg>' to add imports");
while (true) {
System.out.print("\njshell> ");
String input = scanner.nextLine().trim();
if ("exit".equalsIgnoreCase(input)) {
break;
} else if ("reset".equalsIgnoreCase(input)) {
reset();
System.out.println("Context reset.");
continue;
} else if (input.startsWith("import ")) {
handleImport(input);
continue;
} else if (input.startsWith("/")) {
handleCommand(input);
continue;
}
executeCode(input);
}
}
private void executeCode(String code) {
try {
List<SnippetEvent> events = jshell.eval(code);
for (SnippetEvent event : events) {
if (event.status() == Snippet.Status.VALID) {
if (event.value() != null && !"null".equals(event.value())) {
System.out.println("==> " + event.value());
}
} else {
System.err.println("Error: " + event.status());
if (event.exception() != null) {
event.exception().printStackTrace();
}
}
}
} catch (Exception e) {
System.err.println("Execution error: " + e.getMessage());
}
}
private void handleImport(String importStatement) {
jshell.eval(importStatement);
System.out.println("Import added: " + importStatement);
}
private void handleCommand(String command) {
switch (command) {
case "/vars":
jshell.variables().forEach(var -> 
System.out.println("var: " + var.source()));
break;
case "/methods":
jshell.methods().forEach(method -> 
System.out.println("method: " + method.source()));
break;
case "/classes":
jshell.types().forEach(type -> 
System.out.println("class: " + type.source()));
break;
case "/imports":
jshell.imports().forEach(imp -> 
System.out.println("import: " + imp.source()));
break;
case "/list":
jshell.snippets().forEach(snippet -> 
System.out.println(snippet.id() + ": " + snippet.source()));
break;
default:
System.out.println("Unknown command: " + command);
}
}
private void reset() {
jshell.stop();
jshell.snippets().forEach(snippet -> {
if (snippet.status() == Snippet.Status.VALID) {
jshell.drop(snippet);
}
});
}
public static void main(String[] args) {
new InteractivePlayground().start();
}
}

3. Educational Code Runner

public class EducationalCodeRunner {
private final JShell jshell;
private final Map<String, Exercise> exercises;
public EducationalCodeRunner() {
this.jshell = JShell.create();
this.exercises = loadExercises();
setupLearningEnvironment();
}
private Map<String, Exercise> loadExercises() {
Map<String, Exercise> exercises = new HashMap<>();
exercises.put("variables", new Exercise(
"Declare and use variables",
"Declare an integer variable 'age' with value 25, then print it",
"int age = 25;",
"System.out.println(age);",
"25"
));
exercises.put("methods", new Exercise(
"Create a method",
"Create a method 'square' that takes an int and returns its square",
"int square(int n) { return n * n; }",
"System.out.println(square(5));",
"25"
));
exercises.put("loops", new Exercise(
"Use loops",
"Use a for loop to print numbers 1 to 5",
"",
"for (int i = 1; i <= 5; i++) { System.out.println(i); }",
"1\\n2\\n3\\n4\\n5"
));
return exercises;
}
private void setupLearningEnvironment() {
jshell.eval("import java.util.*;");
}
public ExerciseResult runExercise(String exerciseId, String userCode) {
Exercise exercise = exercises.get(exerciseId);
if (exercise == null) {
return ExerciseResult.error("Exercise not found: " + exerciseId);
}
try {
// Reset context
jshell.stop();
// Execute setup code
if (!exercise.setupCode().isEmpty()) {
jshell.eval(exercise.setupCode());
}
// Execute user code
List<SnippetEvent> setupEvents = jshell.eval(userCode);
// Check for compilation errors
for (SnippetEvent event : setupEvents) {
if (event.status() != Snippet.Status.VALID) {
return ExerciseResult.error("Compilation error: " + event.status());
}
}
// Execute test code
List<SnippetEvent> testEvents = jshell.eval(exercise.testCode());
// Capture output
StringBuilder output = new StringBuilder();
for (SnippetEvent event : testEvents) {
if (event.value() != null) {
output.append(event.value()).append("\n");
}
}
String actualOutput = output.toString().trim();
boolean passed = actualOutput.equals(exercise.expectedOutput());
return new ExerciseResult(passed, actualOutput, exercise.expectedOutput());
} catch (Exception e) {
return ExerciseResult.error("Runtime error: " + e.getMessage());
}
}
public static class Exercise {
private final String title;
private final String description;
private final String setupCode;
private final String testCode;
private final String expectedOutput;
public Exercise(String title, String description, String setupCode, 
String testCode, String expectedOutput) {
this.title = title;
this.description = description;
this.setupCode = setupCode;
this.testCode = testCode;
this.expectedOutput = expectedOutput;
}
// Getters...
public String title() { return title; }
public String description() { return description; }
public String setupCode() { return setupCode; }
public String testCode() { return testCode; }
public String expectedOutput() { return expectedOutput; }
}
public static class ExerciseResult {
private final boolean passed;
private final String actualOutput;
private final String expectedOutput;
private final String error;
private ExerciseResult(boolean passed, String actualOutput, String expectedOutput, String error) {
this.passed = passed;
this.actualOutput = actualOutput;
this.expectedOutput = expectedOutput;
this.error = error;
}
public static ExerciseResult success(String actualOutput, String expectedOutput) {
return new ExerciseResult(true, actualOutput, expectedOutput, null);
}
public static ExerciseResult error(String error) {
return new ExerciseResult(false, null, null, error);
}
// Getters...
public boolean passed() { return passed; }
public String actualOutput() { return actualOutput; }
public String expectedOutput() { return expectedOutput; }
public String error() { return error; }
}
}

Advanced JShell Integration

1. Custom Execution Control

public class SandboxedJShell {
private final JShell jshell;
private final Set<String> allowedPackages;
public SandboxedJShell() {
this.allowedPackages = new HashSet<>(Arrays.asList(
"java.lang", "java.util", "java.math", "java.time"
));
this.jshell = JShell.builder()
.executionEngine(createSandboxedProvider())
.build();
}
private ExecutionControlProvider createSandboxedProvider() {
return new LocalExecutionControlProvider() {
@Override
public String name() {
return "sandboxed";
}
@Override
public ExecutionControl generate(ExecutionEnv env) throws Throwable {
return new SandboxedExecutionControl();
}
};
}
private class SandboxedExecutionControl extends LocalExecutionControl {
@Override
public void load(ClassBytecodes[] cbcs) throws ClassInstallException {
// Validate class loading
for (ClassBytecodes cbc : cbcs) {
if (!isClassAllowed(cbc.className())) {
throw new ClassInstallException("Class not allowed: " + cbc.className(), null);
}
}
super.load(cbcs);
}
private boolean isClassAllowed(String className) {
String packageName = className.contains(".") 
? className.substring(0, className.lastIndexOf('.'))
: "";
return allowedPackages.contains(packageName);
}
}
public void addAllowedPackage(String packageName) {
allowedPackages.add(packageName);
}
public ExecutionResult executeSafely(String code) {
try {
List<SnippetEvent> events = jshell.eval(code);
return processEvents(events);
} catch (Exception e) {
return ExecutionResult.error(e.getMessage());
}
}
private ExecutionResult processEvents(List<SnippetEvent> events) {
ExecutionResult result = new ExecutionResult();
for (SnippetEvent event : events) {
if (event.status() == Snippet.Status.VALID) {
result.addOutput(event.value() != null ? event.value() : "OK");
} else {
result.addError(event.status().toString());
}
}
return result;
}
public static class ExecutionResult {
private final List<String> outputs = new ArrayList<>();
private final List<String> errors = new ArrayList<>();
public void addOutput(String output) { outputs.add(output); }
public void addError(String error) { errors.add(error); }
public boolean isSuccess() { return errors.isEmpty(); }
public List<String> getOutputs() { return outputs; }
public List<String> getErrors() { return errors; }
public static ExecutionResult error(String error) {
ExecutionResult result = new ExecutionResult();
result.addError(error);
return result;
}
}
}

2. JShell with Dependency Management

public class JShellWithDependencies {
private final JShell jshell;
private final List<URL> classpathUrls;
public JShellWithDependencies() {
this.classpathUrls = new ArrayList<>();
this.jshell = createJShellWithClasspath();
}
private JShell createJShellWithClasspath() {
// Create a custom class loader with additional dependencies
URLClassLoader classLoader = new URLClassLoader(
classpathUrls.toArray(new URL[0]),
ClassLoader.getSystemClassLoader()
);
return JShell.builder()
.remoteVMOptions("-Djava.system.class.loader=" + classLoader.getClass().getName())
.build();
}
public void addToClasspath(URL url) {
classpathUrls.add(url);
// JShell needs to be recreated after classpath changes
// In real implementation, you'd handle this more gracefully
}
public void addToClasspath(File file) throws MalformedURLException {
addToClasspath(file.toURI().toURL());
}
public void executeWithDependencies(String code) {
try {
List<SnippetEvent> events = jshell.eval(code);
for (SnippetEvent event : events) {
if (event.status() != Snippet.Status.VALID) {
System.err.println("Error: " + event.status());
} else if (event.value() != null) {
System.out.println("Result: " + event.value());
}
}
} catch (Exception e) {
System.err.println("Execution failed: " + e.getMessage());
}
}
}

Best Practices and Error Handling

1. Robust JShell Wrapper

public class RobustJShell implements AutoCloseable {
private JShell jshell;
private boolean initialized = false;
public synchronized void initialize() {
if (!initialized) {
try {
jshell = JShell.create();
setupSafeEnvironment();
initialized = true;
} catch (Exception e) {
throw new JShellInitializationException("Failed to initialize JShell", e);
}
}
}
private void setupSafeEnvironment() {
// Set memory limits
jshell.eval("""
// Prevent infinite loops with timeout simulation
long startTime = System.currentTimeMillis();
""");
// Add safe imports
jshell.eval("import java.util.*;");
jshell.eval("import java.math.*;");
}
public ExecutionResult executeSafely(String code, long timeoutMs) {
if (!initialized) {
initialize();
}
CompletableFuture<ExecutionResult> future = CompletableFuture.supplyAsync(() -> {
try {
return execute(code);
} catch (Exception e) {
return ExecutionResult.error(e.getMessage());
}
});
try {
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
} catch (TimeoutException e) {
future.cancel(true);
return ExecutionResult.error("Execution timed out after " + timeoutMs + "ms");
} catch (Exception e) {
return ExecutionResult.error("Execution failed: " + e.getMessage());
}
}
private ExecutionResult execute(String code) {
List<SnippetEvent> events = jshell.eval(code);
ExecutionResult result = new ExecutionResult();
for (SnippetEvent event : events) {
processEvent(event, result);
}
return result;
}
private void processEvent(SnippetEvent event, ExecutionResult result) {
switch (event.status()) {
case VALID:
result.markSuccess();
if (event.value() != null) {
result.setOutput(event.value());
}
break;
case REJECTED:
result.addError("Code rejected: " + event.snippet().source());
break;
default:
result.addError("Execution status: " + event.status());
}
if (event.exception() != null) {
result.addError("Exception: " + event.exception().getMessage());
}
}
@Override
public synchronized void close() {
if (jshell != null) {
jshell.close();
initialized = false;
}
}
public static class JShellInitializationException extends RuntimeException {
public JShellInitializationException(String message, Throwable cause) {
super(message, cause);
}
}
}

Conclusion

JShell API provides powerful capabilities for dynamic Java execution:

Key Benefits:

  • REPL functionality embedded in applications
  • Educational tools for learning Java
  • Dynamic scripting capabilities
  • Rapid prototyping and experimentation
  • Code evaluation and validation

Use Cases:

  • Interactive learning platforms
  • Dynamic configuration evaluators
  • Mathematical expression parsers
  • Code testing and validation systems
  • Plugin systems with scriptable behavior

Best Practices:

  • Always use try-with-resources for JShell instances
  • Implement proper sandboxing for untrusted code
  • Use timeouts for potentially long-running code
  • Handle exceptions gracefully
  • Reset state between unrelated executions

JShell API opens up exciting possibilities for making Java applications more dynamic and interactive while maintaining the language's robustness and type safety.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper