Azure Functions with Spring Cloud — Introduction
Glossary
To be well-prepared to read this article, we need to define a couple of buzzwords that everyone who wants to deploy Azure Functions should know.
What the heck are those functions? The crucial thing to remember is — serverless. We don’t need to worry about infrastructure. We only create code that covers all our business logic. That’s all!
We can think of it like running scripts in the Linux Shell. If we want to run a script, we provide only input data; a Shell script has some logic, and as a result, there is some output. Our favorite Linux distribution takes care of running it properly. We don’t need to maintain our own ‘infrastructure’ to run it.
Now, somehow, we need to connect our business logic with Azure Functions. This can be achieved by using Spring Cloud Function (Azure Adapter). With this adapter, we have a very convenient way to build, test locally, and deploy our application. This library behaves like a typical Spring application, which delights us.
The described flow looks:
Hello World!
Let’s try our first “Hello World!” application. The flow will be pretty straightforward. We put some text as an input, and we return its uppercased transformation. Just as simple as that.
Let’s look at the flow:
To begin, we only need to generate a very basic Spring application using start.spring.io:
Here is the information about all supported Java versions.
As a first step we need to add the spring-cloud-function-adapter-azure
dependency. This allows us to integrate our
codebase with the Azure Functions.
implementation 'org.springframework.cloud:spring-cloud-function-adapter-azure:4.1.1'
Our codebase is divided into two simple pieces.
UppercaseFunction
with the business logic:
@Component
public class UppercaseFunction implements Function<String, String> {
@Override
public String apply(String message) {
// Get input data to uppercase
String payload = hasText(message) ? message : "default";
// Transform using uppercase function
return payload.toUpperCase();
}
}
At this point, we can run the application locally (without integrating with Azure Functions) using
the spring-cloud-starter-function-web
dependency. Spring Cloud Function automatically exposes all functions as HTTP
endpoints (Standalone Web Applications):
implementation 'org.springframework.cloud:spring-cloud-starter-function-web:4.1.1'
We can check our business logic and output before we deploy or even test the application with Azure Functions by calling exposed endpoints:
AzureUppercaseHandler
describes the trigger when the business logic is called:
@Component
public class AzureUppercaseHandler {
private final UppercaseFunction uppercaseFunction;
public AzureUppercaseHandler(UppercaseFunction uppercaseFunction) {
this.uppercaseFunction = uppercaseFunction;
}
@FunctionName("uppercase")
public String execute(
@HttpTrigger(
name = "request",
// Run for GET and POST requests
methods = {GET, POST},
// Without authorization
authLevel = ANONYMOUS
) HttpRequestMessage<Optional<String>> request,
ExecutionContext context
) {
String body = request.getBody().orElse(null);
// We can log something
context.getLogger()
.info("Trying to uppercase string [%s]...".formatted(body));
return uppercaseFunction.apply(body);
}
}
Run locally
With the application at that stage, we can try to run it locally using an Azure Functions Plugin for Gradle.
1.14.0
. This is due
to poorly handled pricingTier
and region
parameters.Let’s add the Gradle plugin and configure it, putting (for now) only an appName
property:
azurefunctions {
appName = 'uppercase'
}
To run our application locally, we need to run a Gradle task which is provided by the Azure Functions Plugin for Gradle.
./gradlew clean azureFunctionsRun
The following output should be produced:
So let’s try to call our uppercase
function. The entry point is http://localhost:7071/api/uppercase
.
💥 BOOOM! 💥
Unfortunately, our application isn’t working. We received the following exception:
INFO 1095179 --- [uppercase] [pool-2-thread-1] o.s.boot.SpringApplication : Started application in 1.595 seconds (process running for 17.217)
Executed 'Functions.uppercase' (Failed, Id=5555b7a4-5925-4a92-b1ee-6f2df865cb36, Duration=1990ms)
System.Private.CoreLib: Exception while executing function: Functions.uppercase. System.Private.CoreLib: Result: Failure
Exception: IllegalStateException: Failed to retrieve Bean instance for: class codes.piotrooo.azurefunctions.service.AzureUppercaseHandler. The class should be annotated with @Component to let the Spring framework initialize it!
Stack: java.lang.IllegalStateException: Failed to initialize
at org.springframework.cloud.function.adapter.azure.AzureFunctionInstanceInjector.getInstance(AzureFunctionInstanceInjector.java:80)
at com.microsoft.azure.functions.worker.binding.ExecutionContextDataSource.getFunctionInstance(ExecutionContextDataSource.java:103)
at com.microsoft.azure.functions.worker.broker.JavaMethodInvokeInfo.invoke(JavaMethodInvokeInfo.java:20)
at com.microsoft.azure.functions.worker.broker.EnhancedJavaMethodExecutorImpl.execute(EnhancedJavaMethodExecutorImpl.java:22)
at com.microsoft.azure.functions.worker.chain.FunctionExecutionMiddleware.invoke(FunctionExecutionMiddleware.java:19)
at com.microsoft.azure.functions.worker.chain.InvocationChain.doNext(InvocationChain.java:21)
at com.microsoft.azure.functions.worker.broker.JavaFunctionBroker.invokeMethod(JavaFunctionBroker.java:125)
at com.microsoft.azure.functions.worker.handler.InvocationRequestHandler.execute(InvocationRequestHandler.java:34)
at com.microsoft.azure.functions.worker.handler.InvocationRequestHandler.execute(InvocationRequestHandler.java:10)
at com.microsoft.azure.functions.worker.handler.MessageHandler.handle(MessageHandler.java:44)
at com.microsoft.azure.functions.worker.JavaWorkerClient$StreamingMessagePeer.lambda$onNext$0(JavaWorkerClient.java:94)
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572)
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.lang.IllegalStateException: Failed to retrieve Bean instance for: class codes.piotrooo.azurefunctions.service.AzureUppercaseHandler. The class should be annotated with @Component to let the Spring framework initialize it!
at org.springframework.cloud.function.adapter.azure.AzureFunctionInstanceInjector.getInstance(AzureFunctionInstanceInjector.java:70)
... 15 more
The most important (and interesting) part in this stack trace is:
Failed to retrieve Bean instance for: class codes.piotrooo.azurefunctions.service.AzureUppercaseHandler.
The class should be annotated with @Component to let the Spring framework initialize it!
But how?! We added a @Component
stereotype to the AzureUppercaseHandler
class, so what is the reason for
complaining?
If we take a closer look at the generated output, we can notice multiple lines with the following message:
Searching for start class in manifest:
and:
org.springframework.cloud.function.utils.FunctionClassUtils -- Loaded Start Class: class org.springframework.cloud.function.web.RestApplication
org.springframework.cloud.function.utils.FunctionClassUtils -- Main class: class org.springframework.cloud.function.web.RestApplication
The main class that is being picked up is definitely NOT our application’s starting point. Upon further investigation and digging deeper, we arrive at the FunctionClassUtils class.
If we examine
the FunctionClassUtils.getStartClass()
method
— in the Javadoc — we find the following description:
/**
* Discovers the start class in the currently running application.
* The discover search order is 'MAIN_CLASS' environment property,
* 'MAIN_CLASS' system property, META-INF/MANIFEST.MF:'Start-Class' attribute,
* meta-inf/manifest.mf:'Start-Class' attribute.
*/
This gives us a hint that we don’t have an entry point for our application to load all the needed classes. We need to
create a MANIFEST.MF
file with the Start-Class
header. The easiest way to create it in the generated JAR
is by
putting it in the build.gradle
file.
jar {
manifest {
attributes(
'Start-Class': 'codes.piotrooo.azurefunctions.UppercaseApplication'
)
}
}
Now, we can try to run the application once again. Let’s try to uppercase something!
If there is an error, check if the previous application releases the port. In the Gradle output, you can see a message:
Port 7071 is unavailable. Close the process using that port, or specify another port using --port [-p].
You may need to terminate a previously started process that uses port 7071
.
The netstat -tulpn | grep LISTEN
command could be useful.
In the application logs we can also see our log statement:
Trying to uppercase string [running locally!]...
Perfect! 👌
At this point, it’s also noteworthy that the version
— specified in the build.gradle
file or through command line
parameters (-Pversion=X
) — is required. Without passing the version
parameter, we may encounter the following
error:
Caused by: org.gradle.api.GradleException: Cannot package functions due to error: Azure Functions entry point not found, plugin will exit.
at com.microsoft.azure.plugin.functions.gradle.task.PackageTask.build(PackageTask.java:56)
The GradleProjectUtils
class
in the Azure Functions Plugin for Gradle uses a Gradle version
property. Without this defined property, the plugin
cannot determine the build artifact file.
Deploy to Azure
Before you try to deploy something, you must be sure that you are connected to the correct Azure subscription. You can check it by:
To log in to the Azure and set Azure subscription, you may need to run the following commands:
az login
az account set --subscription <subscription-id>
To deploy something, we need to make some adjustments to the Azure Functions Plugin for Gradle.
azurefunctions {
resourceGroup = 'PO'
appName = 'uppercase'
region = 'westeurope'
runtime {
os = 'linux'
javaVersion = '17'
}
auth {
type = 'azure_cli'
}
appSettings {
FUNCTIONS_EXTENSION_VERSION = '~4'
}
}
With this configuration, the plugin will create all necessary objects in the Azure Resource Group, which is called PO
in our sample.
The Gradle plugin provides us with a task called azureFunctionsDeploy
. Let’s try it by calling:
./gradlew clean azureFunctionsDeploy
Let’s take a closer look at what was created on Azure:
The Azure Functions Plugin for Gradle handled everything for us. It created a Function App
named uppercase and
also set up the necessary Storage account
for internal files. Additionally, it created Application Insight
,
providing many features to enhance the performance, reliability, and quality of our application.
Now it’s demo time! Let’s try converting something to uppercase, but this time — using Azure Functions. In the previously generated output, we can see an endpoint to make a request:
The generated log statements are also available in the Azure Portal:
We carried out our mission! The function is up and running, and what is more, we don’t even create any virtual machine, Kubernetes cluster, or any of the ‘infrastructure’ stuff. It’s awesome! That’s the power of the Azure Functions and general serverless programming.
The code which covers this article is available on the GitHub repository.