Playing with Java in Azure Functions - New Release
October 19, 2019 Ā· 25 min
In one of the previous posts, I introduced the Azure Functions Java. I felt that I need to write a dedicated tutorial to this great Azure Serverless service š
In this post, I will be covering many concepts in deep:
[INFO] ...
[INFO] --- azure-functions-maven-plugin:1.3.4:package (package-functions) @ helloworld-functions ---
[INFO][INFO] Step 1 of 7: Searching for Azure Functions entry points
[INFO]1 Azure Functions entry point(s) found.
[INFO][INFO] Step 2 of 7: Generating Azure Functions configurations
[INFO] Generation done.
[INFO][INFO] Step 3 of 7: Validating generated configurations
[INFO] Validation done.
[INFO][INFO] Step 4 of 7: Saving empty host.json
[INFO] Successfully saved to /Users/nebrass/azure/helloworld-functions/target/azure-functions/hello-world-example/host.json
[INFO][INFO] Step 5 of 7: Saving configurations to function.json
[INFO] Starting processing function: HttpTrigger-Java
[INFO] Successfully saved to /Users/nebrass/azure/helloworld-functions/target/azure-functions/hello-world-example/HttpTrigger-Java/function.json
[INFO][INFO] Step 6 of 7: Copying JARs to staging directory/Users/nebrass/azure/helloworld-functions/target/azure-functions/hello-world-example
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 1 resource to /Users/nebrass/azure/helloworld-functions/target/azure-functions/hello-world-example
[INFO] Copied successfully.
[INFO] Step 7 of 7: Installing function extensions if needed
[INFO] Extension bundle specified, skip install extension
[INFO] Successfully built Azure Functions.
This command will create an azure-functions folder under the Maventarget folder:
In the Function.java class, shown in the sample code above, we have a function called HttpTrigger-Java, this is why we got a folder with the same name, with a function definition file called function.json file:
The scriptFile is pointing on the packaged application JAR file.
the entryPoint is pointing on the run method of the Function class; where the Azure Function is defined.
The first element in the bindings array is the HTTP Trigger. The type and direction properties identify the trigger. The name property identifies the function parameter that receives the request content. The methods array will list the HTTP verbs that are identifying the HTTP Request. The second element in the bindings array is the HTTP output binding. The type and direction properties identify the binding. The name property specifies how the function provides the response, in this case by using the function return value. This means, when we do a return "Hello World"; this String will be emitted as an HTTP Response.
Running & deploying the Azure Functions Application#
To run the project locally just do: mvn azure-functions:run
To deploy the project to your Azure Subscription: mvn azure-functions:deploy
The azure-functions:deploy command needs that your Azure CLI have to be authenticated to your subscription š¤
Triggers are what cause a function to run. A trigger defines how a function is invoked and a function must have exactly one trigger. Triggers have associated data, which is often provided as the payload of the function.
Binding to a function is a way of declaratively connecting another resource to the function; bindings may be connected as input bindings, output bindings, or both. Data from bindings is provided to the function as parameters.
We can mix and match different bindings to suit your needs. Bindings are optional and a function might have one or multiple input and/or output bindings.
With Triggers and bindings, we can avoid hardcoding access to other services. Our function receives data (for example, the content of a queue message) in function parameters. We send data (for example, to create a queue message) by using the return value of the function.
I found a great animated GIF in the Azure Functions documentation that describes how Triggers and Bindings work ?
In Azure Functions, bindings are available as separate packages from the functions runtime. Extension bundles allow other functions access to all bindings through a configuration setting. HTTP and timer triggers are supported by default and donāt require an extension.
Extension bundles is a local development technology that helps us to add a compatible set of Functions binding extensions to our Azure Functions project.
When used, these extension packages will be included in the deployment package when we deploy it to Azure. To avoid conflicts between packages, the Bundles guarantee that the packaged extension inside are compatible with each other.
To develop Azure Functions application locally, we need to have the latest version of Azure Functions Core Tools, which provide a local development experience for creating, developing, testing, running, and debugging Azure Functions locally.
The host.json file included in the generated project skull enables the Extension Bundle:
First of all, we need to go to the Keys menu, next choose the Read-write Keys section and copy the Primary Connection String, this property will be used in the Azure Function Application to access the Cosmos DB:
Next, we will use this connection key in the next steps..
1) Plug the connection Key to the Azure Function Application portal#
To plug the Connection Key to the Azure Function Application, we need to go to our Azure Functiondashboard, and click on the Configuration menu:
Next, we will access the Configuration listing:
Now, click on the New application setting and paste the Connection Key to the form :
After filling the form and validating the creation, you will be back to the Configuration listing, you need to click on Save to confirm the creation.
2) Plug the connection Key to the local development project#
We will go back to the source code project. We will add this Connection Key to the l_ocal.settings.json_ file. The file will look like:
Thatās all for the Cosmos DB configuration part ! š
As you see, we have already an AzureWebJobsStorage entry that did not have a value, but in the Configuration listing we have already this entry in the Application Settings; just click on the Show values button to show the values. Next, we need to copy this value and past it into the l_ocal.settings.json_ file:
We need to create a new Azure Service Bus namespace:
Click on Add or Create namespace to go to the creation form:
Click on Create and confirm the creation.
Next, we will create a Queue on which we will be sending some messages. To do that, we need to click on + Queue:
The Queue creation form:
Name: hello-world-app-queue
Max queue size: 1 GB
Time To Live: 14 Days
We need to enable:
Enable dead lettering on message expiration ā¹ļø Dead lettering messages involves holding messages that cannot be successfully delivered to any receiver to a separate queue after they have expired. Messages do not expire in the dead letter queue, and it supports peek-lock delivery and all transactional operations.
A dedicated tutorial about sessions will come soon š ā¹ļø Service bus sessions allow ordered handling of unbounded sequences of related messages. With sessions enabled a queue can guarantee first-in-first-out delivery of messages.
Now, we need to connect our Azure Functions with the Service Bus. To do that, we will do as we did with the Cosmos DB and the Storage Account: on the Service Bus Namespace dashboard, we need to go to the Shared access policies. Next, double click on the RootManageSharedAccessKey policy to show the keys window.
In this window, just click on the Copy button of the Primary Connection String.
Next, go back to the Configuration listing menu of our Azure Functions Application. We will add the Connection String as a setting called ServiceBusConnection:
Next, we will need to add this same setting to our local.settings.json:
This function will have as input a Name and Email address, requested using the HTTP Post verb. After parsing the request payload, this function will create a record of a Student in Cosmos DB.
The sample HTTP Hello World Function shown in the beginning of this tutorial, will be a good starting point:
/**
* Azure Functions with HTTP Trigger.
*/publicclassFunction {
@FunctionName("HttpTrigger-Java")
public HttpResponseMessage run(
@HttpTrigger(name ="req",
methods = {HttpMethod.GET, HttpMethod.POST},
authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context) {
context.getLogger().info("Java HTTP trigger processed a request.");
// Parse query parameter String query = request.getQueryParameters().get("name");
String name = request.getBody().orElse(query);
if (name ==null) {
return request
.createResponseBuilder(HttpStatus.BAD_REQUEST)
.body("Please pass a name on the query string or in the request body")
.build();
} else {
return request
.createResponseBuilder(HttpStatus.OK)
.body("Hello, "+ name)
.build();
}
}
}
In the run() method, we will add an other parameter: the Cosmos DB Binding š the Azure Functions connector to Cosmos DB š¤
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@FunctionName("CosmosDBStoreBinding")
public HttpResponseMessage run(
@HttpTrigger(
name ="req",
methods = {HttpMethod.POST}, 1ļøā£ authLevel = AuthorizationLevel.FUNCTION)
HttpRequestMessage<Optional<String>> request,
@CosmosDBOutput(
name ="database",
databaseName ="university",
collectionName ="students",
connectionStringSetting ="CosmosDbConnection") 2ļøā£ OutputBinding<String> outputItem,
final ExecutionContext context) {
...
}
1ļøā£ I only kept the HTTP Post verb - I donāt want that the function get called using the HTTP Get verb.
2ļøā£ Here we are defining an OutputBinding parameter, that will hold the computing result to Cosmos DB. Thatās why this parameter is annotated using the @CosmosDBOutput.
The @CosmosDBOutput annotation has these main attributes:
name: The variable name used in function.json š Here we will use ādatabaseā
databaseName: Defines the database name of the CosmosDB to which to write š Here we will use āuniversityā
collectionName: Defines the collection name of the CosmosDB to which to write š Here we will use āstudentsā
connectionStringSetting: Defines the app setting name that contains the CosmosDB connection string- This is the same attribute that we already defined in the local.settings.json and also in the Configuration section of the Azure Function in the Azure Subscription š Here we will use āCosmosDbConnectionā
@FunctionName("CosmosDBStoreBinding")
public HttpResponseMessage run(
@HttpTrigger(
name ="req",
methods = {HttpMethod.POST},
authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Optional<String>> request,
@CosmosDBOutput(
name ="database",
databaseName ="university",
collectionName ="students",
connectionStringSetting ="CosmosDbConnection")
OutputBinding<String> outputItem,
final ExecutionContext context) {
context.getLogger().info("Java HTTP trigger processed a request.");
String name ="empty";
String email ="empty";
// Parse query parameterif (request.getBody().isPresent()) {
JSONObject jsonObject =new JSONObject(request.getBody().get());
name = jsonObject.getString("name");
email = jsonObject.getString("email");
}
// Generate random IDfinal String id = String.valueOf(Math.abs(new Random().nextInt()));
// Generate document Student student =new Student(id, name, email);
final String database = JSONWriter.valueToString(student);
context.getLogger().info(String.format("Document to be saved in DB: %s", database));
outputItem.setValue(database);
// return the document to calling client.return request.createResponseBuilder(HttpStatus.OK)
.body(database)
.build();
}
Here, we used the Bindings explicitly, we can do the same thing, just by annotating the Function itself using the @OutputBinding - as we will not have an OutputBinding object to assign the value to it; we will have to return the value that we want to insert into Cosmos DB as a return value of run() method. The function can be written like this:
@FunctionName("CosmosDBStoreAnnotation")
@CosmosDBOutput(
name ="database",
databaseName ="university",
collectionName ="students",
connectionStringSetting ="CosmosDbConnection")
public String run(
@HttpTrigger(
name ="req",
methods = {HttpMethod.POST},
authLevel = AuthorizationLevel.FUNCTION) HttpRequestMessage<Optional<String>> request,
final ExecutionContext context) {
context.getLogger().info("Java HTTP trigger processed a request.");
String name ="empty";
String email ="empty";
// Parse query parameterif (request.getBody().isPresent()) {
JSONObject jsonObject =new JSONObject(request.getBody().get());
name = jsonObject.getString("name");
email = jsonObject.getString("email");
}
// Generate random IDfinal String id = String.valueOf(Math.abs(new Random().nextInt()));
// Generate document Student student =new Student(id, name, email);
final String database =new JSONObject(student).toString();
context.getLogger().info(String.format("Document to be saved in DB: %s", database));
return database;
}
These two possible formats of the function will give the same result - just in the second one, we will not be able to define the HTTP Response to the caller client.
ā ļøā ļø At this level, the insertion in the Cosmos DB is not effective yet; at this stage, we donāt know if the creation will be done or may fail whatever the reason. This is why the Azure Functions have a verbose logging that will help us understand if things are going in the correct way or not.
When you run the functions locally you can have these logs on your console:
[19/10/2019..] Executing HTTP request: {[19/10/2019..]"requestId": "28cb88b7-3a7a-4c96-bacc-f70349d30cd9",
[19/10/2019..]"method": "POST",
[19/10/2019..]"uri": "/api/CosmosDBStoreAnnotation"[19/10/2019..]}[19/10/2019..] Executing 'Functions.CosmosDBStoreAnnotation'(Reason='This function was programmatically called via the host APIs.', Id=627d4d5d-6064-4322-aa2d-5ee1050f9a89)[19/10/2019..] Java HTTP trigger processed a request.
[19/10/2019..] Document to be saved in DB: {"id":"1234769986", "name":"nebrass", "email":"lnibrass@gmail.com"}[19/10/2019..] Function "CosmosDBStoreAnnotation"(Id: 627d4d5d-6064-4322-aa2d-5ee1050f9a89) invoked by Java Worker
[19/10/2019..] Executed 'Functions.CosmosDBStoreAnnotation'(Failed, Id=627d4d5d-6064-4322-aa2d-5ee1050f9a89)[19/10/2019..] System.Private.CoreLib: Exception while executing function: Functions.CosmosDBStoreAnnotation. System.Private.CoreLib: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.
[19/10/2019..] Executed HTTP request: {[19/10/2019..]"requestId": "28cb88b7-3a7a-4c96-bacc-f70349d30cd9",
[19/10/2019..]"method": "POST",
[19/10/2019..]"uri": "/api/CosmosDBStoreAnnotation",
[19/10/2019..]"identities": [[19/10/2019..]{[19/10/2019..]"type": "WebJobsAuthLevel",
[19/10/2019..]"level": "Admin"[19/10/2019..]}[19/10/2019..]],
[19/10/2019..]"status": 500,
[19/10/2019..]"duration": 14[19/10/2019..]}
We can easily notice that there is a problem in the execution, and even there is a {status: 500} for the HTTP Request, which is the same headers sent back to the curl command.
Now, we will create the function that will be triggered by a message in the Service Bus Queue and will insert data to Cosmos DB. The function will look like:
@FunctionName("ServiceBusMessageToCosmosDb")
publicvoidrun(
@ServiceBusQueueTrigger(
name ="messageTrigger",
queueName ="hello-world-app-queue",
connection ="ServiceBusConnection" ) String student,
@CosmosDBOutput(
name ="database",
databaseName ="university",
collectionName ="students",
connectionStringSetting ="CosmosDbConnection")
OutputBinding<String> outputItem,
final ExecutionContext context) {
context.getLogger().info(String.format("Service Bus message trigger processed a request: %s", student));
// This line will be used to validate the received JSON String jsonValue =new JSONObject(student).toString();
context.getLogger().info(String.format("Document to be saved in DB: %s", jsonValue));
outputItem.setValue(jsonValue);
}
When we run our Functions Application, the Message that is already in the Service Bus Queue will be consumed - the execution log:
1
2
3
4
5
6
7
8
9
10
[19/10/2019..] Executing 'Functions.ServiceBusMessageToCosmosDb'(Reason='New ServiceBus message detected on 'hello-world-app-queue'.', Id=dcb8e81d-f09e-48ee-a236-49542ae2e34d)[19/10/2019..] Trigger Details: MessageId: f5a0a1d6e5e94fc790aed052217dda5a, DeliveryCount: 1, EnqueuedTime: 19/10/2019 17:47:39, LockedUntil: 19/10/2019 18:00:44
[19/10/2019..] Service Bus message trigger processed a request: {[19/10/2019..]"id": "954440360",
[19/10/2019..]"name": "nebrass",
[19/10/2019..]"email": "lnibrass@gmail.com"[19/10/2019..]}[19/10/2019..] Document to be saved in DB: {"name":"nebrass","id":"954440360","email":"lnibrass@gmail.com"}[19/10/2019..] Function "ServiceBusMessageToCosmosDb"(Id: dcb8e81d-f09e-48ee-a236-49542ae2e34d) invoked by Java Worker
[19/10/2019..] Executed 'Functions.ServiceBusMessageToCosmosDb'(Succeeded, Id=dcb8e81d-f09e-48ee-a236-49542ae2e34d)
Yooppi š„³ every thing is working like a charm ! š„°
Now, we will see how to debug our Azure Functions Java Application š
DEBUGGING the Azure Functions Application Locally#
To debug the application locally just do: mvn clean package azure-functions:run -DenableDebug to run the Azure Functions application in the Debugging Mode.
In you IDE, you need to attach a debugger to the Host: localhost and the Port: 5005.
For NetBeans In the Debug menu, click on Attach Debugger:
Next, in the Attach Debugger window, we need to make localhost as Host and 5005 as Port:
Next, try to put a breakpoint and call your application, you will see your application pause on your breakpoint:
For IntelliJ In the Run menu, select Edit Configurations, next click on the (+) button..
On the Configuration section:
Host: localhost
Port: 5005
Cmd line arguments:-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005
Now, you can put a breakpoint in your code, start the application in Debug Mode, run the Remote Debug configuration that you just created in IntelliJ. When you invoke one of the functions, you will see the application pause on the breakpoint.
$ mvn package azure-functions:deploy
...
[INFO] --- azure-functions-maven-plugin:1.3.4:deploy (default-cli) @ helloworld-functions ---
[INFO] Authenticate with Azure CLI 2.0
[INFO] Updating the specified function app...
[INFO] Java version of function host : 1.8
[INFO] Set function worker runtime to java
[INFO] Successfully updated the function app.hello-world-app-example
[INFO] Trying to deploy the function app...
[INFO] Trying to deploy artifact to hello-world-app-example...
[INFO] Successfully deployed the artifact to https://hello-world-app-example.azurewebsites.net
[INFO] Successfully deployed the function app at https://hello-world-app-example.azurewebsites.net
...
But from where comes the Authorization ? Did we secured our Functions ??
Actually, yes and this comes in the generated function sample. In the method signature, there is a @HttpTrigger annotation with an attribute authLevel=AuthorizationLevel.FUNCTION . In the Javadoc of this annotation, we see that authLevel determines what keys, if any, need to be present on the request in order to invoke the function. The authorization level can be one of the following values:
anonymous: No API key is required.
function: A function-specific API key is required. This is the default value if none is provided.
admin: The master key is required.
Good ! So we need to have an API Key to be able to consume our Functions. Great, but from where we can get that? š¤
KEEP CALM ! š¤ Everything is easy to find in the Azure Portal š
Go to the Azure Functions Application, then click on Function App Settings:
Next, in this window, you will find the default Host Keys:
The Azure Functions API keys can be either:
a Host Key : an API Key that can be used with all the functions in the same application
a Function Key: an API Key that can be used for only an assigned the function
Based on this definition, if we want to generate a Function Key, we need to go to the Function, next, click on Manage:
Next, after you copy the Key, you need to include it to the request. We can do that in several ways:
in the URL: https://APP_URL/api/CosmosDBStoreAnnotation?code=XXXXXXXXXXXXXXXXXXXXXXX
in the headers, in a format: Key=x-functions-key and Value=XXXXXXXXXXXXXXXXXXXXXXX
Great ! Letās test the curl again:
1
2
$ curl -i -X POST https://hello-world-app-example.azurewebsites.net/api/ServiceBusStoreAnnotation?code=XXXXXXXXXXXXXXXXXXX== -d '{"name":"nebrass", "email":"lnibrass@gmail.com"}'
1
2
3
4
5
6
7
8
9
HTTP/1.1200OKContent-Length:68Content-Type:text/plain; charset=utf-8Set-Cookie:ARRAffinity=aaaaaabbbbbbbbcccccccdddddeeeeeefffffff;Path=/;HttpOnly;Domain=hello-world-app-example.azurewebsites.netRequest-Context:appId=cid-v1:12345678-abcd-efgh-xywz-537739c295b6Set-Cookie:ARRAffinity=wwwwwwxxxxxxxxvvvvvvvuuuuuiiiiiihhhhhhh;Path=/;HttpOnly;Domain=hello-world-app-example.azurewebsites.netDate:Sat, 19 Oct 2019 20:03:48 GMT{"id":"1251989744","name":"nebrass","email":"lnibrass@gmail.com"}%
When we created our application, we created an Application Insights account. To access the Azure Functions Monitoring, you need to go to the Function, next you click on the Monitor menu. Here you will find the latest requests with a small stats about the successful and failing ones:
To check more, you can click on a request to see more details. Or even you can click on:
Live app metrics to access the Metrics dashboard for our Functions application.
Run in Application Insights to access the detailed Logs and Log Analytics platform for our Functions application.
Cool ! As you see, we can do many crazy things easily with the Azure Functions, in a very efficient way. No hard configuration, no crazy yaml configuration files..
Feel free to try in deep this tutorial and I will be interested to get your feedback. If you have a request or any inquiry, feel free to get in touch with me š