From 9e5ebcc28dd46e501f3930ec1f2849f42627f483 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Thu, 18 Nov 2021 16:17:15 +0100 Subject: [PATCH] Fix documentation around ExecutionContext for Azure More cleanup in Azure samples Resolves #759 --- .../main/asciidoc/adapters/azure-intro.adoc | 84 ++++++++++--------- .../function-sample-azure/README.adoc | 11 +-- .../main/java/example/UppercaseHandler.java | 1 - 3 files changed, 51 insertions(+), 45 deletions(-) diff --git a/docs/src/main/asciidoc/adapters/azure-intro.adoc b/docs/src/main/asciidoc/adapters/azure-intro.adoc index 4d56cfa82..4a94264d8 100644 --- a/docs/src/main/asciidoc/adapters/azure-intro.adoc +++ b/docs/src/main/asciidoc/adapters/azure-intro.adoc @@ -3,65 +3,71 @@ === Microsoft Azure The https://azure.microsoft.com[Azure] adapter bootstraps a Spring Cloud Function context and channels function calls from the Azure -framework into the user functions, using Spring Boot configuration where necessary. Azure Functions has quite a unique, but -invasive programming model, involving annotations in user code that are specific to the platform. The easiest way to use it with -Spring Cloud is to extend a base class and write a method in it with the `@FunctionName` annotation which delegates to a base class method. +framework into the user functions, using Spring Boot configuration where necessary. Azure Functions has quite a unique and +invasive programming model, involving annotations in user code that are specific to the Azure platform. +However, it is important to understand that because of the style of integration provided by Spring Cloud Function, specifically `org.springframework.cloud.function.adapter.azure.FunctionInvoker`, this annotation-based programming model is simply a type-safe way to configure +your simple java function (function that has no awareness of Azure) to be recognized as Azure function. +All you need to do is create a handler that extends `FunctionInvoker`, define and configure your function handler method and +make a callback to `handleRequest(..)` method. This handler method provides input and output types as annotated method parameters +(enabling Azure to inspect the class and create JSON bindings). -This project provides an adapter layer for a Spring Cloud Function application onto Azure. -You can write an app with a single `@Bean` of type `Function` and it will be deployable in Azure if you get the JAR file laid out right. - -There is an `org.springframework.cloud.function.adapter.azure.FunctionInvoker` which you must extend, and provide the -input and output types as annotated -method parameters (enabling Azure to inspect the class and create JSON bindings). The base class has two useful -methods (`handleRequest` and `handleOutput`) to which you can delegate the actual function call, so mostly the function will only ever have one line. - -Example: - ```java -public class FooHandler extends FunctionInvoker { +public class UppercaseHandler extends FunctionInvoker, String> { + @FunctionName("uppercase") - public Bar execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, - HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, + HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, ExecutionContext context) { - return handleRequest(request.getBody().get(), context); + Message message = MessageBuilder.withPayload(request.getBody().get()).copyHeaders(request.getHeaders()).build(); + return handleRequest(message, context); } } ``` -This Azure handler will delegate to a `Function` bean (or a `Function,Publisher>`). Some Azure -triggers (e.g. `@CosmosDBTrigger`) result in a input type of `List` and in that case you can bind to `List` in the Azure handler, -or `String` (the raw JSON). The `List` input delegates to a `Function` with input type `Map`, or `Publisher` or `List` of -the same type. The output of the `Function` can be a `List` (one-for-one) or a single value (aggregation), and the output binding in the -Azure declaration should match. +Note that aside form providing configuration via Azure annotation we create an instance of `Message` inside the body of this handler method and make a callback to `handleRequest(..)` method returning its result. -If your app has more than one `@Bean` of type `Function` etc. then you can choose the one to use by configuring -`function.name`. Or if you make the `@FunctionName` in the Azure handler method match the function name it should work that -way (also for function apps with multiple functions). The functions are extracted from the Spring Cloud `FunctionCatalog` so the default -function names are the same as the bean names. +The actual user function you're delagating to looks like this + +```java +@Bean +public Function uppercase() { + return payload -> payload.toUpperCase(); +} + +OR + +@Bean +public Function, String> uppercase() { + return message -> message.getPayload().toUpperCase(); +} + +``` + +Note that when creating a Message you can copy HTTP headers effectively making them available to you if necessary. + +The `org.springframework.cloud.function.adapter.azure.FunctionInvoker` class has two useful +methods (`handleRequest` and `handleOutput`) to which you can delegate the actual function call, so mostly the function will only ever have one line. + +The function name (definition) will be retrieved from Azure's `ExecutionContext.getFunctionName()` method, effectively supporting multiple function in the application context. ==== Accessing Azure ExecutionContext -Some time there is a need to access the target execution context provided by Azure runtime in the form of `com.microsoft.azure.functions.ExecutionContext`. +Some time there is a need to access the target execution context provided by the Azure runtime in the form of `com.microsoft.azure.functions.ExecutionContext`. For example one of such needs is logging, so it can appear in the Azure console. -For that purpose we propagate `ExecutionContext` as Message header under `executionContext` name, so all you need is access it -is have your function accept a Message and access this header. +For that purpose the FunctionInvoker will add an instance of the `ExecutionContext` as a Message header so you can retrieve it via `executionContext` key. -Spring Cloud Function will register `ExecutionContext` as bean in the Application context, so it could be injected into your function. -For example -```java +``` @Bean -public Function, Bar> uppercase() { +public Function, String> uppercase(JsonMapper mapper) { return message -> { - ExecutionContext targetContext = message.getHeaders().get("executionContext"); - targetContext.getLogger().info("Invoking 'uppercase' on " + foo.getValue()); - return new Bar(message.getPayload().getValue().toUpperCase()); - }; + String value = message.getPayload(); + ExecutionContext context = (ExecutionContext) message.getHeaders().get("executionContext"); + . . . + } } ``` -With Message you will also have access to additional Azure meta information as Message headers that come as part of your request. - ==== Notes on JAR Layout diff --git a/spring-cloud-function-samples/function-sample-azure/README.adoc b/spring-cloud-function-samples/function-sample-azure/README.adoc index 024fbd244..f20f7d7e3 100644 --- a/spring-cloud-function-samples/function-sample-azure/README.adoc +++ b/spring-cloud-function-samples/function-sample-azure/README.adoc @@ -25,9 +25,11 @@ $ curl -H "Content-Type: application/json" localhost:7071/api/uppercase -d '{"gr The HTTP headers of the incoming request will be copied into input Message's MessageHeaders, so they become accessible if need to. It is done in implementation of `UppercaseHandler` which extends `FunctionInvoker`. -NOTE: Implementation of FunctionInvoker, should contain the least amount of code. Everything should be delegated to the base FunctionInvoker. -These implementations of FunctionInvoker are really a type-safe way to define and configure function to be recognized as Azure Function. -Look at it as _configuration with the callback_ (e.g., `this.handleRequest(..)`). +NOTE: Implementation of `FunctionInvoker` (your handler), should contain the least amount of code. It is really a type-safe way to define +and configure function to be recognized as Azure Function. +Everything else should be delegated to the base `FunctionInvoker` via `handleRequest(..)` callback which will invoke your function, taking care of +necessary type conversion, transformation etc. + ---- @FunctionName("uppercase") public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, @@ -40,8 +42,7 @@ public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, The `echo` function does the same as the `uppercase` less the actual uppercasing. However, the important difference to notice is that function itself -takes primitive `String` as its input while the actual handler passes instance of `Message` the same way as with `uppercase`. The framework recognizes that -you only care about the payload and extracts it from the message before calling the function. +takes primitive `String` as its input (i.e., `public Function echo()`) while the actual handler passes instance of `Message` the same way as with `uppercase`. The framework recognizes that you only care about the payload and extracts it from the `Message` before calling the function. There is also a reactive version of 'uppercase' - `uppercaseReactive` which will produce the same result, but diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/UppercaseHandler.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/UppercaseHandler.java index 983c4b350..c95233f30 100644 --- a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/UppercaseHandler.java +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/UppercaseHandler.java @@ -42,5 +42,4 @@ public class UppercaseHandler extends FunctionInvoker, String> { Message message = MessageBuilder.withPayload(request.getBody().get()).copyHeaders(request.getHeaders()).build(); return handleRequest(message, context); } - }