JJava is a Java kernel for Jupyter maintained by the DFLib.org community. Internally, the kernel executes Java code via JShell. Some of its additional commands are supported via a syntax similar to IPython magics.

Features

The kernel supports the following Jupyter features:

  • Code execution: output

  • Autocompletion (TAB in Jupyter notebook): autocompletion

  • Code inspection (Shift-TAB up to 4 times in Jupyter notebook). code-inspection

  • Colored error message displays. compilation-error incomplete-src-error runtime-error

  • Add maven dependencies at runtime (See also magics). maven-pom-dep

  • Display rich output (See also display and maven magic). E.g. here is a chart produced by DFLib and ECharts:

chart in jupyter
  • eval function. (See also kernel) Note: the signature is Object eval(String) throws Exception. This evaluates the expression (a cell) in the user scope and returns the actual evaluation result instead of a serialized one. eval

  • Configurable evaluation timeout timeout

Installation

Prerequisites

  1. Java 11 or newer

  2. If you already have another version of jjava kernel installed, remove it with the following command:

jupyter kernelspec remove java

Install Python and Jupyter

There are a few ways to install Python and Jupyter, depending on your OS and preferences. Below we provide a few specific recipes to help you to get started (especially if you are a Java developer new to the Python environment). But generally, Python is available from their official site, and Jupyter has its own installation instructions.

MacOS

If you are on MacOS, you can install both Python and Jupyter ("lab" and "notebook") with a single Homebrew command:

brew install jupyter

Windows

If you are on Windows, you can install Python using the official installer, and use "pip" for Jupyter:

  1. Go to https://www.python.org/downloads/windows/ and download the latest Python installer

  2. Run the installer

  3. Open Command Prompt (cmd), and run pip install jupyterlab (or pip install notebook if you prefer the "classic" notebook)

Install JJava

  1. Download JJava: go to GitHub releases, pick the latest version (or a specific one that you need) and under the "Assets" section download a file called jjava-$version.zip

  2. Unzip the file into a temporary location

  3. Run the following commands:

    # change to the directory where you unpacked the JJava kernel
    cd jjava-*
    
    # install Java kernel for the current user
    python install.py --user
    It is important to run install.py with the same version of Python that runs Jupyter. If you only have a single Python installation, the instructions above will work. If you have multiple, you will need to determine which one to use. E.g., on Mac or Linux, you might try the following recipe:
    cd jjava-*
    PY=$(cat `which jupyter` |head -1 |cut -d'!' -f2)
    
    # sanity check - the output should be a valid Python binary
    echo $PY
    $PY install.py --user
  4. If all the steps finished successfully, check that the Java kernel is installed:

    jupyter kernelspec list
    
    Available kernels:
      python3    /path/to/python/kernel
      java       /path/to/java/kernel
install.py script has a number of options. Some are similar to jupyter kernelspec install, but there are also additional parameters to configure the kernel, as discussed in the following chapters. Run the script with -h to see the available options.

Running Jupyter

Depending on which Jupyter environment you installed, there will be a specific command to run the notebook. E.g.:

jupyter notebook

jupyter lab

jupyter console --kernel=java

Configuring

Configuring the kernel can be done via environment variables. These can be set on the system or inside the kernel.json. The configuration can be done at install time, which may be repeated as often as desired. The parameters are listed with python3 install.py -h as well as below in the list of options.

List of options

Environment variable Parameter name Default Description

JJAVA_COMPILER_OPTS

comp-opts

""

A space delimited list of command line options that would be passed to the javac command when compiling a project. For example -parameters to enable retaining parameter names for reflection.

JJAVA_TIMEOUT

timeout

"-1"

A duration specifying a timeout (in milliseconds by default) for a single top level statement. If less than 1 then there is no timeout. If desired a time may be specified with a TimeUnit may be given following the duration number (ex "30 SECONDS").

JJAVA_CLASSPATH

classpath

""

A file path separator delimited list of classpath entries that should be available to the user code. Important: no matter what OS, this should use forward slash ``/'' as the file separator. Also each path may actually be a simple glob.

JJAVA_STARTUP_SCRIPTS_PATH

startup-scripts-path

""

A file path seperator delimited list of .jshell scripts to run on startup. This includes jjava-jshell-init.jshell and jjava-display-init.jshell. Important: no matter what OS, this should use forward slash / as the file separator. Also each path may actually be a simple glob.

JJAVA_STARTUP_SCRIPT

startup-script

""

A block of java code to run when the kernel starts up. This may be something like import my.utils; to setup some default imports or even void sleep(long time) { try {Thread.sleep(time); } catch (InterruptedException e) { throw new RuntimeException(e); }} to declare a default utility method to use in the notebook.

JJAVA_JVM_OPTS

-

""

A space delimited list of command line options that would be passed to the java command running the kernel. NOTE this is a runtime only option, and have no corresponding install parameter

JJAVA_LOAD_EXTENSIONS

-

"1"

Option that controls autoloading Kernel extensions feature. If you do not want third-party libraries to load anything implicitly you could turn it off by export JJAVA_LOAD_EXTENSIONS=0

Simple glob syntax

Options that support this glob syntax may reference a set of files with a single path-like string. Basic glob queries are supported including:

  • * to match 0 or more characters up to the next path boundary /

  • ? to match a single character

  • A path ending in / implicitly adds a * to match all files in the resolved directory

Any relative paths are resolved from the notebook server’s working directory. For example the glob *.jar will match all jars is the directory that the jupyter notebook command was run.

Note: users on any OS should use / as a path separator.

Changing VM/compiler options

See the List of options section for all of the configuration options.

To change compiler options use the JJAVA_COMPILER_OPTS environment variable (or --comp-opts parameter during installation) with a string of flags as if running the javac command.

To change JVM parameters use the JJAVA_JVM_OPTS environment variable with a string of flags as if running the java command. For example to enable assertions and set a limit on the heap size to 128m:

export JJAVA_JVM_OPTS='-ea -Xmx128m'

or enabled kernel debug:

export JJAVA_JVM_OPTS='-agentlib:jdwp=transport=dt_socket,server=y,address=5005,suspend=n'

Magics

Magics in JJava are very similar to those from IPython. There are:

  • Line magics: which are inline function calls via a magic function.

    %mavenRepo oss-sonatype-snapshots https://oss.sonatype.org/content/repositories/snapshots/
    %maven io.github.spencerpark:jupyter-jvm-basekernel:2.0.0-SNAPSHOT
    
    List<String> addedJars = %jars C:/all/my/*.jar
  • Cell magics: which are entire cell function calls that use the body of the cell as a special argument.

    %%loadFromPOM
    <repository>
      <id>oss-sonatype-snapshots</id>
      <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
    </repository>
    
    <dependency>
      <groupId>io.github.spencerpark</groupId>
      <artifactId>jupyter-jvm-basekernel</artifactId>
      <version>2.0.0-SNAPSHOT</version>
    </dependency>

The magics simply desugar to calls to lineMagic and cellMagic in case programmatic access is desired. These functions are in the notebook namespace and have the signatures below. Note the return type which allows for an implicit cast to what ever type is required but there is no safety in these checks.

  • <T> T lineMagic(String name, java.util.List<String> args)

  • <T> T cellMagic(String name, java.util.List<String> args, String body)

Magics provided by JJava

Things that are likely to become magics are kernel meta functions or functions that operate on source code. Magics should only be used for things that only appear in a Jupyter-like context and only use string arguments. Other things (like display and render) should be provided as plain functions.

jars

Add jars to the notebook classpath.

Line magic
  • arguments:

    • varargs list of simple glob paths to jars on the local file system. If a glob matches a directory all files in that directory will be added.

classpath

Add entries to the notebook classpath.

Line magic
  • arguments:

    • varargs list of simple glob paths to entries on the local file system. This includes directories or jars.

addMavenDependencies

Add maven artifacts to the notebook classpath. All transitive dependencies are also added to the classpath. See also addMavenRepo.

Line magic
  • aliases: addMavenDependency, maven

  • arguments:

    • varargs list of dependency coordinates in the form groupId:artifactId:[packagingType:[classifier]]:version

addMavenRepo

Add a maven repository to search for when using addMavenDependencies.

Line magic
  • aliases: mavenRepo

  • arguments:

    • repository id

    • repository url

loadFromPOM

Load any dependencies specified in a POM. This ignores repositories added with addMavenRepo as the POM would likely specify its own.

The cell magic is designed to make it very simple to copy and paste from any READMEs specifying maven POM fragments to use in depending on an artifact (including repositories other than central).

Line magic
  • arguments:

    • path to local POM file

    • varargs list of scope types to filter the dependencies by. Defaults to compile, runtime, system, and import if not supplied.

Cell magic
  • arguments:

    • varargs list of scope types to filter the dependencies by. Defaults to compile, runtime, system, and import if not supplied.

  • body: A partial POM literal.

    If the body is an xml <project> tag, then the body is used as a POM without being modified.

    Otherwise, the magic attempts to build a POM based on the xml fragments it gets.

    <modelVersion>, <groupId>, <artifactId>, and <version> are given default values if not supplied which there is no reason to supply other than if they happen to be what is copy-and-pasted.

    All children of <dependencies> and <repositories> are collected along with any loose <dependency> and repository tags.

    Ex: To add a dependency not in central simply add a valid <repository> and <dependency> and the magic will take care of putting it together into a POM.

    %%loadFromPOM
    <repository>
      <id>oss-sonatype-snapshots</id>
      <url>https://oss.sonatype.org/content/repositories/snapshots/</url>
    </repository>
    
    <dependency>
      <groupId>io.github.spencerpark</groupId>
      <artifactId>jupyter-jvm-basekernel</artifactId>
      <version>2.0.0-SNAPSHOT</version>
    </dependency>

Kernel

All code running in JJava flows through the kernel. This makes it the place to register magics, add things to the classpath, and perform many jupyter related operations.

Notebook functions

JJava injects a function for getting the active kernel instance and additional helpers for making use of the kernel at runtime. These are defined in the runtime Kernel class.

JavaKernel getKernelInstance()

Get a reference to the current kernel. It may return null if called outside of a kernel context but should be considered @NonNull when inside a notebook or similar. The kernel api has lots of goodies, look at the JavaKernel class for more information. Specifically there is access to adding to the classpath, getting the magics registry and maven resolver, and access to eval.

Object eval(String expr) throws Exception

The eval function provides full access to the code evaluation mechanism of the kernel. It evaluates the code in the same scope as the kernel and returns an object. This object is an object that lives in the kernel!

The given expression can be anything you would write in a cell, including magics.

(int) eval("1 + 2") + 3

Display

One of the many great things about the Jupyter front ends is the support for display_data. JJava interfaces with the base kernel’s high level rendering API.

Notebook functions

JJava injects 2 functions into the user space for displaying data: display and render. Most use cases should prefer the former but there is a necessary case for render that is outline below. In addition the updateDisplay function can be used to update a previously displayed object. All are defined in the runtime Display class.

All display/render functions include a text/plain representation in their output. By default this is the String.valueOf(Object) value but it can be overridden.

String display(Object o)

Display an object as it’s preferred types. If you don’t want a specific type it is best to let the object decide how it is best represented.

The object is rendered and published on the display stream. An id is returned which can be used to updateDisplay if desired.

String display(Object o, String... as)

Display an object as the requested types. In this case the object attempts to be rendered as the desired mime types given in as. No promises though, if a type is unsupported it will simply not appear in the output.

The object is rendered and published on the display stream. An id is returned which can be used to updateDisplay if desired.

This is useful when a type has many potential representations but not all are preferred. For example a CharSequence has many representations but only the text/plain is preferred. To display it as executable javascript we can use the following:

display("alert('Hello from JJava!');", "application/javascript");

Since there is the potential that some front ends don’t support a given format many can be given and the front end chooses the best. For example, to display as html and markdown:

display("<b>Bold</b>", "text/html", "text/markdown");

This will trigger a display message with values for text/html, text/markdown, and the implicit text/plain.

DisplayData render(Object o)

Renders an object as it’s preferred types and returns it’s rendered format. Similar to display(Object o) but without publishing the result.

DisplayData render(Object o, String... as)

Renders an object as the requested types and returns it’s rendered format. Similar to display(Object o, String... as) but without publishing the result.

When expressions are the last code unit in a cell they are rendered with the render(Object o) semantics. If this is not desired it can be hijacked by wrapping it in a call to this function.

String md = "Hello from **JJava**";

render(md, "text/markdown")

This will result in the Out[_] result to be the pretty text/markdown representation rather than the boring text/plain representation.

void updateDisplay(String id, Object o)

Renders an object as it’s preferred types and updates an existing display with the given id to contain the new rendered object. Similar to display(Object o) but updates an existing displayed object instead of appending a new one.

void updateDisplay(String id, Object o, String... as)

Renders an object as it’s requested types and updates an existing display with the given id to contain the new rendered object. Similar to display(Object o, String... as) but updates an existing displayed object instead of appending a new one.

String id = display("<b>Countdown:</b> 3", "text/html");
for (int i = 3; i >= 0; i--) {
    updateDisplay(id, "<b>Countdown:</b> " + i, "text/html");
    Thread.sleep(1000L);
}
render("<b>Liftoff!</b>", "text/html")

Notebooks and Version Control

Jupyter places the data generated by the notebook in the notebook itself. This creates a challenge maintaining notebooks under version control. Assuming you are using Git, you will need to add Git hooks to your project to strip off the data before commit.

TODO: a recipe for excluding data