psuter.net

Building OpenWhisk Java actions with Gradle

In this post, we demonstrate how to build Apache OpenWhisk actions written in Java, using Gradle as a build tool.

(If you are in a hurry, all the code discussed in this post is available on GitHub.)

Why Java?

Apache OpenWhisk supports multiple language runtimes, with Node.js being the most popular one. Scripting languages in general and JavaScript in particular are indeed a sensible choice for small, self-contained, short-lived actions.

There are scenarios, however, where Java is still the most appropriate choice: you could be breaking down an existing application into small, serverless components, and want to reuse code to the extent possible. Or maybe you simply want to leverage libraries from the very rich Java ecosystem.

Hello Java

For all OpenWhisk actions, regardless of the implementation language, the contract is to accept and return JSON objects. Java is no exception, and the entry point of an action has a signature that is almost familiar:

public static JsonObject main(JsonObject args);

The class JsonObject in the signature refers to the Google GSON library: in the absence of an officially sanctioned JSON library in the Java standard library, OpenWhisk has resorted to relying on one of the most popular ones. Note that you can also declare the method to throw any exception: Java exceptions are caught by the runtime and translate into action invocation errors.

The full source code of a “hello world” Java action is given below:

package example;

import com.google.gson.JsonObject;

public class Hello {
  public static JsonObject main(JsonObject args) {
    String name = args.getAsJsonPrimitive("name").getAsString();
    JsonObject response = new JsonObject();
    response.addProperty("greeting", "Hello " + name + "!");
    return response;
  }
}

Building

One major difference between Java and scripting languages is that the actions need to be compiled into a .jar file before they are uploaded to OpenWhisk. In our examples, we use Gradle, as it is relatively lightweight and simple to configure.

We set up a project with following filesystem layout:

hello/build.gradle
hello/src/main/java/example/Hello.java

The Java file is given above, and the Gradle file has the following contents:

apply plugin: 'java'

version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.google.code.gson:gson:2.6.2'
}

The only important lines relate to the resolution of dependencies: because our Java action relies on the GSON library, it needs to be included in the classpath. Gradle automatically fetches the library from Maven Central (or other software repositories you may have configured locally).

We can build the project with a single command, which we run from within the hello directory (where build.gradle resides):

$ gradle jar

This compiles our action into a .class file, and packages it as a .jar file. We can look at the contents of this archive as follows:

$ jar -tf build/libs/hello-1.0.jar
META-INF/
META-INF/MANIFEST.MF
example/
example/Hello.class

Observe that the archive contains only our class, and not the GSON dependency. This is not an issue, as the OpenWhisk runtime provides this dependency.

We can now deploy our action:

$ wsk action create hello-java build/libs/hello-1.0.jar --main example.Hello
ok: created action hello-java

The action creation process is similar to other runtimes, with the notable difference that one must provide the name of the main class with the --main flag. We can finally confirm that our Java action is working as expected:

$ wsk action invoke -br hello-java -p name reader
{
    "greeting": "Hello reader!"
}

On the shoulders of giants

Our previous example demonstrates the very first steps in running Java actions. As mentioned above, however, part of the appeal of Java is the rich ecosystem of libraries. To demonstrate the use of external dependencies, we build an OpenWhisk action to generate QR codes. Specifically, the action accepts text as an argument, and returns a base64-encoded PNG image or a QR code encoding the text. With the ZXing library, the action code is relatively short:

package qr;

import java.io.*;
import java.util.Base64;

import com.google.gson.JsonObject;

import com.google.zxing.*;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;

public class Generate {
  public static JsonObject main(JsonObject args) throws Exception {
    String text = args.getAsJsonPrimitive("text").getAsString();

    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    OutputStream b64os = Base64.getEncoder().wrap(baos);    

    BitMatrix matrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, 300, 300);
    MatrixToImageWriter.writeToStream(matrix, "png", b64os);
    b64os.close();

    String output = baos.toString("utf-8");

    JsonObject response = new JsonObject();
    response.addProperty("qr", output);
    return response;
  }
}

Building

If we build the code above as we did the first example, our .jar file will contain a single class Generate.class. The action will not run on OpenWhisk, since it will be missing the com.google.zxing dependencies. One way to address this issue is to create a so-called “fat jar” (or “über-jar”): a single archive containing, in addition to the action code, all classes from all dependencies.

We can configure Gradle to build such a file. Below is the build.gradle for the QR code action:

apply plugin: 'java'

version = '1.0'

repositories {
    mavenCentral()
}

configurations {
    provided
    compile.extendsFrom provided
}

dependencies {
    provided 'com.google.code.gson:gson:2.6.2'
    compile 'com.google.zxing:core:3.3.0'
    compile 'com.google.zxing:javase:3.3.0'
}

jar {
    dependsOn configurations.runtime

    from {
        (configurations.runtime - configurations.provided).collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
}

(The code above was modeled after this blog post.)

The Gradle configuration file does two important things:

  1. It declares a provided configuration in addition to the default compile and runtime configurations. We use provided for dependencies that are required at build time but that should not be included in the final fat jar. In our case, this concerns the GSON library which, as we know, is already provided by the OpenWhisk runtime.
  2. It overrides the jar task to include all classes found in all dependencies, except those marked as provided.

With this new configuration in place, we can build our .jar file:

$ gradle jar

If we now inspect the contents of build/libs/qr-1.0.jar, we see over 600 included classes (and no GSON).

We can finally deploy our serverless QR generator:

$ wsk action create qr build/libs/qr-1.0.jar --main qr.Generate
$ wsk action invoke -br qr -p text 'Hello world!'
{
    "qr": "iVBOR...YII="
}

Looking at base64-encoded images is not exactly as enticing as looking at the images themselves, unfortunately. Luckily, using jq and some pipes, we can retrieve them:

wsk action invoke -br qr -p text 'Hello world!' | jq -r .qr | base64 -D > qr.png
A QR code
QR code generated with ZXing on OpenWhisk.