Handling Webhooks with Java, Spring Cloud Function and Azure Functions

March 08, 2021
Written by
Reviewed by
Liz Moy
Twilion

Title: Handling Webhooks with Java, Spring Cloud Function and Azure Functions

When you want to send an SMS or a WhatsApp message using Twilio's API, the programming model is straightforward: You call the API and the message gets sent. Responding to incoming messages is a different question. When the message arrives Twilio needs to find out how to handle it, and the mechanism we use is the Webhook. You configure your Twilio number with a URL and the platform makes a request to it passing some details about the incoming message - your app's response decides what happens next.

This means your app needs a public URL, which in turn means that you need some hosting. In the last few years serverless platforms have become popular for this, because they alleviate many of the pains of hosting. You provide code, which can handle an individual request, and the platform takes care of routing, scaling and most other concerns. Java's no exception to this trend and Spring Cloud Function was created to give Java developers a consistent programming model for serverless platforms while being able to use other popular features of Spring like dependency injection.

In this post I'll show how to create a serverless Java function using Spring Cloud Function and deploy it to Azure Functions. The function will act as a Webhook to instruct Twilio how to respond to an incoming SMS. It won't do anything fancy, but once you've got a serverless function responding to SMS you will have the full power of Java at your fingertips.

Prerequisites

To work along with this tutorial you'll need:

If you want to skip to the end, the full code for this project is on GitHub. If you want to see how it's built up, then keep reading!

Let's get cracking

To start a new Spring project, I always use the Spring Initializr. This link will pre-select a few important things, including the Spring Cloud Function dependency.

Generate and unzip the generated project then open it in your IDE. The Spring template saves a lot of time - you can get to coding right away. Create a new package named com.example.webhooks.functions, and add a class in there called WebhookHandler.

package com.example.webhooks.functions;

import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.function.Function;

@Component
public class WebhookHandler {

        @Bean
        public Function<String, String> respondToSms(){
            return String::toUpperCase;
        }
}

[this code on GitHub]

Note that the respondToSms function takes no arguments and returns a java.util.Function that turns a String into another String - this is the actual function which will handle requests. To start with I've used a method reference. Later on when you need something more complex this can be changed into a lambda expression.

Although this function is destined for the cloud, it's possible to run it locally by adding the spring-cloud-starter-function-web dependency under <dependencies> in pom.xml which is in the root of your project directory:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-function-web</artifactId>
</dependency>

In a terminal at the root of the project, run ./mvnw spring-boot:run, wait for the ascii-art Spring logo then test it with this command in another terminal window:

curl -H "Content-Type: text/plain" localhost:8080/respondToSms -d 'hello function'

 You'll see "HELLO FUNCTION" as the response, just like you'd expect from String::toUpperCase.

Screenshot of the curl command from the prose, with output in all-caps.

Once you're happy with this you need to remove the spring-cloud-starter-function-web dependency because it will interfere with deploying this code to a serverless platform.

A more useful function

You can add more classes, wire them together using Spring DI annotations like @Bean and @Autowired and you can add more dependencies into pom.xml to make your function more useful. Feel free to try it - the Maven configuration in this post will make sure that will all work when deploying to the serverless platform, but is out of scope for this blog post.

Deploying to Azure Functions

It's time to fulfill the promise I made in the introduction: deploying this function to Azure Functions to give the function code a public URL.

To deploy to Azure we need to add an adapter which wraps the Spring Cloud Function as an Azure Function, which needs another new dependency and a new class.

The dependency you need to add to pom.xml is:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-function-adapter-azure</artifactId>
</dependency>

Create a new package called com.example.webhooks.azure and in there a class called AzureFunctionWrapper. In fact the name doesn't matter, but that's what I called it. This class needs to extend AzureSpringBootRequestHandler<String, String> - the type parameters match those of the respondToSms function. It also needs a method annotated with @FunctionName whose value matches the name of the method in the Spring Cloud Function, like this:

public class AzureFunctionWrapper extends AzureSpringBootRequestHandler<String, String> {

        @FunctionName("respondToSms")
        public HttpResponseMessage execute(
            @HttpTrigger(
                name = "request",
                methods = {HttpMethod.GET, HttpMethod.POST},
                authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request, ExecutionContext context) {

            return request
                .createResponseBuilder(HttpStatus.OK)
                .body(handleRequest(request.getQueryParameters().get("Body"), context))
                .header("Content-Type", "text/plain")
                .build();
        }
}

[this code on GitHub, including import statements]

There are a few bits of config needed to tie this all together:

  • Create a directory called azure in src/main, and add host.json and local.settings.json which you can copy from here. You don't need to edit these files.
  • Add this enormous chunk of XML to the pom.xml, below </dependencies>,replacing the <dependencyManagement> and  <build> sections that are already there. This adds config for the Maven Plugin for Azure Functions which let you build and deploy your function from the command line. None of this needs editing.

Finally add some values to the <properties> section of pom.xml:

<spring.boot.wrapper.version>1.0.26.RELEASE</spring.boot.wrapper.version>

<azure.functions.java.library.version>1.4.0</azure.functions.java.library.version>
<azure.functions.maven.plugin.version>1.9.2</azure.functions.maven.plugin.version>
<stagingDirectory>${project.build.directory}/azure-functions/${functionAppName}</stagingDirectory>

<!-- customize these three properties depending to control the Azure deployment -->
<functionResourceGroup>twilio-spring-function-resource-group</functionResourceGroup>
<functionAppName>twilio-spring-function</functionAppName>
<functionAppRegion>westeurope</functionAppRegion>

<!-- this property has to match the name of the class from the Spring Initializr -->
<start-class>com.example.webhooks.HandlingTwilioWebhooksUsingSpringCloudFunctionApplication</start-class>

[see this code on GitHub]

Once you have installed the Azure Functions Core Tools you can run this function locally again, this time using the Azure tools instead of Spring's, with:

./mvnw clean package azure-functions:run

Note: this downloads a lot of dependencies and can take a while. You'll know it's done when you see respondToSms: [GET,POST] http://localhost:7071/api/respondToSms in the output. Maven caches downloaded dependencies, so after the first run it will be noticeably quicker.

Now, test with:

curl 'http://localhost:7071/api/respondToSms?Body=hello+azure+function'

Note that the request is different to before - this matches how Azure will make the function available once it's deployed. The response is still made by String::toUpperCase, of course:

Screenshot of the curl command from the prose, with output in all-caps.

Deploying to Azure

To upload and configure the function on Azure, all the config is in place so you just need to run:

./mvnw clean package azure-functions:deploy

This may ask you to log in to Azure using your browser. After the deployment is finished you will see the full public URL of the function:

Screenshot of the last part of the maven output showing "BUILD SUCCESS" in green bold.

You can curl a request to the function on Azure just the same as before - swap out the localhost URL for the public Azure URL.

Screenshot of the curl command against Azure&#x27;s public URL.

Looks like it's working - Perfect!

Serverless Cold Starts

Perhaps you noticed that it seemed a little slow to respond the first time? Serverless platforms can respond slowly if a function hasn't been called for a while, a phenomenon known as a "cold start". There are lots of factors which can influence the frequency and severity of cold starts. Twilio waits for 15 seconds for a response to a webhook HTTP request. Personally I have never had an Azure Function so slow to start that Twilio times out and this investigation shows that the vast majority of Java functions are serviced in under 15s. If you wish to avoid the problem entirely, the Azure Function Premium Plan offers guaranteed-warm function invocations, for a price. Everything else in this post fits comfortably into Azure's free tier.

Configuring Twilio to use your Function for SMS responses

If you don't have one already, create your free Twilio account and grab a phone number. On the phone number configuration page, set the behaviour for "a message comes in" to be a GET request to the Azure Functions URL:

The section of the Twilio console where you choose the webhook URL for incoming SMS.

You're all set 🎉 Test your function with an SMS:

Screenshot of the android messaging app showing an outgoing SMS saying "Hello Azure" and getting the same text as a response, but in all-caps.

Summing up

If you followed this post all the way then congrats! You've got a running Spring Cloud Function on Azure hooked up with Twilio to provide responses to an SMS. Now go make it do something useful and fun! As well as the Body, Twilio will pass the From number for the incoming message and other metadata so you can create personalised responses, look things up in your customer database or on other Web APIs - whatever you can imagine.

If you're building with serverless Java and Twilio I'd love to hear from you - get in touch and tell me about it. Happy Coding!

📧 mgilliard@twilio.com

🐦 @MaximumGilliard