Azure Function for Java developers — Spring Cloud Function in Azure[Updated with Spring Boot3]

Jay Lee
Microsoft Azure
Published in
9 min readFeb 27, 2023

According to the 2022 State of Serverless report by Datadog, it’s clear that Azure Function has been adopted by over 40% of Azure customers, demonstrating the widespread usage of FaaS in the cloud. Using Java as a runtime for FaaS is something that developers would want to debate, but it’s undeniable that Java developers tend to prefer Java runtime for FaaS due to familiarity and productivity. This is the first article in a series on developing Azure Function with Java.

Before Getting Started

If you have never created Azure Function before, you need to do one thing immediately which is to install Azure Function Core Tools. This tool is to create and executes functions locally. Please visit at https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4%2Cmacos%2Ccsharp%2Cportal%2Cbash and install it before proceeding.

Create Spring Boot application

Let’s create a Spring Boot app using Spring initializr. We will start as if we’re creating a plain CRUD application with an H2 database.

Spring initializr

The sample code is at https://github.com/eggboy/springcloud-azurefunction

$ git clone https://github.com/eggboy/springcloud-azurefunction

The sample app has an endpoint, /user with basic GET/POST HTTP method.

$ curl -i -X POST -H "Content-Type: application/json" \
-d "{\"userId\": \"jayuser1\",\"lastName\": \"Jay\",\"firstName\": \"lee\", \"email\":\"jaylee@email.com\"}" \
http://127.0.0.1:8080/user
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 8
Date: Sat, 25 Feb 2023 02:02:46 GMT

jayuser1

$ curl http://localhost:8080/user/jayuser1
{"userId":"jayuser1","lastName":"Jay","firstName":"lee","email":"jaylee@email.com"}

Bootstrap Spring Boot with Azure Function

For those of you who never developed Azure Functions before, I’d like to prep you with the basics of Azure Function so the following steps would make sense to you. At the core, Azure Functions are written in .NET core. Even if you use Java as runtime, the Azure Function host (which is .NET based) gets fired up first and then Java gets started as a subprocess. Now, you might be thinking this is unnecessary complexity, but it actually makes sense because the core is built to be able to support polyglot languages beyond .NET, and can also support triggers and bindings such as HTTP, RabbitMQ, Service Bus, etc.

Azure Functions Host

ps in running function environment below clearly shows as I explained above. Function Host(PID 30) is the parent process of the Java process(PID 52).

root@7e42a04fee0b:~/site/wwwroot# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 04:26 pts/0 00:00:00 bash /azure-functions-host/start.sh
root 30 1 1 04:26 pts/0 00:00:03 /azure-functions-host/Microsoft.Azure.WebJobs.Script.WebHost
root 52 30 0 04:26 pts/0 00:00:01 /usr/lib/jvm/msft-17-x64/bin/java -XX:+TieredCompilation -XX:TieredStopAtLevel=1 -nov

One thing to note is that PID 52 is not our Spring Boot app, it’s an Azure Function Java worker process. — https://github.com/Azure/azure-functions-java-worker Java worker process expects a certain project structure that it can read from, and that is handled by the maven with the Azure Function plug-in. We will look into it later.

Bootstrapping the Spring Boot with Azure Function is a three-step process.

  1. Configure Function Host with Triggers, Bindings, and Extensions
  2. Replace inbound parts with Azure Function Triggers — @RestController to @HttpTrigger, @StreamListener to @ServiceBusTopicTrigger or @RabbitMQTrigger, etc.
  3. Configure pom.xml so that it can package Functions properly

Let’s begin. Function Host is expectinghost.json which is the configuration file for Triggers, Bindings, Extensions, etc. Here are the details of each configuration in the file. HttpTrigger is included in extensionBundle, and it doesn’t require any additional setup other than the version specification.

$ ls
HELP.md host.json pom.xml src

$ cat host.json
{
"version": "2.0",
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[4.*, 5.0.0)"
},
"functionTimeout": "00:10:00"
}

Another file is to be created, local.settings.json is to configure the settings for the local environment.

$ cat local.settings.json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "",
"FUNCTIONS_WORKER_RUNTIME": "java",
"FUNCTIONS_EXTENSION_VERSION": "~4",
"AzureWebJobsDashboard": ""
}
}

The second step of replacing inbounds requires a code-level change. We will create an additional package function, and create a class called GetUserFunctionHandler. This is to replace the @RestController.

package io.jaylee.springcloud.function;

import com.microsoft.azure.functions.*;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;
import io.jaylee.springcloud.model.UserDTO;
import org.springframework.cloud.function.adapter.azure.FunctionInvoker;

import java.util.Optional;

public class GetUserFunctionHandler extends FunctionInvoker<String, UserDTO> {
@FunctionName("getUserFunction")
public HttpResponseMessage execute(
@HttpTrigger(name = "request", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
ExecutionContext context) {

String userId = request.getQueryParameters().get("userId");

context.getLogger().info(userId);
return request
.createResponseBuilder(HttpStatus.OK)
.body(handleRequest(userId, context))
.header("Content-Type", "application/json")
.build();
}
}

NOTE: Each endpoint (@GetMapping, @PostMapping, etc.) in @RestController needs to be individually separated as a class.

Code must explain itself. @HttpTrigger exposes HttpMethod.GET, and it will execute the function which has the name as “getUserFunction”. It will read the query parameter “userid” and return the result type as UserDTO. To compile this code, we gotta add a few dependencies in the pom.xml

    <dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-adapter-azure</artifactId>
<version>4.0.1</version>
</dependency>
...
</dependencies>
<dependencyManagement>
...
<dependency>
<groupId>com.microsoft.azure.functions</groupId>
<artifactId>azure-functions-java-library</artifactId>
<version>3.0.0</version>
</dependency>
</dependencies>
</dependencyManagement>

The spring-cloud-function-adapter-azure is a crucial dependency that acts as a bridge between Azure Function and Spring Cloud Function. Essentially, @FunctionNam from Azure SDK is hooked by Spring Cloud Function, enabling the function getUserFunction to be looked up from Spring Cloud Function registry and executed once handleRequest is invoked. It should sound straightforward so far except for one thing. How do we register functions to Spring Cloud Function registry? Here comes the Configuration class.

package io.jaylee.function.springfunction.function;

import io.jaylee.function.springfunction.model.UserDTO;
import io.jaylee.function.springfunction.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.function.Function;

@Configuration
public class FunctionRegistration {

private UserService userService;

public FunctionRegistration(UserService userService) {
this.userService = userService;
}

@Bean
public Function<UserDTO, String> createUserFunction() {
return user -> userService.saveUser(user);
}

@Bean
public Function<String, UserDTO> getUserFunction() {
return userId -> userService.getUser(userId);
}
}

The key here is to match the name @FunctionName with the method name so that the Spring Cloud Function registry can retrieve the function accordingly.

This is it for step 1 and 2. Step 3 is to configure pom.xml to properly compile and package the Spring Boot app in the structure that Azure Function can interpret and execute. Azure Function SDK provides a Maven plug-in azure-functions-maven-plugin for that.

...
<properties>
<java.version>17</java.version>
<spring-cloud.version>2021.0.5</spring-cloud.version>

<functionResourceGroup>function-rg</functionResourceGroup>
<functionAppServicePlanName>spring-function-service-plan</functionAppServicePlanName>
<functionAppName>springcloud-azurefunction</functionAppName>
<functionAppRegion>westus</functionAppRegion>
<functionPricingTier>Y1</functionPricingTier>

<start-class>io.jaylee.function.springcloud.AzureFunctionApplication</start-class>
</properties>
...

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<version>1.23.0</version>
</plugin>
</plugins>
</pluginManagement>

<plugins>
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<configuration>
<resourceGroup>${functionResourceGroup}</resourceGroup>
<appName>${functionAppName}</appName>
<region>${functionAppRegion}</region>
<appServicePlanName>${functionAppServicePlanName}</appServicePlanName>
<pricingTier>${functionPricingTier}</pricingTier>

<hostJson>${project.basedir}/host.json</hostJson>
<localSettingsJson>${project.basedir}/local.settings.json</localSettingsJson>

<runtime>
<os>linux</os>
<javaVersion>17</javaVersion>
</runtime>
<appSettings>
<!-- Run Azure Function from package file by default -->
<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>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

<start-class> in the properties is easily overlooked but it is super important. This tells Azure Function the entry point of our app, guiding Azure Function Java Worker on how to start the application. Briefly looking at the configuration, <hostJson> and <localSettingsJson> should be set for the proper location where those files reside in the project. runtime and JVM are set for linux and 17 respectively.

All 3 steps are done at this point, it’s time to build and run the function locally. The final source code is in azurefunction branch.

$ git checkout azurefunction
$ mvn clean package
...

package creates the subfolder at ./target/azure-functions/springcloud-azurefunction, showing the exact structure of Azure Function in runtime. I highly recommend taking a look at each file in there. Once mvn package is done, run the function locally.


$ mvn azure-functions:run
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< io.jaylee:springcloud >------------------------
[INFO] Building azurefunction 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- azure-functions:1.23.0:run (default-cli) @ springcloud ---
[INFO] Function App's staging directory found at: /Users/jaylee/Desktop/springcloud/target/azure-functions/springcloud-azurefunction
4.0.4915
[INFO] Azure Functions Core Tools found.

Azure Functions Core Tools
Core Tools Version: 4.0.4915 Commit hash: N/A (64-bit)
Function Runtime Version: 4.14.0.19631

[2023-02-27T04:01:23.555Z] OpenJDK 64-Bit Server VM warning: Options -Xverify:none and -noverify were deprecated in JDK 13 and will likely be removed in a future release.

Functions:

createUserFunction: [POST] http://localhost:7071/api/createUserFunction

getUserFunction: [GET] http://localhost:7071/api/getUserFunction

For detailed output, run func with --verbose flag.
[2023-02-27T04:01:24.317Z] Worker process started and initialized.

Deployment to Azure can be also done using maven. It creates a resource group and a service plan in case they’re missing.

$ mvn azure-functions:deploy
[INFO] Scanning for projects...
[INFO]
[INFO] -----------------------< io.jaylee:springcloud >------------------------
[INFO] Building azurefunction 0.0.1-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- azure-functions:1.23.0:deploy (default-cli) @ springcloud ---
[INFO] Auth type: AZURE_CLI
[INFO] Default subscription: ***
[INFO] Username: ***
[INFO] Subscription: ***
[INFO] Reflections took 329 ms to scan 6 urls, producing 26 keys and 786 values
[INFO] Start creating Resource Group(function-rg) in region (West US)...
[INFO] Using service version null
[INFO] Using service version null
[INFO] Resource Group(function-rg) is successfully created.
[INFO] Reflections took 24 ms to scan 3 urls, producing 12 keys and 12 values
[INFO] Start creating App Service plan (spring-function-service-plan)...
[INFO] App Service plan (spring-function-service-plan) is successfully created
[INFO] Start creating Application Insights (springcloud-azurefunction)...
[INFO] Application Insights (springcloud-azurefunction) is successfully created. You can visit *** to view your Application Insights component.
[INFO] Set function worker runtime to java.
[INFO] Start creating Function App(springcloud-azurefunction)...
[INFO] Function App(springcloud-azurefunction) is successfully created
[INFO] Starting deployment...
[INFO] Trying to deploy artifact to springcloud-azurefunction...
[INFO] Successfully deployed the artifact to https://springcloud-azurefunction.azurewebsites.net
[INFO] Deployment done, you may access your resource through springcloud-azurefunction.azurewebsites.net
[INFO] Syncing triggers and fetching function information
[INFO] Querying triggers...
[INFO] HTTP Trigger Urls:
[INFO] createUserFunction : https://springcloud-azurefunction.azurewebsites.net/api/createuserfunction
[INFO] getUserFunction : https://springcloud-azurefunction.azurewebsites.net/api/getuserfunction
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 03:31 min
[INFO] Finished at: 2023-02-27T11:30:42+08:00
[INFO] ------------------------------------------------------------------------

Azure Portal has a developer view where you can see the configuration of Azure Function and even do the simple test on the spot.

Test Azure Function on Azure portal
Azure Function Integration on Azure portal

[Update] Spring Boot 3

Since legacy FunctionInvoker programming model is deprecated, it requires an update with Spring Boot 3. This change greatly simplifies the usage of Azure Function with more spring friendly function definition.

All you need to do is to use @FunctionName , the rest are handled automatically without creating FunctionInvoker explicitly as before.

package io.jaylee.springcloud.function;

import com.microsoft.azure.functions.*;
import com.microsoft.azure.functions.annotation.AuthorizationLevel;
import com.microsoft.azure.functions.annotation.FunctionName;
import com.microsoft.azure.functions.annotation.HttpTrigger;
import io.jaylee.springcloud.model.UserDTO;
import io.jaylee.springcloud.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
@RequiredArgsConstructor
public class FunctionHandler {

private final UserService userService;

@FunctionName("createUserFunction")
public HttpResponseMessage createUser(
@HttpTrigger(name = "request", methods = {HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<UserDTO>> request,
ExecutionContext context) {

if (request == null) {
throw new IllegalArgumentException("Request cannot be null");
}

context.getLogger()
.info(request.getBody()
.map(UserDTO::toString).orElse(""));
userService.saveUser(request.getBody().orElseThrow());

return request
.createResponseBuilder(HttpStatus.OK)
.header("Content-Type", "application/json")
.build();
}

@FunctionName("getUserFunction")
public HttpResponseMessage getUser(
@HttpTrigger(name = "request", methods = {HttpMethod.GET}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
ExecutionContext context) {
String userId = request.getQueryParameters().get("userId");

if (userId == null || "".equals(userId)) {
throw new IllegalArgumentException("Request cannot be null");
}

context.getLogger().info(userId);
return request
.createResponseBuilder(HttpStatus.OK)
.body(userService.getUser(userId))
.header("Content-Type", "application/json")
.build();
}
}

You can see the entire source in springboot3 branch.

$ git checkout springboot3

Wrapping Up

I tried to write this article as if I were a Spring developer who wants to write an Azure Function for the first time, ensuring that the flow and logic are intuitively coherent. I hope it worked as I originally intended. Keep an eye out for upcoming articles, including Spring Native with Azure Function and Azure Function in the container. Stay tuned for more!

If you liked my article, please leave a few claps or start following me. You can get notified whenever I publish something new. Let’s stay connected on Linkedin, too! Thanks so much for reading!

--

--

Jay Lee
Microsoft Azure

Cloud Native Enthusiast. Java, Spring, Python, Golang, Kubernetes.