From 3d1c1e2314aa4afd1a6bacb58e5a7b3c5d01a97d Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Tue, 25 May 2021 17:25:06 +0200 Subject: [PATCH] GH-692 Fix reactive support in Azure FunctionInvoker Resolves #692 --- .../adapter/azure/FunctionInvoker.java | 30 +++++++------ .../adapter/azure/FunctionInvokerTests.java | 28 +++++++++++++ .../function-sample-azure/README.adoc | 3 ++ .../function-sample-azure/pom.xml | 2 +- .../src/main/java/example/Config.java | 10 +++++ .../example/ReactiveUppercaseHandler.java | 42 +++++++++++++++++++ 6 files changed, 102 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveUppercaseHandler.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java index ff3988182..73840fb83 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java @@ -37,6 +37,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.SpringApplication; import org.springframework.boot.WebApplicationType; @@ -119,23 +120,28 @@ public class FunctionInvoker { Object enhancedInput = enhanceInputIfNecessary(input, executionContext); Object output = function.apply(enhancedInput); - if (output instanceof Publisher && !function.isOutputTypePublisher()) { - List resultList = new ArrayList<>(); - for (Object resultItem : Flux.from((Publisher) output).toIterable()) { - if (resultItem instanceof Collection) { - resultList.addAll((Collection) resultItem); - } - else { - if (Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getInputType())) - && !Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getOutputType()))) { - return (O) this.convertOutputIfNecessary(input, resultItem); + if (output instanceof Publisher) { + if (FunctionTypeUtils.isMono(function.getOutputType())) { + return (O) this.convertOutputIfNecessary(input, Mono.from((Publisher) output).blockOptional().get()); + } + else { + List resultList = new ArrayList<>(); + for (Object resultItem : Flux.from((Publisher) output).toIterable()) { + if (resultItem instanceof Collection) { + resultList.addAll((Collection) resultItem); } else { - resultList.add(resultItem); + if (Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getInputType())) + && !Collection.class.isAssignableFrom(FunctionTypeUtils.getRawType(function.getOutputType()))) { + return (O) this.convertOutputIfNecessary(input, resultItem); + } + else { + resultList.add(resultItem); + } } } + return (O) this.convertOutputIfNecessary(input, resultList); } - return (O) this.convertOutputIfNecessary(input, resultList); } return (O) this.convertOutputIfNecessary(input, output); } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java index d4d26eec8..1f31d49ce 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java @@ -28,6 +28,7 @@ import com.microsoft.azure.functions.ExecutionContext; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.Bean; @@ -144,6 +145,17 @@ public class FunctionInvokerTests { assertThat(consumerResult).isEqualTo("foo1"); } + @Test + public void testReactiveFunctions() { + FunctionInvoker handler = handler(ReactiveFunctionConfiguration.class); + String result = handler.handleRequest("hello", new TestExecutionContext("uppercaseMono")); + + System.out.println(result); + +// assertThat(result).isNull(); +// assertThat(consumerResult).isEqualTo("foo1"); + } + @AfterEach public void close() throws IOException { if (this.handler != null) { @@ -161,6 +173,22 @@ public class FunctionInvokerTests { } + @Configuration + @EnableAutoConfiguration + protected static class ReactiveFunctionConfiguration { + + @Bean + public Function, Flux> echoStream() { + return f -> f; + } + + @Bean + public Function, Mono> uppercaseMono() { + return f -> f.map(v -> v.toUpperCase()); + } + + } + @Configuration protected static class NonFluxSupplierConfig { diff --git a/spring-cloud-function-samples/function-sample-azure/README.adoc b/spring-cloud-function-samples/function-sample-azure/README.adoc index c8d9d6313..66cc7dbad 100644 --- a/spring-cloud-function-samples/function-sample-azure/README.adoc +++ b/spring-cloud-function-samples/function-sample-azure/README.adoc @@ -37,6 +37,9 @@ $ http POST localhost:7071/api/uppercase greeting=hello name='your name' The same is for `echo` function, however it will take any input since all it does is just echos it back. +There is also a reactive version of 'uppercase' - `uppercaseReactive` which will produce the same result, but +demonstrates and validates the ability to use reactive functions with Azure. + To run locally on top of Azure Functions, and to deploy to your live Azure environment, you will need the Azure Functions Core Tools installed along with the Azure CLI (see https://docs.microsoft.com/en-us/azure/azure-functions/functions-create-first-java-maven for more details). To deploy the function to your live Azure environment, including an automatic provisioning of an HTTPTrigger for the function: diff --git a/spring-cloud-function-samples/function-sample-azure/pom.xml b/spring-cloud-function-samples/function-sample-azure/pom.xml index 3fa46faaf..fa48d9d2b 100644 --- a/spring-cloud-function-samples/function-sample-azure/pom.xml +++ b/spring-cloud-function-samples/function-sample-azure/pom.xml @@ -54,7 +54,7 @@ org.springframework.cloud spring-cloud-function-dependencies - 3.1.2-SNAPSHOT + 3.1.3-SNAPSHOT pom import diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java index 72e77958d..beafa72af 100644 --- a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/Config.java @@ -27,6 +27,8 @@ import org.springframework.messaging.Message; import com.microsoft.azure.functions.ExecutionContext; +import reactor.core.publisher.Mono; + @SpringBootApplication public class Config { @@ -65,5 +67,13 @@ public class Config { }; } + + @Bean + public Function, Mono> uppercaseReactive() { + return mono -> mono.map(value -> { + return value.toUpperCase(); + }); + } + } diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveUppercaseHandler.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveUppercaseHandler.java new file mode 100644 index 000000000..cb82087b5 --- /dev/null +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/ReactiveUppercaseHandler.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package example; + +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.annotation.AuthorizationLevel; +import com.microsoft.azure.functions.annotation.FunctionName; +import com.microsoft.azure.functions.annotation.HttpTrigger; + +import java.util.Optional; + +import org.springframework.cloud.function.adapter.azure.FunctionInvoker; + +/** + * @author Oleg Zhurakousky + */ +public class ReactiveUppercaseHandler extends FunctionInvoker { + + @FunctionName("uppercaseReactive") + public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, + HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, + ExecutionContext context) { + return handleRequest(request.getBody().get(), context); + } + +}