Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Java on Azure: A Complete Serverless Cloud Native Application

0.00/5 (No votes)
22 Apr 2021 2  
In this article, we’ll show you how to use Azure Functions to implement a hypothetical temperature monitoring application.

Azure Functions is a serverless compute service for event-driven architectures. With Azure Functions, you don’t need to provision low-level cloud resources like virtual machines, load balancers, and auto-scalers. The service provisions all this for you­ automatically, so you can focus on your code.

Microsoft has always had excellent tool support for .NET and .NET languages, including C#. As the company’s vision is now "Any Developer, Any App, Any Platform," Microsoft now provides powerful tools to support Java developers as well. Since the cloud native and microservices approach promotes using any programming language, as determined by the problem to solve, Azure is a great place to run your cloud native, primarily containerized applications no matter what languages are used.

In our previous Java on Azure series, we introduced cloud native concepts and used them to create our own poll application, using multiple functions. In this series, we’ll build on our Azure Functions skills, creating a complete serverless application, building a Kubernetes cluster, and adding containers.

In this article, we’ll show you how to use Azure Functions to implement a hypothetical temperature monitoring application. This application will work with other Azure services, including Table Storage and SendGrid. More specifically, Table Storage will contain sensor data from an Internet of things (IoT) device. The first Azure Function will be triggered by the HTTP request and return the average temperature stored in the Azure Table. The second Azure Function will be invoked periodically to check whether the temperature exceeds a predefined threshold. If it does, an e-mail notification will be sent through SendGrid.

Setting Up the Project

All code examples included in this project are written in Visual Studio Code on macOS. Install the Azure Functions extension for Visual Studio Code and Azure Functions Core Tools, as outlined in Core Tools documentation. We’ll use Maven build tools throughout the article. You are welcome to access and download the full project source code.

Table Storage

Before implementing Azure Functions, use the Azure portal to set up the Storage account (StorageV2). Then, create the SensorReadings table under the Table service. Using the Storage Explorer, add several entries to the table, as shown in the figure below:

Finally, go to Access keys to obtain the Connection string under key1. You’ll need this later when connecting a function with Azure Table.

SendGrid

To send email notifications from your function, use the SendGrid service. First, set up the SendGrid account. Then, configure the default Sender Identity and obtain the SendGrid API key. You will need this later to configure your function for email sending.

Implementing Temperature Monitoring

After setting up the dependent services, go back to Visual Studio Code with the Azure Functions extension and create a new function app. Set the app name to "TemperatureMonitoring," select Java 8, and set the package name to "com.temperaturemonitoring." Leave all other parameters at their default values.

Add to the project the SensorReading class (main/java/temperaturemonitoring/SensorReading.java):

java
public class SensorReading {
    private String PartitionKey;
    private String RowKey;
    private double Temperature;    
 
    // PartitionKey
    public String getPartitionKey() { return this.PartitionKey; }
    public void setPartitionKey(String key) { this.PartitionKey = key; }
 
    // RowKey
    public String getRowKey() { return this.RowKey; }
    public void setRowKey(String key) { this.RowKey = key; }
 
    // Temperature
    public double getTemperature() { return this.Temperature; }
    public void setTemperature(double temperature) { this.Temperature = temperature; }
}

The above class represents the items in the SensorReadings table from Azure Storage. Therefore, it has only three properties: PartitionKey, RowKey, and Temperature.

We are now ready to implement the first function, which will fetch items from the table, calculate their average, and return it through the HTTP response message. The function will be triggered by the anonymous HTTP GET request. Keep the default HttpTrigger annotation (see main/java/temperaturemonitoring/Function.java in the source code):

java
@FunctionName("GetMeanTemperature")
public HttpResponseMessage get(
    @HttpTrigger(
        name = "get",
        methods = {HttpMethod.GET},
        authLevel = AuthorizationLevel.ANONYMOUS)
        HttpRequestMessage<Optional<String>> request, 
 
    @TableInput(name="inputTable",
        tableName="SensorReadings",
        connection="AzureWebJobsStorage",
        take="20") SensorReading[] sensorReadings,
 
    final ExecutionContext context) {
 
    // Function code
 
}

Use the TableInput interface to access the Storage Table without the need to write integration code. Let’s configure TableInput to use a connection string defined by AzureWebJobsStorage in the local.settings.json file. This is where you need to paste your connection string and SendGrid API key:

JavaScript
{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "<YOUR_CONNECTION_STRING>",
    "AzureWebJobsSendGridApiKey": "<YOUR_SENDGRID_API_KEY>",
    "FUNCTIONS_WORKER_RUNTIME": "java"
  }
}

Now limit the number of items to 20 using the take method. The items will be available via the sensorReadings collection.

To proceed further, check whether this collection contains any items. If so, calculate the average and return it to the caller:

java
if(sensorReadings.length == 0){
    return request.createResponseBuilder(HttpStatus.OK).
        body("Not enough sensor data").build();
}       

double meanTemperature = Arrays.stream(sensorReadings)
    .mapToDouble((s) -> s.getTemperature())
    .average()
    .orElse(-1);
 
String responseString = String.format("Mean temperature = %.2f", meanTemperature);

return request.createResponseBuilder(HttpStatus.OK).body(responseString).build();

Now, we can build and test the function locally:

mvn clean install
mvn azure-functions:run

The build should output the function endpoint, similar to this: http://localhost:7071/api/GetMeanTemperature

After sending a GET request to the above URL, you get the average temperature. See the Terminal tab in the screenshot below.

Configuring an Email Notification

Now we proceed to implementing the second function, which will send an email whenever the temperature exceeds a predefined threshold. This function will be invoked periodically — once a minute, with the use of TimerTrigger.

Next, configure the TableInput interface to take one item from the SensorReadings table. Finally, configure the SendGridOutput interface to integrate the function with our SendGrid account:

java
@FunctionName("CheckTemperature")
public void checkTemperature(
    @TableInput(name="inputTable",
        tableName="SensorReadings", 
        connection="AzureWebJobsStorage", 
        take="1") SensorReading[] sensorReadings,

    @TimerTrigger(name="keepAliveTrigger", schedule = "0 */1 * * * *") String timerInfo,

    @SendGridOutput(
        name = "message",
        dataType = "String", 
        apiKey = "AzureWebJobsSendGridApiKey",
        from = "dawid@borycki.com.pl",
        to = "dawid@borycki.com.pl", 
        subject = "Temperature alert",
        text = "")
        OutputBinding<String> message,
    final ExecutionContext context) {
    
    // Function code 
}

Check whether the sensorReadings collection contains any items. If so, compare the first item against the threshold value. If the temperature is greater than the threshold, we need to send an email. Configure the email message body using the PrepareMessageBody helper method:

java
private static String PrepareMessageBody(double temperature, double temperatureThreshold) {
    final String value = String.format(
        "The temperature of %.1f is above the threshold of %.1f", 
            temperature, temperatureThreshold);
 
    StringBuilder builder = new StringBuilder()
        .append("{")
        .append("\"content\": [{\"type\": \"text/plain\", \"value\": \"%s\"}]") 
        .append("}");

    return String.format(builder.toString(), value);
}

Note that we access the outgoing message through OutputBinding, which is configured in the function declaration.

To test the solution we need to rebuild the code and run the function app:

mvn install
mvn azure-functions:run

The new build output will show two functions: GetMeanTemperature and CheckTemperature.

Note that the CheckTemperature function is invoked automatically every minute. It will send an email notification if the Temperature property of the first item in SensorReadings is above 35. You can edit this item using Storage Explorer to simulate a high temperature reading and trigger the alarm. Then, you should receive the email notification that looks as follows:

Next Steps

In this article, we created a serverless solution, composed of two Azure functions, which orchestrated Azure Storage for storing hypothetical IoT sensor data and SendGrid for sending emails. We did not have to set up any underlying infrastructure, which significantly reduced the implementation time.

Note that our solution has several limitations. First, when the temperature is above a threshold, TableInput will always take the first item from the Azure table. In practice, we’d want to send an email alert only when the average temperature (calculated from the most recent items) exceeds the threshold. We could achieve that by filtering the table. We can also use the filter method on TableInput. The filter has to be a compiled constant, such as (where ge stands for "greater than or equal to"):

filter="Timestamp ge datetime'2021-03-08T09:47:18.356Z'"

This means that we cannot change the filter dynamically to adjust it to the current date and time. We could overcome this limitation in two ways. One straightforward solution would be to use a proxy function. This function would calculate the filter and then invoke the next function through HTTP. The other option would be to use the Azure Table client and perform queries in code. However, both the above solutions will add extra complexity to the code. More than that, the custom query will delay function execution, thus increasing the solution cost.

A better approach would be to use a custom API running in a Kubernetes cluster. Stay tuned: we’ll implement this in the last article of this series. But first, we need a Kubernetes cluster we can deploy to. In the next article, we'll learn how to set up Azure Kubernetes Service the cloud native way and create our Kubernetes cluster.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here