Skip to content

Java

Java

The java21-vertx template is recommended for Java functions. It uses Java 21 LTS, Vert.x and Gradle, with the of-watchdog handling HTTP requests.

Do you need to customise this template?

You can customise the official template, or provide your own. The code is available on GitHub: openfaas/templates.

Create a new function

Pull the java21-vertx template from the template store:

faas-cli template store pull  java21-vertx 

Scaffold a function with a name of your choice:

faas-cli new --lang java21-vertx my-function

The function directory contains:

my-function/
├── build.gradle
└── src/main/java/com/openfaas/function/Handler.java

build.gradle defines your dependencies. Add your function logic to src/main/java/com/openfaas/function/Handler.java.

The default handler returns a greeting using the request body:

package com.openfaas.function;

import io.vertx.core.Vertx;
import io.vertx.ext.web.RoutingContext;

public class Handler implements io.vertx.core.Handler<RoutingContext> {
    private final Vertx vertx;

    public Handler(Vertx vertx) {
        this.vertx = vertx;
    }

    @Override
    public void handle(RoutingContext context) {
        String body = context.body().asString();
        String name = body == null || body.isBlank() ? "World" : body;
        context.response()
            .putHeader("Content-Type", "text/plain; charset=utf-8")
            .end("Hello, " + name + "!");
    }
}

The template provides the HTTP server and calls handle for each request. The Vertx instance is passed to the handler's constructor.

Working with requests and JSON

Use the handler's RoutingContext to read request data and return a response. This example returns the method, path, query parameter, header and body as JSON.

Add import io.vertx.core.json.JsonObject; to the handler and update the handle method:

@Override
public void handle(RoutingContext context) {
    JsonObject body = new JsonObject()
        .put("method", context.request().method().name())
        .put("path", context.request().path())
        .put("name", context.queryParam("name").stream()
            .findFirst().orElse("World"))
        .put("requestId", context.request().getHeader("X-Request-Id"))
        .put("body", context.body().asString());

    context.response()
        .setStatusCode(200)
        .putHeader("Content-Type", "application/json; charset=utf-8")
        .end(body.encode());
}

Deploy the function, then invoke it with a path and query parameter such as /greet?name=OpenFaaS.

Reuse clients between invocations

The template creates one handler per process. Initialise shared clients in the constructor and keep request data in local variables or the RoutingContext.

Use asynchronous clients to avoid blocking other requests. See the HTTP client and database connection pool examples below.

Add Maven dependencies

Add libraries to the dependencies block in build.gradle. Maven Central and the Vert.x platform are already configured, so Vert.x modules do not need a version.

For example, add the Vert.x Web Client to make outgoing HTTP requests:

implementation 'io.vertx:vertx-web-client'

For other Maven libraries, use implementation 'group:artifact:version'. To include a JAR, place it under libs/ and add:

implementation files('libs/helper.jar')

Add Java packages

Place your own classes under src/main/java/, in directories matching their package. Gradle includes these classes automatically, so no changes to build.gradle are needed.

For example, create src/main/java/com/openfaas/function/util/Greeting.java:

package com.openfaas.function.util;

public class Greeting {
    public static String greet(String name) {
        return "Hello, " + name + "!";
    }
}

Add import com.openfaas.function.util.Greeting; to Handler.java, then use the class in your handle method:

@Override
public void handle(RoutingContext context) {
    context.response().end(Greeting.greet("OpenFaaS"));
}

Unit testing

JUnit Jupiter is already configured. Add tests under src/test/java/, they run automatically during the image build.

Make an HTTP request to another function

This example calls the nodeinfo store function through the OpenFaaS gateway. Make sure the upstream function is deployed:

faas-cli store deploy nodeinfo

Add the Web Client to the existing dependencies block in build.gradle:

implementation 'io.vertx:vertx-web-client'

Create a shared http client:

package com.openfaas.function;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.client.WebClient;

public class Handler implements io.vertx.core.Handler<RoutingContext> {
    private final WebClient client;
    private final String upstreamUrl;

    public Handler(Vertx vertx) {
        client = WebClient.create(vertx);

        upstreamUrl = System.getenv().getOrDefault(
            "UPSTREAM_URL",
            "http://gateway.openfaas:8080/function/nodeinfo"
        );
    }

    @Override
    public void handle(RoutingContext context) {
        if (!context.request().method().name().equals("GET")) {
            context.response().putHeader("Allow", "GET");
            respond(context, 405, new JsonObject().put("error", "Use GET"));
            return;
        }

        // Send asynchronously to keep the event loop free.
        client.getAbs(upstreamUrl)
            .followRedirects(false)
            .timeout(3000)
            .send()
            .onSuccess(response -> {
                // HTTP errors also reach onSuccess; check the status code.
                if (response.statusCode() < 200
                    || response.statusCode() >= 300) {
                    JsonObject body = new JsonObject()
                        .put("error", "Upstream rejected the request")
                        .put("upstreamStatus", response.statusCode());
                    respond(context, 502, body);
                    return;
                }

                JsonObject body = new JsonObject()
                    .put("upstreamStatus", response.statusCode())
                    .put("body", response.bodyAsString());
                respond(context, 200, body);
            })
            .onFailure(error -> {
                JsonObject record = new JsonObject()
                    .put("event", "upstream_request_failed")
                    .put("errorType", error.getClass().getSimpleName());
                System.err.println(record.encode());
                respond(context, 502, new JsonObject()
                    .put("error", "Upstream unavailable or timed out"));
            });
    }

    private static void respond(
        RoutingContext context, int status, JsonObject body
    ) {
        context.response()
            .setStatusCode(status)
            .putHeader("Content-Type", "application/json; charset=utf-8")
            .end(body.encode());
    }
}

The client is reused between invocations. Requests run asynchronously, allowing the function to handle other invocations while waiting for the upstream response.

Access a SQL database

This example uses PostgreSQL to query a SQL database with a shared connection pool.

Add the PostgreSQL client to the existing dependencies block in build.gradle:

implementation 'io.vertx:vertx-pg-client'

Connection settings use environment variables, while the password is read from an OpenFaaS secret. The function's configuration in stack.yaml might look like this:

functions:
  my-function:
    lang: java21-vertx
    handler: ./my-function
    image: my-function:latest
    environment:
      PGHOST: postgres.java-examples-db.svc.cluster.local
      PGPORT: "5432"
      PGUSER: app_user
      PGDATABASE: app_db
    secrets:
      - postgres-password

This example reuses a connection pool and reads the password from the mounted secret. Queries run asynchronously, so other requests can be handled while waiting for the database:

package com.openfaas.function;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import io.vertx.pgclient.PgBuilder;
import io.vertx.pgclient.PgConnectOptions;
import io.vertx.sqlclient.Pool;
import io.vertx.sqlclient.PoolOptions;
import io.vertx.sqlclient.Tuple;

import java.util.concurrent.TimeUnit;

public class Handler implements io.vertx.core.Handler<RoutingContext> {
    private static final String SECRET_PATH =
        "/var/openfaas/secrets/postgres-password";

    private final Pool pool;

    public Handler(Vertx vertx) {
        PoolOptions options = new PoolOptions()
            .setMaxSize(4)
            .setMaxWaitQueueSize(32)
            .setConnectionTimeout(5)
            .setConnectionTimeoutUnit(TimeUnit.SECONDS);

        pool = PgBuilder.pool()
            .using(vertx)
            .with(options)
            // Read the secret when opening a connection.
            .connectingTo(() -> vertx.fileSystem()
                .readFile(SECRET_PATH)
                .map(buffer -> new PgConnectOptions()
                    .setHost(env("PGHOST",
                        "postgres.java-examples-db.svc.cluster.local"))
                    .setPort(Integer.parseInt(env("PGPORT", "5432")))
                    .setDatabase(env("PGDATABASE", "app_db"))
                    .setUser(env("PGUSER", "app_user"))
                    .setPassword(buffer.toString().strip())))
            .build();
    }

    @Override
    public void handle(RoutingContext context) {
        if (!context.request().method().name().equals("GET")) {
            context.response().putHeader("Allow", "GET");
            respond(context, 405, new JsonObject().put("error", "Use GET"));
            return;
        }

        String name = context.queryParam("name").stream()
            .findFirst().orElse("OpenFaaS");
        if (name.length() > 100) {
            respond(context, 400, new JsonObject()
                .put("error", "name must be at most 100 characters"));
            return;
        }

        String query =
            "SELECT $1::text AS name, current_database() AS database, "
            + "pg_backend_pid() AS connection_id";

        pool.preparedQuery(query)
            .execute(Tuple.of(name))
            .onSuccess(rows -> {
                var row = rows.iterator().next();
                JsonObject body = new JsonObject()
                    .put("message", "Hello, " + row.getString("name") + "!")
                    .put("database", row.getString("database"))
                    .put("connectionId", row.getInteger("connection_id"));
                respond(context, 200, body);
            })
            .onFailure(error -> {
                JsonObject record = new JsonObject()
                    .put("event", "database_query_failed")
                    .put("errorType", error.getClass().getSimpleName());
                System.err.println(record.encode());
                respond(context, 503, new JsonObject()
                    .put("error", "Database unavailable"));
            });
    }

    private static String env(String name, String fallback) {
        return System.getenv().getOrDefault(name, fallback);
    }

    private static void respond(
        RoutingContext context, int status, JsonObject body
    ) {
        context.response()
            .setStatusCode(status)
            .putHeader("Content-Type", "application/json; charset=utf-8")
            .end(body.encode());
    }
}

Include and serve static files

Functions can serve static content such as HTML pages, stylesheets or JSON files alongside dynamic responses.

Place files under src/main/resources/webroot/. Gradle includes them in the function JAR automatically:

src/main/resources/webroot/
├── index.html
├── style.css
└── message.json

Use Vert.x's StaticHandler to serve the packaged files through your function:

package com.openfaas.function;

import io.vertx.core.Vertx;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import io.vertx.ext.web.handler.StaticHandler;

public class Handler implements io.vertx.core.Handler<RoutingContext> {
    private final Router router;

    public Handler(Vertx vertx) {
        router = Router.router(vertx);

        // Serve packaged resources; / defaults to index.html.
        StaticHandler files = StaticHandler.create("webroot")
            .setIncludeHidden(false)
            .setDirectoryListing(false)
            .setAlwaysAsyncFS(true)
            .setMaxAgeSeconds(60);

        router.route().handler(files);

        // Missing files fall through to this route.
        router.route().handler(context -> context.response()
            .setStatusCode(404)
            .putHeader("Content-Type", "text/plain; charset=utf-8")
            .end("Not found\n"));
    }

    @Override
    public void handle(RoutingContext context) {
        router.handleContext(context);
    }
}

The router maps request paths to packaged files: / serves index.html, and /style.css serves the stylesheet.

Authenticate requests with middleware

Use middleware to authenticate requests with a shared bearer token. This example protects every path and HTTP method without additional dependencies.

A random token can be generated with OpenSSL:

openssl rand -hex 32

The token is stored in an OpenFaaS secret named auth-bearer, referenced in the function's stack.yaml configuration:

functions:
  my-function:
    lang: java21-vertx
    handler: ./my-function
    image: my-function:latest
    secrets:
      - auth-bearer

The handler reads the token from the mounted secret at startup and reuses it between invocations. If the secret changes, restart or redeploy the function to load the new value.

package com.openfaas.function;

import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

public class Handler implements io.vertx.core.Handler<RoutingContext> {
    private static final String SECRET_PATH =
        "/var/openfaas/secrets/auth-bearer";

    private final byte[] bearerToken;
    private final Router router;

    public Handler(Vertx vertx) {
        bearerToken = readBearerToken(vertx);
        router = Router.router(vertx);

        // Authenticate every request before running the handler.
        router.route()
            .handler(this::authenticate)
            .handler(context -> {
                JsonObject body = new JsonObject()
                    .put("message", "Authenticated request");
                respond(context, 200, body);
            });
    }

    @Override
    public void handle(RoutingContext context) {
        router.handleContext(context);
    }

    private static byte[] readBearerToken(Vertx vertx) {
        // Read once during startup, before handling requests.
        String token = vertx.fileSystem()
            .readFileBlocking(SECRET_PATH)
            .toString(StandardCharsets.UTF_8)
            .strip();

        if (token.isEmpty()) {
            throw new IllegalStateException(
                "OpenFaaS secret auth-bearer must not be empty");
        }

        return token.getBytes(StandardCharsets.UTF_8);
    }

    private void authenticate(RoutingContext context) {
        String authorization = context.request().getHeader("Authorization");
        if (authorization == null
            || !authorization.regionMatches(true, 0, "Bearer ", 0, 7)) {
            unauthorized(context);
            return;
        }

        byte[] suppliedToken = authorization.substring(7)
            .getBytes(StandardCharsets.UTF_8);
        // Compare tokens without an early exit on the first mismatch.
        if (!MessageDigest.isEqual(bearerToken, suppliedToken)) {
            unauthorized(context);
            return;
        }

        context.next();
    }

    private static void unauthorized(RoutingContext context) {
        context.response()
            .putHeader("WWW-Authenticate", "Bearer realm=\"private-api\"");
        respond(context, 401, new JsonObject()
            .put("error", "A valid bearer token is required"));
    }

    private static void respond(
        RoutingContext context, int status, JsonObject body
    ) {
        context.response()
            .setStatusCode(status)
            .putHeader("Content-Type", "application/json; charset=utf-8")
            .putHeader("Cache-Control", "no-store")
            .end(body.encode());
    }
}

Callers send the token in the Authorization: Bearer <token> header. The middleware checks it before passing the request to the handler with next(). Requests with a missing or invalid token are rejected.

OpenFaaS also supports authentication without changing your handler code. See OpenFaaS IAM function authentication for access control with roles and policies, or OAuth/OIDC login for browser sign-in.