Azure Functions with Scala using the Java Handler

In this post we look at how we create an Azure Function in Scala by making small changes to a basic Java function.
Azure Functions
Scala
Author

Chris Hansen

Published

June 19, 2022

Overview

Azure Functions is a serverless solution that lets you run code in the cloud. First-class support is provided for several languages, including C#, Java, JavaScript, PowerShell, Python, and TypeScript. This set will have a good number of people covered (though I’d love to know how many people out there are writing web services using PowerShell), though not everybody. One option is to write Functions in Scala (and, I imagine, Kotlin) using the Java handler, since Java and Scala are largely interoperable, and that is the focus of this post. Another possibility is to use a custom handler, in which case we can use pretty much any other language, and the is the focus of a different post.

Prerequisites

We can develop Azure Functions entirely locally using Azure Functions Core Tools (though you’ll need Azure CLI to publish your apps). At the time this post was written, I was running Ubuntu 22.04, and Microsoft had not released a version of the core tools that I could use. So, I decided to put all the required tools in a Docker container, but I would argue that’s a good way to work anyway. So, we create a Dockerfile with companion docker-compose.yml file as follows:

FROM ubuntu:20.04

ENV DEBIAN_FRONTEND=noninteractive
ARG NODE_VERSION=16.15.1
ARG SBT_VERSION=1.6.2

RUN apt-get update && \
  apt-get dist-upgrade -y && \
  apt-get install -y --no-install-recommends \
    gpg curl lsb-release maven openjdk-8-jdk openjdk-11-jdk openssh-server xz-utils && \
  curl -sL https://aka.ms/InstallAzureCLIDeb | bash && \
  curl -s https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > microsoft.gpg && \
  mv microsoft.gpg /etc/apt/trusted.gpg.d/microsoft.gpg && \
  sh -c 'echo "deb [arch=amd64] https://packages.microsoft.com/repos/microsoft-ubuntu-$(lsb_release -cs)-prod $(lsb_release -cs) main" > /etc/apt/sources.list.d/dotnetdev.list' && \
  apt-get update && \
  apt-get install -y --no-install-recommends \
    azure-functions-core-tools-4 azure-cli dotnet-sdk-3.1 && \
  mkdir -p /usr/local/node && \
  curl -sL https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz | \
    tar xJvf - --strip-components=1 -C /usr/local/node && \
  echo "export PATH=\$PATH:/usr/local/node/bin" >> /etc/profile && \
  PATH=$PATH:/usr/local/node/bin npm install --location=global azurite && \
  echo "export JAVA_HOME=/usr" >> /etc/profile

EXPOSE 22

CMD mkdir -p /root/.ssh && \
  echo "$PUB_KEY" >> /root/.ssh/authorized_keys && \
  chmod 700 /root/.ssh && chmod 600 /root/.ssh/* && \
  service ssh start && \
  tail -f /dev/null
version: "3.8"
services:
  development:
    image: azure
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "24:22"
    environment:
      - "PUB_KEY=${PUB_KEY}" 
    volumes:
      - azuredev-azfct:/root/.azure-functions-core-tools
      - azuredev-cache:/root/.cache
      - azuredev-mvn:/root/.m2
      - azuredev-vscode:/root/.vscode-server
      - azuredev-npm:/root/.npm
      - ./work:/root/work
volumes:
  azuredev-azfct:
  azuredev-cache:
  azuredev-mvn:
  azuredev-vscode:
  azuredev-npm:

This provides a single container with the following components:

  • Azure Functions Core Tools for local development
  • Azure CLI to deoploy to Azure
  • Azurite for local simulation of cloud storage: event queue, BLOB, and table storage
  • Java development kit for compilation of Java (and Scala) code
  • Maven to manage projects
  • OpenSSH sever to allow us to connect to running containers via SSH

To run an instance (and build the image if it doesn’t already exist):

docker compose up -d

I typically add an entry to my SSH config and then access the running container via the remote extension. That is, I add something like the following to ~/config:

Host azuredev
  HostName localhost
  Port 24
  User root

A Simple Java Function

To create a basic Java function, run the following command:

mvn archetype:generate \
  -DarchetypeGroupId=com.microsoft.azure \
  -DarchetypeArtifactId=azure-functions-archetype \
  -DjavaVersion=8

When doing this, users will be prompted to provide values for several parameters. In this example, we provide values as follows:

parameter value
groupId org.example
artifactId hellojava
version 1.0-SNAPSHOT
package org.example

After doing this, a folder named hellojava is created, with the following layout:

hellojava
├── host.json
├── local.settings.json
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── org
    │           └── example
    │               └── Function.java
    └── test
        └── java
            └── org
                └── example
                    ├── FunctionTest.java
                    └── HttpResponseMessageMock.java

The most important files here are Function.java and pom.xml. Function.java contains the logic for our function, and pom.xml lists all the dependencies and other metadata Maven needs to build and run our project.

package org.cmhh;

import com.microsoft.azure.functions.ExecutionContext;
import com.microsoft.azure.functions.HttpMethod;
import com.microsoft.azure.functions.HttpRequestMessage;
import com.microsoft.azure.functions.HttpResponseMessage;
import com.microsoft.azure.functions.HttpStatus;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;

import java.util.Optional;

/**
 * Azure Functions with HTTP Trigger.
 */
public class Function {
    /**
     * This function listens at endpoint "/api/HttpExample". Two ways to invoke it using "curl" command in bash:
     * 1. curl -d "HTTP Body" {your host}/api/HttpExample
     * 2. curl "{your host}/api/HttpExample?name=HTTP%20Query"
     */
    @FunctionName("HttpExample")
    public HttpResponseMessage run(
            @HttpTrigger(
                name = "req",
                methods = {HttpMethod.GET, HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS)
                HttpRequestMessage<Optional<String>> request,
            final ExecutionContext context) {
        context.getLogger().info("Java HTTP trigger processed a request.");

        // Parse query parameter
        final String query = request.getQueryParameters().get("name");
        final String name = request.getBody().orElse(query);

        if (name == null) {
            return request.createResponseBuilder(HttpStatus.BAD_REQUEST).body("Please pass a name on the query string or in the request body").build();
        } else {
            return request.createResponseBuilder(HttpStatus.OK).body("Hello, " + name).build();
        }
    }
}
<?xml version="1.0" encoding="UTF-8" ?>
<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>org.cmhh</groupId>
    <artifactId>hellojava</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Azure Java Functions</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
        <azure.functions.maven.plugin.version>1.18.0</azure.functions.maven.plugin.version>
        <azure.functions.java.library.version>2.0.0</azure.functions.java.library.version>
        <functionAppName>org-20220618002424789</functionAppName>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.microsoft.azure.functions</groupId>
            <artifactId>azure-functions-java-library</artifactId>
            <version>${azure.functions.java.library.version}</version>
        </dependency>

        <!-- Test -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.4.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-core</artifactId>
            <version>2.23.4</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>com.microsoft.azure</groupId>
                <artifactId>azure-functions-maven-plugin</artifactId>
                <version>${azure.functions.maven.plugin.version}</version>
                <configuration>
                    <!-- function app name -->
                    <appName>${functionAppName}</appName>
                    <!-- function app resource group -->
                    <resourceGroup>java-functions-group</resourceGroup>
                    <!-- function app service plan name -->
                    <appServicePlanName>java-functions-app-service-plan</appServicePlanName>
                    <!-- function app region-->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-regions for all valid values -->
                    <region>westus</region>
                    <!-- function pricingTier, default to be consumption if not specified -->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details#supported-pricing-tiers for all valid values -->
                    <!-- <pricingTier></pricingTier> -->
                    <!-- Whether to disable application insights, default is false -->
                    <!-- refers https://github.com/microsoft/azure-maven-plugins/wiki/Azure-Functions:-Configuration-Details for all valid configurations for application insights-->
                    <!-- <disableAppInsights></disableAppInsights> -->
                    <runtime>
                        <!-- runtime os, could be windows, linux or docker-->
                        <os>windows</os>
                        <javaVersion>8</javaVersion>
                    </runtime>
                    <appSettings>
                        <property>
                            <name>FUNCTIONS_EXTENSION_VERSION</name>
                            <value>~4</value>
                        </property>
                    </appSettings>
                </configuration>
                <executions>
                    <execution>
                        <id>package-functions</id>
                        <goals>
                            <goal>package</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!--Remove obj folder generated by .NET SDK in maven clean-->
            <plugin>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>obj</directory>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

We can build and run our function locally by running:

$ mvn clean package
$ mvn azure-functions:run

All going well, your app will now be running, most likely on port 7071. Visiting http://localhost:7071 we should see:

Function.java provides a single function, and the @FunctionName annotation tells us that the name is HttpExample. The signature of the run method tells us that this is an HttpTrigger (meaning it runs every time we make an HTTP request), and that it expects a single parameter called name, an optional string. We can call the function by GETting or POSTing:

curl http://localhost:7071/api/HttpExample?name=Java!
Hello, Java!

or

curl -d "Java!" http://localhost:7071/api/HttpExample
Hello, Java!

A Simple Scala Function

We can convert our simple Java function by adding Scala as a dependecy to pom.xml, as well as the Scala Maven plugin; and by replacing src/main/java/org/example/Function.java with src/main/scala/org/example/Function.java. These two files are as follows:

package org.cmhh

import com.microsoft.azure.functions.{ExecutionContext, HttpMethod, HttpRequestMessage, HttpResponseMessage, HttpStatus}
import com.microsoft.azure.functions.annotation.{AuthorizationLevel, FunctionName, HttpTrigger}
import java.util.Optional

/**
 * Convert java Optional to Scala Option
 */
object utils {
  def toOption[T](x: Optional[T]): Option[T] = {
    if (x.isPresent) Option(x.get()) else None
  }
}

/**
 * Azure Functions with HTTP Trigger.
 */
class Function {
  /**
  * curl -d "HTTP Body" {your host}/api/HttpExample
  * curl "{your host}/api/HttpExample?name=HTTP%20Query"
  */
  @FunctionName("HttpExample")
  def run(
    @HttpTrigger(
      name = "req",
      methods = Array(HttpMethod.GET, HttpMethod.POST),
      authLevel = AuthorizationLevel.ANONYMOUS
    )
    request: HttpRequestMessage[Optional[String]],
    context: ExecutionContext
  ): HttpResponseMessage = {
    context.getLogger().info("Scala HTTP trigger processed a request.")

    val name: Option[String] = utils.toOption(request.getBody()) match {
      case None => Option(request.getQueryParameters().get("name"))
      case Some(s) => Some(s)
    }

    name match {
      case None => 
        request
          .createResponseBuilder(HttpStatus.BAD_REQUEST)
          .body("Please pass a name on the query string or in the request body")
          .build
      case Some(s) =>
        request
          .createResponseBuilder(HttpStatus.OK)
          .body(s"Hello, $s")
          .build
    }
  }
}
<?xml version="1.0" encoding="UTF-8" ?>
<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>org.cmhh</groupId>
  <artifactId>helloscala</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>Azure Scala Functions</name>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <azure.functions.maven.plugin.version>1.18.0</azure.functions.maven.plugin.version>
    <azure.functions.java.library.version>2.0.0</azure.functions.java.library.version>
    <functionAppName>org-20220618002424789</functionAppName>
  </properties>
  <dependencies>
    <dependency>
      <groupId>com.microsoft.azure.functions</groupId>
      <artifactId>azure-functions-java-library</artifactId>
      <version>${azure.functions.java.library.version}</version>
    </dependency>
    <!-- Test -->
    <dependency>
      <groupId>org.junit.jupiter</groupId>
      <artifactId>junit-jupiter</artifactId>
      <version>5.4.2</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.mockito</groupId>
      <artifactId>mockito-core</artifactId>
      <version>2.23.4</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.scala-lang</groupId>
      <artifactId>scala-library</artifactId>
      <version>2.13.8</version>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <source>${java.version}</source>
          <target>${java.version}</target>
          <encoding>${project.build.sourceEncoding}</encoding>
        </configuration>
      </plugin>
      <plugin>
        <groupId>com.microsoft.azure</groupId>
        <artifactId>azure-functions-maven-plugin</artifactId>
        <version>${azure.functions.maven.plugin.version}</version>
        <configuration>
          <appName>${functionAppName}</appName>
          <resourceGroup>java-functions-group</resourceGroup>
          <appServicePlanName>java-functions-app-service-plan</appServicePlanName>
          <region>westus</region>
          <runtime>
            <os>linux</os>
            <javaVersion>8</javaVersion>
          </runtime>
          <appSettings>
              <property>
                <name>FUNCTIONS_EXTENSION_VERSION</name>
                <value>~4</value>
              </property>
          </appSettings>
        </configuration>
        <executions>
          <execution>
            <id>package-functions</id>
            <goals>
              <goal>package</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <!--Remove obj folder generated by .NET SDK in maven clean-->
      <plugin>
        <artifactId>maven-clean-plugin</artifactId>
        <version>3.1.0</version>
        <configuration>
          <filesets>
            <fileset>
              <directory>obj</directory>
            </fileset>
          </filesets>
        </configuration>
      </plugin>
      <plugin>
        <groupId>net.alchim31.maven</groupId>
        <artifactId>scala-maven-plugin</artifactId>
        <version>4.6.3</version>
        <executions>
          <execution>
            <goals>
              <goal>compile</goal>
              <goal>testCompile</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

We build and run the function exactly as before but, crucially, we can now write all our code in Scala, as we would for any other Scala project. Cool.