Fix documentation around ExecutionContext for Azure

More cleanup in Azure samples

Resolves #759
This commit is contained in:
Oleg Zhurakousky
2021-11-18 16:17:15 +01:00
parent 52fd99998c
commit 946a37dccc
3 changed files with 51 additions and 45 deletions

View File

@@ -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<Foo, Bar> {
public class UppercaseHandler extends FunctionInvoker<Message<String>, String> {
@FunctionName("uppercase")
public Bar execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET,
HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<Foo>> request,
public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET,
HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage<Optional<String>> request,
ExecutionContext context) {
return handleRequest(request.getBody().get(), context);
Message<String> message = MessageBuilder.withPayload(request.getBody().get()).copyHeaders(request.getHeaders()).build();
return handleRequest(message, context);
}
}
```
This Azure handler will delegate to a `Function<Foo,Bar>` bean (or a `Function<Publisher<Foo>,Publisher<Bar>>`). 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<String,Object>`, 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<String, String> uppercase() {
return payload -> payload.toUpperCase();
}
OR
@Bean
public Function<Message<String>, 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<Message<Foo>, Bar> uppercase() {
public Function<Message<String>, 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

View File

@@ -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<String, String> 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

View File

@@ -42,5 +42,4 @@ public class UppercaseHandler extends FunctionInvoker<Message<String>, String> {
Message<String> message = MessageBuilder.withPayload(request.getBody().get()).copyHeaders(request.getHeaders()).build();
return handleRequest(message, context);
}
}