Handling Webhooks with Java, Spring Cloud Function and Azure Functions
Time to read: 5 minutes
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:
- Java 8 or newer - I manage Java installations with SDKMAN! and I recommend you do too!
- A Java IDE - I'm a fan of IntelliJ IDEA but others will work just as well.
- An Azure account & the Azure Functions Core Tools installed.
- A Twilio account & a phone number.
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
.
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:
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:
You'll see "HELLO FUNCTION" as the response, just like you'd expect from String::toUpperCase
.
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:
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:
[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
insrc/main
, and addhost.json
andlocal.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
:
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:
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:
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:
Deploying to Azure
To upload and configure the function on Azure, all the config is in place so you just need to run:
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:
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.
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:
You're all set 🎉 Test your function with an SMS:
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!
Related Posts
Related Resources
Twilio Docs
From APIs to SDKs to sample apps
API reference documentation, SDKs, helper libraries, quickstarts, and tutorials for your language and platform.
Resource Center
The latest ebooks, industry reports, and webinars
Learn from customer engagement experts to improve your own communication.
Ahoy
Twilio's developer community hub
Best practices, code samples, and inspiration to build communications and digital engagement experiences.