Start the fun with Java 14 and Micronaut inside serverless Containers on Cloud Run

Hot on the heels of the announcement of the general availability of JDK 14, I couldn’t resist taking it for a spin. Without messing up my environment — I’ll confess I’m running 11 on my machine, but I’m still not even using everything that came past Java 8! — I decided to test this new edition within the comfy setting of a Docker container.

Minimal OpenJDK 14 image running JShell

Super easy to get started (assuming you have Docker installed on your machine), create a Dockerfile with the following content:

FROM openjdk:14
CMD ["jshell"]

Only two lines: one to declare an OpenJDK base image with the 14 tag, and one to launch the JShell REPL (introduced in Java 9).

Build and tag the image with:

$ docker build -t 14fun .

I called it 14fun, because you could almost pronounce it “one for the fun”! And then you can run this container image interactively with:

$ docker run -it 14fun

Then you will land directly in JShell, and can try a hello world of sorts:

Mar 20, 2020 9:17:28 AM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
| Welcome to JShell -- Version 14
| For an introduction type: /help intro
jshell> System.out.println("Stay at home!")
Stay at home!

You can enter /exit to quit the REPL.

And you certainly noticed that we’re indeed on version 14, congrats!

New and preview features

If you read the announcement, you will have remarked that some of the new features are not necessarily generally available, but are still only in preview mode. 

Here’s what’s new:

If you want to play with those upcoming features, you have to let the Java tooling know that you want to enable them. You can do that with the --enable-preview flag. So let’s update our Dockerfile accordingly:

FROM openjdk:14
CMD ["jshell", "--enable-preview"]

Rebuild and rerun the docker commands.

Text blocks

What about trying the text blocks? With text blocks, don’t bother with appending strings with + operations, not forgetting the \n at the end of each line. It’s now sweeter to write long strings spanning several lines, for example:

$ docker run -it 14fun 
Mar 20, 2020 1:12:28 PM java.util.prefs.FileSystemPreferences$1 run
INFO: Created user preferences directory.
| Welcome to JShell -- Version 14
| For an introduction type: /help intro
jshell> var s = """
...> Hello everyone!
...> How are you doing?
...> """
s ==> "Hello everyone!\nHow are you doing?\n"


Java is often criticized for its verbosity — think for instance how writing good POJOs can be tiresome, with proper equals() / hashCode() / toString() methods, constructors, getters and setters. Fortunately, IDEs help a lot here, but sometimes you really want some simple data holder classes without all the boilerplate. That’s where records come into the picture. 

Note however that these are not immutable (for example, like in Apache Groovy with its @Immutable transformation), if the fields in the record are mutable objects.

Let’s imagine a 3D point Record, what would it look like?

jshell> record Point3D(double x, double y, double z) { }
| created record Point3D
jshell> var p1 = new Point3D(0, 1, 2)
p1 ==> Point3D[x=0.0, y=1.0, z=2.0]
jshell> var p2 = new Point3D(0, 1, 2)
p2 ==> Point3D[x=0.0, y=1.0, z=2.0]
jshell> p1.equals(p2)
$5 ==> true

Notice the toString() representation, and that equals() is implemented comparing the values of each field.

Improved switch expressions

Switch statements are… indeed statements, not expressions. It means they didn’t so far return any value, or couldn’t be passed as parameter values to methods. 

Times they are a-changin! Switch borrows the arrow syntax from lambdas to get a… break from break! And they can be used as values too!

jshell> var day = "Saturday"
day ==> "Saturday"
jshell> var isWeekend = switch (day) {
...> case "Monday", "Tuesday", "Wednesday", 
...> "Thursday", "Friday" -> false;
...> case "Saturday", "Sunday" -> true;
...> default -> false;
...> }
isWeekend ==> true

Pattern matching on instanceof

Working with Apache Groovy for years, whether with its static type checking or dynamic nature, I’m quite used to skipping the redundant cast inside if (someObj instanceof SomeObject) {} blocks, thanks to smart type inference and flow typing. Java 14 takes a slightly different approach to this problem by with its pattern matching on instanceof, introducing a new local variable of the right type, rather than assuming the variable itself is of the right type. Well, it’s better explained with an example:

jshell> String name = " Guillaume "
name ==> " Guillaume "
jshell> if (name instanceof String nameString) {
...> System.out.println(nameString.trim());
...> } else {
...> System.out.println("Not a string!");
...> }

JDK 14 in a serverless container, with Cloud Run

Together we discovered the cool new syntax enhancements and constructs, and how we can play with them inside a Docker container image. But what about deploying some Java 14 powered containerized app in the cloud, in a serverless fashion? (ie. transparent scaling from 0 to 1, 1 to n, and back, paying only for what you use). For that purpose, you can easily deploy and scale containers in the cloud thanks to Cloud Run.

With the launch of Java / JDK 14, also came the first 2.0 milestone of the lovely and powerful Micronaut framework! Micronaut is probably the best framework for serverless microservices on the market, thanks to its awesome performance, lightness, in particular regarding super fast startup times. So it’s the right occasion to have fun with Micronaut again. So let’s build a Java 14 application, with Micronaut, running on Cloud Run.

Create a Micronaut app

To get started, have a look at the installation guide for Micronaut. In a nutshell, it’s using the Sdkman tool to manage versions of various SDKs. You can install Sdkman easily:

$ curl -s "" | bash

Once installed, you can also install the Micronaut command-line:

$ sdk install micronaut 2.0.0.M1

Next, we’ll create an empty app named “app” with:

$ mn create-app app

The project will be created in the app/ subdirectory, cd into it, to also create a controller, and call it hello:

$ mn create-controller hello

You’ll need to implement the controller, and tweak the app/build.gradle file to enable Java 14’s preview features.

Update this section at the bottom of the build file to add the --enable-preview flag:

tasks.withType(JavaCompile) {
options.encoding = "UTF-8"

Now, open the src/main/java/app/ class:

package app;

import io.micronaut.http.MediaType;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Produces;

public class HelloController {

public String index(String day) {
var isWeekend = switch(day.toLowerCase()) {
case "saturday", "sunday" -> true;
case "monday", "tuesday", "wednesday",
"thursday", "friday" -> false;
default -> false;

return String.format(
<!DOCTYPE html>
It's %s, it is %s the weekend!
day, (isWeekend ? "" : "not"));

Notice how you take advantage of the improved switch expression and the text block!

Create a Docker image

Micronaut’s project template comes with a default Dockerfile, but update it to look like this:

FROM openjdk:14
COPY ./ ./
RUN ./gradlew shadowJar
CMD ["java", "--enable-preview", "-jar", "build/libs/app-0.1-all.jar"]

Then build this container image (with your name of choice) with:

docker build -t IMAGE_NAME .

And check that it runs fine with this docker run command:

docker run -p 8080:8080 -it IMAGE_NAME

Then head over to or to see if it works fine.

So you now have a working Micronaut 2.0 application, running on JDK 14, using some of the new and preview features of Java 14! Congrats!

Scaling your container image in the cloud

Time to deploy your Java 14-powered Micronaut web application into the cloud, on Cloud Run. Why Cloud Run? Because with Cloud Run, you can easily push a container in production in matters of seconds. It abstracts away all the infrastructure, so you don’t have to worry about it. Google Cloud Platform handles it for you, so you can focus on your code instead. You pay proportionally to your usage: it’s serveless, so if nobody pings your app, you won’t pay anything as no container will be running. But as traffic ramps up, one or more containers will be lined up to serve your requests.

If you haven’t already, you can get started on Google Cloud Platform with its free trial (and free quota). For this tutorial however, you need to create a billing account. Once you have an account ready, create a new GCP project in the Google Cloud console. Head over to the Cloud Run section, from the hamburger menu, and click on the “Enable API” button.

Last thing before heading to the command-line, install the gcloud SDK command-line to work from your terminal. Once gcloud is installed, you can login with:

gcloud auth login

Set the project name to the one you created in the console:

gcloud config set project YOUR_PROJECT_ID

You’ll be using the fully-managed version of Cloud Run:

gcloud config set run/platform managed

Define a default region, for me, that’s gonna be europe-west1

gcloud config set run/region REGION

It’s possible to also build container images in Cloud Build (see some instructions that show this), but here you are using Docker locally to build your images. So let’s configure the Docker integration and Container Registry access with the following commands:

gcloud auth configure-docker
gcloud components install docker-credential-gcr

Tag your image with the following naming convention:

docker build . --tag

Let’s push our image to Container Registry (and change the image and project names accordingly):

docker push
gcloud run deploy weekend-service \

You should see output similar to this, showing the URL where you can access your app:

Deploying container to Cloud Run service [weekend-service] in project [YOUR_PROJECT_ID] region [europe-west1]
✓ Deploying new service... Done.
✓ Creating Revision...
✓ Routing traffic...
✓ Setting IAM Policy...
Service [weekend-service] revision [weekend-service-00001-xig] has been deployed and is serving 100 percent of traffic at

Navigate to that URL, append the name of the day, and check whether it’s weekend time!

And voilà! 

Less than a minute later, your Java 14 + Micronaut container app has been deployed to Cloud Run. Automatically, you got a secured HTTPS endpoint for your app (you can also provide your own domain name), without bothering with the infrastructure and scaling aspects.

Use the free HTML editor or subscribe for a membership to have even more features. You can purchase a license at

Serverless Tip #7 -- Create mini APIs with Cloud Functions and Express routing

  • an existing Google Cloud Platform account and project
  • Cloud Functions should be enabled for that project
Compared to the previous tip when using Exress' request path attribute, we can take advantage of Express routing.

So to support the following paths:

We can have our functions require Express by adding Express in package.json:

  "name": "mini-api-router",
  "version": "0.0.1",
  "dependencies": {
    "express": "^4.17.1"

Then we can require that dependency in our new functions script:

// we use express explicitly
const express = require('express');
const app = express();
// some customer data retrieved from Firestore or elsewhere
const customers = {
  "32": { name: 'Alice', address: '21 Jump Street' },
  "33": { name: 'Bob', address: '1 Main Street' }
// this time we can define the path easily
app.get('/', (req, res) => 
        res.send('Hello World!'));
app.get('/customers', (req, res) => 
// we can also specify path variables like :id 
// that we can retrieve via the request params object
app.get('/customers/:id', (req, res) => 
app.get('/customers/:id/address', (req, res) => 
        res.status(200).json({address: customers[].address}).end());
// we need to export the app object for Cloud Functions to expose it
exports.api = app;

More information

Serverless Tip #6 -- Create a mini Web API with Cloud Functions

  • an existing Google Cloud Platform account and project
  • Cloud Functions should be enabled for that project
We often use individual HTTP Cloud Functions as a single endpoint, and we pass data to the functions with either query parameters, or via a POST body payload. Although it's a good practice to keep the scope of a function small, however, you can easily write mini Web APIs for a given function, with different paths for different needs, like with usual Web frameworks.

So instead of having just a single endpoint with:

You can have sub-paths below the name of your function:

Let's have a look at the Node functions runtime, and how you can implement this approach. The key trick here is to use the request path: req.path, which will give you the /customers/32 part of the fully qualified URL.

// some customer data retrieved from Firestore or elsewhere
const customers = {
  "32": { name: 'Alice', address: '21 Jump Street' },
  "33": { name: 'Bob', address: '1 Main Street' }
exports.myapi = (req, res) => {
  if (req.path.startsWith('/customers')) {
    const pathElements = req.path.split('/') // split along the slashes
      .filter(e => e)                        // remove the empty strings in the array
      .splice(1);                            // remove the first "customers" element
    // path: /customers
    if (pathElements.length == 0) { 
      // return all customers
    // path: /customers/32
    else if (pathElements.length == 1) {
    // path: /customers/33/address
    else if (pathElements.length == 2 && pathElements[1] == "address") {
      res.status(200).json({address: customers[pathElements[0]].address}).end();
  res.status(404).send('Unknown path').end();

In the Node.JS runtime, Cloud Functions uses the Express framework under the hood. We have access to the request object, which has lots of useful attributes, including the path.

In this simplistic example, we are using this path attribute directly, but it's also possible to use more advanced routing capabilities, as we shall see in a forthcoming tip.

More information

Serverless Tip #5 -- How to invoke a secured Cloud Run service locally

  • an existing Google Cloud Platform account with a project
  • you have enabled the Cloud Run service and already deployed a container image
  • your local environment's gcloud is already configured to point at your GCP project
By default, when you deploy a Cloud Run service, it is secured by default, unless you use the --allow-unauthenticated flag when using the gcloud command-line (or the appropriate checkbox on the Google Cloud Console).

But once deployed, if you want to call it locally from your development machine, for testing purpose, you'll have to be authenticated.

If you look at the Cloud Console, alongside the URL of the service, you can hover the little icon next to the URL, and you'll see the a pop-up showing how you can invoke that service with a curl command:

curl -H \
    "Authorization: Bearer $(gcloud auth print-identity-token)" \
Note how a bearer token generated by the gcloud command is passed as header to the curl request.

More information

Serverless Tip #4 -- Discover the full URL of your deployed Cloud Run services with gcloud's format flag

  • an existing Google Cloud Platform account
  • you have enabled the Cloud Run service and deployed already a container image
One of the nice things with Cloud Run is that when you deploy your services, you get a URL like, with a certificate, on the domain name, etc.

You see the name of the service: myservice, the region shortcut where it was deployed: ew (Europe West), and then However, you can't guess ahead of time what the final URL will be, as there is a randomly generated part in the URL (here: 8oafjf26aq). Let's see how we can discover this whole URL.

From my terminal, I can request the list of deployed services (here, on the fully managed Cloud Run):

gcloud run services list --platform managed

It's going to show me something like the following output:

   SERVICE      REGION        URL                                        LAST DEPLOYED BY     LAST DEPLOYED AT
✔  myservice    europe-west1    2019-11-20T15:26:39.442Z

When describing the specific service (I had to specify the region as well, but you can set it by default if needed to avoid repeating yourself):

gcloud run services describe myservice --platform managed --region europe-west1

You'll see:

✔ Service hello in region europe-west1
  100%               LATEST (currently myservice-00002-dox)
Last updated on 2019-11-20T15:26:39.442Z by
  Revision myservice-00002-dox

So instead of parsing that ourselves somehow, there's a built-in way to get just the info we want, with the useful --format flag:

gcloud run services describe myservice --format='value(status.url)' --platform managed --region europe-west1 

This time, in output, you'll get just the URL, which you can then export or reuse with other commands.

The glcoud command provides three useful mechanisms to filter, format, or project the output and values returned. Here, we took advantage of format.

More information:
© 2012 Guillaume Laforge | The views and opinions expressed here are mine and don't reflect the ones from my employer.