Java language support for Dispatch
Latest image on Docker Hub
You need a recent version of Dispatch installed in your Kubernetes cluster, Dispatch CLI configured to use it.
To add the base-image to Dispatch:
$ dispatch create base-image java-base dispatchframework/java-base:<tag>
Make sure the base-image status is READY
(it normally goes from INITIALIZED
to READY
):
$ dispatch get base-image java-base
Library dependencies listed in pom.xml
(maven dependency manifest) need to be wrapped into a Dispatch image. The pom.xml
file must include the minimal pom properties. For example, suppose we need a time library:
$ cat ./pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.dispatchframework.examples</groupId>
<artifactId>hello-with-deps</artifactId>
<version>1.0.0</version>
<properties>
<joda.version>2.3</joda.version>
</properties>
<dependencies>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda.version}</version>
</dependency>
</dependencies>
</project>
$ dispatch create image java-mylibs java-base --runtime-deps ./pom.xml
Make sure the image status is READY
(it normally goes from INITIALIZED
to READY
):
$ dispatch get image java-mylibs
Using the Java base-image, you can create Dispatch functions from Java source files. The file can require any libraries from the image (see above).
The only requirement is: a public class must be declared implementing the BiFunction interface and must override the apply
method, which accepts two arguments (context
and payload
), for example:
$ cat ./Hello.java
package io.dispatchframework.examples;
import java.util.Map;
import java.util.function.BiFunction;
import org.joda.time.DateTimeZone;
public class Hello implements BiFunction<Map<String, Object>, Map<String, Object>, String> {
@Override
public String apply(Map<String, Object> context, Map<String, Object> payload) {
final Object name = payload.getOrDefault("name", "Someone");
return String.format("Hello, %s from timezone %s", name, DateTimeZone.UTC);
}
}
$ dispatch create function hello ./Hello.java --image=java-mylibs
--handler=io.dispatchframework.examples.Hello
Make sure the function status is READY
(it normally goes from INITIALIZED
to READY
):
$ dispatch get function hello
As usual:
$ dispatch exec --json --input '{"name": "Jon"}' --wait hello
{
"blocking": true,
"executedTime": 1524786004,
"faasId": "f8287990-dec1-41d9-9d5a-fdfddffd53aa",
"finishedTime": 1524786005,
"functionId": "9e6f77cc-e345-4fcf-b33d-2075b8b122b2",
"functionName": "hello",
"input": {
"name": "Jon"
},
"logs": {
"stderr": null,
"stdout": null
},
"name": "d9705a66-da41-4fb5-ac0c-418c1253ccbf",
"output": "Hello, Jon from timezone UTC",
"reason": null,
"secrets": [],
"services": null,
"status": "READY",
"tags": []
}
There are three types of errors that can be thrown when invoking a function:
InputError
FunctionError
SystemError
SystemError
represents an error in the Dispatch infrastructure. InputError
represents an error in the input detected either early in the function itself or through input schema validation. FunctionError
represents an error in the function logic or an output schema validation error.
Functions themselves can either throw InputError
or FunctionError
For Java, the following exceptions thrown from the function are considered InputError
:
IllegalArgumentException
All other exceptions thrown from the function are considered FunctionError
.
To validate input in the function body:
package io.dispatchframework.javabaseimage;
import java.util.Map;
import java.util.function.BiFunction;
public class Lower implements BiFunction<Map<String, Object>, Map<String, Object>, String> {
@Override
public String apply(Map<String, Object> context, Map<String, Object> payload) {
final Object name = payload.getOrDefault("name", "SOMEONE");
if (name instanceof String) {
return ((String) name).toLowerCase();
} else {
throw new IllegalArgumentException("name is not of type string");
}
}
}
Since IllegalArgumentException
is considered an InputError
, functions should not throw it unless explicitly thrown due to an input validation error. Functions should catch and handle IllegalArgumentException
accordingly if it should not be classified as an InputError
.
To build a function from a source directory, the directory should follow the maven directory structure:
/src/main/java/
/target/lib/
/src/main/java
will hold the source code and /target/lib
will hold the dependencies. For example, if you had a Hello class with a gson dependency:
/src/main/java/io/dispatchframework/examples/Hello.java
/target/lib/gson-2.8.2.jar
Suppose your project was contained in the directory /my-project
:
/my-project/src/main/java/io/dispatchframework/examples/Hello.java
/my-project/target/lib/gson-2.8.2.jar
To create the function from the source directory:
dispatch create function hello /my-project --image=java
--handler=io.dispatchframework.examples.Hello
The Java language base image now supports initialization of a Spring application context to support functions that rely on Spring framework components. For now this support relies on as few Spring components as possible to remain compatibile with as many Spring versions as possible. Further the base image supports choosing whether to start the application context based on the presence of Spring classes on the classpath. As long as the org.springframework.beans.factory.BeanFactory
class is on the classpath, the function image will start an AnnotationConfigApplicationContext
.
First we will need to include the Spring dependencies when we create the image. For our example we will use this simple pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>dispatchframework.examples</groupId>
<artifactId>dispatch-spring</artifactId>
<version>1.0.0</version>
<properties>
<spring.version>5.0.5.RELEASE</spring.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
</project>
Let's pass these runtime dependencies to the creation of our image.
dispatch create image java-spring java-base --runtime-deps ./pom.xml
Now let's take a look at example Java function that uses Spring to wire dependencies into our function.
package io.dispatchframework.examples;
import java.util.Map;
import java.util.function.BiFunction;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class HelloSpring {
@Bean(name = "noone")
Person noone() {
return new Person("Noone", "Nowhere");
}
@Bean
BiFunction function(@Qualifier("noone") Person noone) {
return new HelloSpringFunction(noone);
}
public class HelloSpringFunction implements BiFunction<Map<Object, Object>, Person, Result> {
private Person defaultPerson;
HelloSpringFunction(Person defaultPerson) {
this.defaultPerson = defaultPerson;
}
@Override
public Result apply(Map<Object, Object> context, Person person) {
final String name = person.getName() == null ? defaultPerson.getName() : person.getName();
final String place = person.getPlace() == null ? defaultPerson.getPlace() : person.getPlace();
return new Result("Hello, " + name + " from " + place);
}
}
private class Person {
private String name;
private String place;
public Person(String name, String place) {
this.name = name;
this.place = place;
}
public String getName() {
return this.name;
}
public String getPlace() {
return this.place;
}
}
private class Result {
private String myField;
public Result(String myField) {
this.myField = myField;
}
public String getMyField() {
return this.myField;
}
}
}
The important thing to note about this function file is that the top level class is annotated with @Configuration. This is required for Dispatch to register the beans defined within this file. The other important thing to note is that our support for Spring based functions expects a single bean of type BiFunction
to be registered as a bean in the application context. In this above example this can be seen here:
@Bean
BiFunction function(@Qualifier("noone") Person noone) {
return new HelloSpringFunction(noone);
}
To create this function we run
dispatch create function spring-fn ./HelloSpring.java --image=java-spring
--handler=io.dispatchframework.examples.HelloSpring
Again wait for the function status to show as READY
dispatch get function spring-fn
Finally we can execute this function the same way as above
$ dispatch exec --json --input '{"name": "Jon"}' --wait spring-fn
Unit tests are written using JUnit
for both function-server
and validator
.
To run function-server
tests:
$ pwd
/java-base-image/function-server
$ mvn test
To run validator
tests, you must first generate the function-server
test-jar before running the validator
tests:
$ ls
Dockerfile README.md ci function-template validator
LICENSE build.sh function-server image-template version.txt
$ mvn -f ./function-server/pom.xml install
$ mvn -f ./validator/pom.xml test