Compute Services

Building OpenWhisk actions with Java and Gradle

Share this post:

openwhisk-hero-image

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

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

Why Java?

Since scripting languages in general, and JavaScript in particular are indeed a sensible choice for small, self-contained, short-lived actions, it’s no surprise that Node.js is the most popular of the multiple runtimes that OpenWhisk supports.

There are scenarios, however, in which Java is still the most appropriate choice–for example, if you are breaking down an existing application into small, serverless components and want to reuse code to the extent possible; or you simply want to leverage libraries from the very rich Java ecosystem.

Hello Java

For all OpenWhisk actions, regardless of the implementation language, OpenWhisk accepts and returns JSON objects.

The entry point of a Java-defined 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 relies on one of the most popular. 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;

[code]
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;

}

}
[/code]

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:

[code]
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’

}
[/code]

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.gradleresides):

[code]
$ gradle jar
[/code]

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:

[code]
$ jar -tf build/libs/hello-1.0.jar

META-INF/

META-INF/MANIFEST.MF

example/

example/Hello.class
[/code]

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:

[code]
$ wsk action create hello-java build/libs/hello-1.0.jar –main example.Hello

ok: created action hello-java
[/code]

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:

[code]
$ wsk action invoke -br hello-java -p name reader

{

"greeting": "Hello reader!"

}
[/code]

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:

[code]
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;

}

}
[/code]

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:

[code]
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)

}

}

}
[/code]

(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 providedfor 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:

[code]
$ gradle jar
[/code]

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:

[code]
$ 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 <a href="https://stedolan.github.io/jq/">jq</a> and some pipes, we can build those images:

wsk action invoke -br qr -p text ‘Hello world!’ | jq -r .qr | base64 -D &gt; qr.png
[/code]

More Compute Services stories
December 11, 2018

Unifying Containers, Apps, and Functions

Innovative solutions like Knative and Istio are leading us to a unified container application platform that lets developers leverage the best of containers, apps, and functions in a single integrated way.

Continue reading

December 11, 2018

Using Availability Zones to Enhance Event Streams Resilience

With the Enterprise plan of IBM Event Streams, you can deploy Kafka across availability zones to maximize both its resilience to failures and the durability of your message data. Applications can use Kafka to achieve the right balance of availability and durability to meet your business needs.

Continue reading

December 10, 2018

The Run Up to KubeCon: Easing the Burden of Security and Infrastructure Management

In the run up to KubeCon, IBM Cloud announced new capabilities to ease Kubernetes operations and improve security across multiple cloud architectures.

Continue reading