Spring Cloud Function has had support for Microsoft Azure Functions since version 1.0, but in the latest 2.0 releases (still in milestone phase) we decided to change the programming model a bit. This article describes what the changes mean for users, and provides a bit of background behind the shift. We in the Spring team had a lot of fun working on this and collaborating with the folks at Microsoft to get the best blend of the two technologies for our users.
Microsoft has had Java support in Azure Functions for a while, and it
enables developers to easily write and deploy Java code that connects
in a serverless way to a wide range of platform services (events,
databases, storage, HTTP gateways, etc.) in Azure. It comes with an
annotation-based programming model that puts the function
implementations in Java methods. So you write a method and annotation
it with @FunctionName
, and it becomes an Azure Function. There is a
rich set of tools based on a Maven plugin (currently) that drives the
Azure command line and can be used to build a function, run and debug
it locally and deploy it to the cloud. There is a
Quickstart
Guide on the Azure website which will help you get all the
pre-requisites installed and working, and there is more detailed
documentation about how Azure Functions works in the
Developer’s
Guide.
The annotations also tie the function method parameters and return
types to the services used at deployment time. For example, if you
want to bind to an HTTP gateway at deployment time you use
@HttpTrigger
:
@FunctionName("uppercase")
public Bar execute(
@HttpTrigger(name = "req", methods = { HttpMethod.GET,
HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) Foo foo,
ExecutionContext context) {
return new Bar(foo.getValue());
}
In this example we accept an incoming HTTP POST request and Azure
binds its body to a POJO of type Foo
. We transform the Foo
into a
Bar
and it comes back to the caller in the HTTP response.
HTTP triggers are in the top 5 most popular integrations in Azure Functions, but even more popular are the event-based and storage or database-based triggers. The complete list can be found in the Triggers and Bindings documentation - there is a table where you can click on a specific binding or trigger and it will take you to reference page where there are code samples in all languages, including Java.
Here’s another example using the Azure Event Hub as an input and Cosmos DB as an output. This example is in github:
@FunctionName("uppercase")
public Bar execute(
@EventHubTrigger(name = "data", eventHubName = "events",
connection = "TRANSACTIONS_EVENT_HUB_CONNECTION_STRING")
Foo data,
@CosmosDBOutput(name = "document", databaseName = "inventory", collectionName = "messages",
connectionStringSetting = "PRODUCT_ITEMS_DOCUMENTDB_CONNECTION_STRING", createIfNotExists = true)
OutputBinding<Bar> document,
final ExecutionContext context) {
return document.setValue(new Bar(foo.getValue()));
}
The annotations carry connection credential information through an
indirection to environment variables that are configured in the
function deployment. The configuration for all that happens in the
build pom.xml
through the Azure Functions Maven plugin. For example:
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-functions-maven-plugin</artifactId>
<configuration>
<resourceGroup>${functionResourceGroup}</resourceGroup>
<appName>${functionAppName}</appName>
<region>${functionAppRegion}</region>
<appSettings>
<property>
<name>FUNCTIONS_EXTENSION_VERSION</name>
<value>beta</value>
</property>
<property>
<name>TRANSACTIONS_EVENT_HUB_CONNECTION_STRING</name>
<value>${TRANSACTIONS_EVENT_HUB_CONNECTION_STRING}</value>
</property>
<property>
<name>PRODUCT_ITEMS_DOCUMENTDB_CONNECTION_STRING</name>
<value>${PRODUCT_ITEMS_DOCUMENTDB_CONNECTION_STRING}</value>
</property>
<property>
<name>MSDEPLOY_RENAME_LOCKED_FILES</name>
<value>1</value>
</property>
</appSettings>
</configuration>
<executions>
<execution>
<id>package-functions</id>
<goals>
<goal>package</goal>
</goals>
</execution>
</executions>
</plugin>
In this case the environment variable names link the plugin
configuration to the function binding declaration. For instance the
@EventHubTrigger
has a connection
attribute that will be
popeulated at runtime from the
TRANSACTIONS_EVENT_HUB_CONNECTION_STRING
environment variable. The
plugin configures it remotely using a local environment variable with
the same name (notice the ${}
placeholders), which the developer or
CI process is responsible for setting up at runtime.
Your own personal connection strings are secrets and can be found in
the Azure Dashboard - when you click on the
relevant resource there is usually a Connection Strings
link (or
similar) that you can copy and paste to your local process (e.g. in a
script that you run locally but do not check into source
control). E.g. you might use a setup-env.sh
script like this:
export PRODUCT_ITEMS_DOCUMENTDB_CONNECTION_STRING="AccountEndpoint=https://..."
export TRANSACTIONS_EVENT_HUB_CONNECTION_STRING="Endpoint=sb://..."
and source it once at the beginning of a terminal session.
There are some other plugin declarations in the pom.xml
of the
sample. They are all important but basically boilerplate - you should
be able to copy them and re-use the same configuration in all Azure
Function Applications.
Spring Cloud Function aims to support similar serverless use cases
when the application developer declares Spring beans of type
java.util.Function
. The advantages of using Spring Cloud Function on
Azure, as opposed to vanilla Java functions, are that the actual
business logic code is (in principle) portable to other platforms, and
it is a familiar programming model for existing Spring users. Also,
all the usual benefits of Spring apply: dependency injection and
comprehensive integration with many other Java libraries.
The equivalent of both the examples above would be a single @Bean
:
@Bean
public Function<Foo, Bar> uppercase() {
return foo -> new Bar(foo.getValue().toUpperCase());
}
In version 1.0 of Spring Cloud Function, the user had to map the Microsoft annotations manually to a JSON deployment descriptor, and wrap it up manually into an archive with the right layout for the platform. The process was brittle (but independent of the Azure Java programming model).
In version 2.0 this would still work, but we have chosen to support
the use of the Azure annotations a bit more explicitly. So now we have
a base class that application developers can extend and decorate with
the Azure annotations. The example above would be exactly the same
@Bean
and one of the execute
methods above would be inserted into
the subclass of the Spring Cloud handler. Example:
public class UppercaseHandler extends AzureSpringBootRequestHandler<Foo, Bar> {
@FunctionName("uppercase")
@HttpTrigger(name = "req", methods = { HttpMethod.GET,
HttpMethod.POST }, authLevel = AuthorizationLevel.ANONYMOUS) Foo foo,
ExecutionContext context) {
return super.handle(foo, context);
}
Notice that the base class AzureSpringBootRequestHandler
is generic
with type parameters for input and output. There are 2 handle*
methods in the base class, one (handle
) which returns the response
object, and one (handleOutput
) which accepts an OutputBinding
and
binds it to the output from the Function
.
There are various configuration options that drive the runtime
behaviour of the Azure Function. The most important (and only
mandatory) one is the MAIN_CLASS
, which is the main
@SpringBootApplication
class that carries the declaration of the
Function
(or Functions
). You can specify this as an environment
variable, or as the Main-Class
entry in the application jar manifest
(this is the way it is set up in the sample below). As long as your
app has a main class with precisely one function, there is no need to
do anything else. In the sample app linked above we use the manifest
to define the main class:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>example.FunctionApplication</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
If your app has multiple Function
beans, they can be mapped to the
Azure function name though the @FunctionName
annotation - the bean
name matches the function name. In this way you can create an Azure
Function Application, which is a single deployment artifact for a
group of functions. If you prefer, you can also use an arbitrary
@FunctionName
and configure the Spring Cloud Function name through
an environment variable FUNCTION_NAME
.
There is another simple sample of how to set up a Spring Cloud Function as an Azure Function in the project repo - this one is an HTTP trigger from an Azure perspective, but the Spring Cloud Function parts are very similar.