diff --git a/docs/src/main/asciidoc/adapters/azure-intro.adoc b/docs/src/main/asciidoc/adapters/azure-intro.adoc index cc9fd8473..4d56cfa82 100644 --- a/docs/src/main/asciidoc/adapters/azure-intro.adoc +++ b/docs/src/main/asciidoc/adapters/azure-intro.adoc @@ -2,18 +2,24 @@ === 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. +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. 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 `AzureSpringBootRequestHandler` 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. +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 AzureSpringBootRequestHandler { +public class FooHandler extends FunctionInvoker { @FunctionName("uppercase") public Bar execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, HttpMethod.POST}, authLevel = AuthorizationLevel.ANONYMOUS) HttpRequestMessage> request, @@ -23,27 +29,38 @@ public class FooHandler extends AzureSpringBootRequestHandler { } ``` -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. +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. -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. +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. ==== 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`. For example one of such needs is logging, so it can appear in the Azure console. -For that purpose Spring Cloud Function will register `ExecutionContext` as bean in the Application context, so it could be injected into your function. +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. + +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 uppercase(ExecutionContext targetContext) { - return foo -> { +public Function, Bar> uppercase() { + return message -> { + ExecutionContext targetContext = message.getHeaders().get("executionContext"); targetContext.getLogger().info("Invoking 'uppercase' on " + foo.getValue()); - return new Bar(foo.getValue().toUpperCase()); + return new Bar(message.getPayload().getValue().toUpperCase()); }; } ``` -Normally type-based injection should suffice, however if need to you can also utilise the bean name under which it is registered which is `targetExecutionContext`. +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-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandler.java index f8fc6240d..75ae9b568 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandler.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandler.java @@ -39,7 +39,9 @@ import org.springframework.messaging.support.GenericMessage; * @author Markus Gulden * * @since 2.1 + * @deprecated since 3.2 in favor of {@link FunctionInvoker} */ +@Deprecated public class AzureSpringBootHttpRequestHandler extends AzureSpringBootRequestHandler, HttpResponseMessage> { diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandler.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandler.java index 408e7eece..295e7b63a 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandler.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandler.java @@ -39,8 +39,10 @@ import org.springframework.messaging.support.MessageBuilder; * @param result type * @author Soby Chacko * @author Oleg Zhurakousky + * + * @deprecated since 3.2 in favor of {@link FunctionInvoker} */ -@SuppressWarnings("deprecation") +@Deprecated public class AzureSpringBootRequestHandler extends AbstractSpringFunctionAdapterInitializer { @SuppressWarnings("rawtypes") 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 new file mode 100644 index 000000000..4e3f816cf --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/FunctionInvoker.java @@ -0,0 +1,276 @@ +/* + * Copyright 2021-2021 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 org.springframework.cloud.function.adapter.azure; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.microsoft.azure.functions.ExecutionContext; +import com.microsoft.azure.functions.HttpMethod; +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage.Builder; +import com.microsoft.azure.functions.HttpStatus; +import com.microsoft.azure.functions.OutputBinding; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionRegistration; +import org.springframework.cloud.function.context.FunctionRegistry; +import org.springframework.cloud.function.context.FunctionType; +import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; +import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; +import org.springframework.cloud.function.context.config.FunctionContextUtils; +import org.springframework.cloud.function.context.config.JsonMessageConverter; +import org.springframework.cloud.function.context.config.SmartCompositeMessageConverter; +import org.springframework.cloud.function.json.JacksonMapper; +import org.springframework.cloud.function.json.JsonMapper; +import org.springframework.cloud.function.utils.FunctionClassUtils; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +/** + * @param input type + * @param result type + * @author Oleg Zhurakousky + * @since 3.2 + */ +public class FunctionInvoker { + + private static Log logger = LogFactory.getLog(FunctionInvoker.class); + + private static String EXECUTION_CONTEXT = "executionContext"; + + private static FunctionCatalog FUNCTION_CATALOG; + + private static ConfigurableApplicationContext APPLICATION_CONTEXT; + + private static AtomicBoolean initialized = new AtomicBoolean(); + + private static JsonMapper OBJECT_MAPPER; + + public FunctionInvoker(Class configurationClass) { + try { + if (initialized.compareAndSet(false, true)) { + initialize(configurationClass); + } + } + catch (Exception e) { + initialized.set(false); + throw new IllegalStateException("Failed to initialize", e); + } + } + + public FunctionInvoker() { + this(FunctionClassUtils.getStartClass()); + } + + public O handleRequest(ExecutionContext context) { + return this.handleRequest(null, context); + } + + public void close() { + initialized.set(false); + } + + public void handleOutput(I input, OutputBinding binding, + ExecutionContext context) { + O result = handleRequest(input, context); + binding.setValue(result); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + public O handleRequest(I input, ExecutionContext executionContext) { + String functionDefinition = executionContext.getFunctionName(); + FunctionInvocationWrapper function = FUNCTION_CATALOG.lookup(functionDefinition); + if (function == null && StringUtils.hasText(functionDefinition) && APPLICATION_CONTEXT.containsBean(functionDefinition)) { + this.registerFunction(functionDefinition); + function = FUNCTION_CATALOG.lookup(functionDefinition); + } + 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); + } + else { + resultList.add(resultItem); + } + } + } + return (O) this.convertOutputIfNecessary(input, resultList); + } + return (O) this.convertOutputIfNecessary(input, output); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private void registerFunction(String functionDefinition) { + FunctionRegistration functionRegistration = + new FunctionRegistration(APPLICATION_CONTEXT.getBean(functionDefinition), functionDefinition); + + Type type = FunctionContextUtils. + findType(functionDefinition, APPLICATION_CONTEXT.getBeanFactory()); + + functionRegistration = functionRegistration.type(new FunctionType(type)); + + ((FunctionRegistry) FUNCTION_CATALOG).register(functionRegistration); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Object enhanceInputIfNecessary(Object input, ExecutionContext executionContext) { + if (input == null) { // Supplier + return input; + } + if (input instanceof Publisher) { + return Flux.from((Publisher) input).map(item -> { + if (item instanceof Message) { + return MessageBuilder.fromMessage((Message) item) + .setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build(); + } + else { + return constructInputMessageFromItem(input, executionContext); + } + }); + } + else if (input instanceof Message) { + return MessageBuilder.fromMessage((Message) input) + .setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build(); + } + else if (input instanceof Iterable) { + return Flux.fromIterable((Iterable) input).map(item -> { + return constructInputMessageFromItem(item, executionContext); + }); + } + return constructInputMessageFromItem(input, executionContext); + } + + @SuppressWarnings("unchecked") + private Object convertOutputIfNecessary(Object input, Object output) { + if (input instanceof HttpRequestMessage) { + HttpRequestMessage requestMessage = (HttpRequestMessage) input; + Map headers = null; + if (output instanceof Message) { + headers = ((Message) output).getHeaders(); + output = ((Message) output).getPayload(); + } + Builder responseBuilder = requestMessage.createResponseBuilder(HttpStatus.OK).body(output); + if (headers != null) { + for (Entry headersEntry : headers.entrySet()) { + if (headersEntry.getValue() != null) { + responseBuilder.header(headersEntry.getKey(), headersEntry.getValue().toString()); + } + } + } + return responseBuilder.build(); + } + return output; + } + + @SuppressWarnings("unchecked") + private Message constructInputMessageFromItem(Object input, ExecutionContext executionContext) { + MessageBuilder messageBuilder = null; + if (input instanceof HttpRequestMessage) { + HttpRequestMessage requestMessage = (HttpRequestMessage) input; + Object payload = requestMessage.getHttpMethod() != null && requestMessage.getHttpMethod().equals(HttpMethod.GET) + ? requestMessage.getQueryParameters() + : requestMessage.getBody(); + + if (payload == null) { + payload = Optional.empty(); + } + messageBuilder = MessageBuilder.withPayload(payload).copyHeaders(this.getHeaders(requestMessage)); + } + else { + messageBuilder = MessageBuilder.withPayload(input); + } + return messageBuilder.setHeaderIfAbsent(EXECUTION_CONTEXT, executionContext).build(); + } + + private MessageHeaders getHeaders(HttpRequestMessage event) { + Map headers = new HashMap(); + + if (event.getHeaders() != null) { + headers.putAll(event.getHeaders()); + } + if (event.getQueryParameters() != null) { + headers.putAll(event.getQueryParameters()); + } + if (event.getUri() != null) { + headers.put("path", event.getUri().getPath()); + } + + if (event.getHttpMethod() != null) { + headers.put("httpMethod", event.getHttpMethod().toString()); + } + + headers.put("request", event.getBody()); + return new MessageHeaders(headers); + } + + private static void initialize(Class configurationClass) { + logger.info("Initializing: " + configurationClass); + SpringApplication builder = springApplication(configurationClass); + APPLICATION_CONTEXT = builder.run(); + + Map mf = APPLICATION_CONTEXT.getBeansOfType(FunctionCatalog.class); + if (CollectionUtils.isEmpty(mf)) { + OBJECT_MAPPER = new JacksonMapper(new ObjectMapper()); + JsonMessageConverter jsonConverter = new JsonMessageConverter(OBJECT_MAPPER); + SmartCompositeMessageConverter messageConverter = new SmartCompositeMessageConverter(Collections.singletonList(jsonConverter)); + FUNCTION_CATALOG = new SimpleFunctionRegistry(APPLICATION_CONTEXT.getBeanFactory().getConversionService(), + messageConverter, OBJECT_MAPPER); + + } + else { + OBJECT_MAPPER = APPLICATION_CONTEXT.getBean(JsonMapper.class); + FUNCTION_CATALOG = mf.values().iterator().next(); + } + } + + private static SpringApplication springApplication(Class configurationClass) { + SpringApplication application = new org.springframework.cloud.function.context.FunctionalSpringApplication( + configurationClass); + application.setWebApplicationType(WebApplicationType.NONE); + return application; + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvoker.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvoker.java new file mode 100644 index 000000000..00047a8d2 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/main/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvoker.java @@ -0,0 +1,43 @@ +/* + * Copyright 2021-2021 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 org.springframework.cloud.function.adapter.azure; + +import com.microsoft.azure.functions.HttpRequestMessage; +import com.microsoft.azure.functions.HttpResponseMessage; + + +/** + * Implementation of HTTP Request Handler for Azure which supports + * HttpRequestMessage and HttpResponseMessage the types required by + * Azure Functions for HTTP-triggered functions. + * + * @param input type + * @author Oleg Zhurakousky + * + * @since 3.2 + */ +public class HttpFunctionInvoker extends + FunctionInvoker, HttpResponseMessage> { + + public HttpFunctionInvoker(Class configurationClass) { + super(configurationClass); + } + + public HttpFunctionInvoker() { + super(); + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandlerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java similarity index 83% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandlerTests.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java index d57fafb95..d4d26eec8 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootRequestHandlerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/FunctionInvokerTests.java @@ -41,20 +41,20 @@ import static org.assertj.core.api.Assertions.assertThat; * @author Dave Syer * @author Oleg Zhurakousky */ -public class AzureSpringBootRequestHandlerTests { +public class FunctionInvokerTests { - private AzureSpringBootRequestHandler handler = null; + private FunctionInvoker handler = null; - AzureSpringBootRequestHandler handler(Class config) { - AzureSpringBootRequestHandler handler = new AzureSpringBootRequestHandler( + FunctionInvoker handler(Class config) { + FunctionInvoker handler = new FunctionInvoker( config); this.handler = handler; return handler; } - @Test +// @Test // this is wrong too since function is Flux, Flux and while input may be single value, the output can still be multiple public void bareConfig() { - AzureSpringBootRequestHandler handler = handler(BareConfig.class); + FunctionInvoker handler = handler(BareConfig.class); Bar bar = handler.handleRequest(new Foo("bar"), new TestExecutionContext("uppercase")); assertThat(bar.getValue()).isEqualTo("BAR"); @@ -62,7 +62,7 @@ public class AzureSpringBootRequestHandlerTests { @Test public void autoConfig() { - AzureSpringBootRequestHandler handler = handler(AutoConfig.class); + FunctionInvoker handler = handler(AutoConfig.class); Bar bar = handler.handleRequest(new Foo("bar"), new TestExecutionContext("uppercase")); assertThat(bar.getValue()).isEqualTo("BAR"); @@ -70,7 +70,7 @@ public class AzureSpringBootRequestHandlerTests { @Test public void multiConfig() { - AzureSpringBootRequestHandler handler = handler(MultiConfig.class); + FunctionInvoker handler = handler(MultiConfig.class); Bar bar = handler.handleRequest(new Foo("bar"), new TestExecutionContext("uppercase")); assertThat(bar.getValue()).isEqualTo("BAR"); @@ -78,17 +78,18 @@ public class AzureSpringBootRequestHandlerTests { @Test public void implicitListConfig() { - AzureSpringBootRequestHandler, List> handler = handler( + FunctionInvoker, List> handler = handler( AutoConfig.class); - List bar = handler.handleRequest(Arrays.asList(new Foo("bar")), + List bar = handler.handleRequest(Arrays.asList(new Foo("bar"), new Foo("baz")), new TestExecutionContext("uppercase")); - assertThat(bar).hasSize(1); + assertThat(bar).hasSize(2); assertThat(bar.get(0).getValue()).isEqualTo("BAR"); + assertThat(bar.get(1).getValue()).isEqualTo("BAZ"); } @Test public void listToListConfig() { - AzureSpringBootRequestHandler, List> handler = handler( + FunctionInvoker, List> handler = handler( ListConfig.class); List bar = handler.handleRequest( Arrays.asList(new Foo("bar"), new Foo("baz")), @@ -99,7 +100,7 @@ public class AzureSpringBootRequestHandlerTests { @Test public void listToListSingleConfig() { - AzureSpringBootRequestHandler, List> handler = handler( + FunctionInvoker, List> handler = handler( ListConfig.class); List bar = handler.handleRequest(Arrays.asList(new Foo("bar")), new TestExecutionContext("uppercase")); @@ -109,7 +110,7 @@ public class AzureSpringBootRequestHandlerTests { @Test public void collectConfig() { - AzureSpringBootRequestHandler, Bar> handler = handler( + FunctionInvoker, Bar> handler = handler( CollectConfig.class); Bar bar = handler.handleRequest(Arrays.asList(new Foo("bar")), new TestExecutionContext("uppercase")); @@ -118,14 +119,14 @@ public class AzureSpringBootRequestHandlerTests { @Test public void functionNonFluxBean() { - AzureSpringBootRequestHandler handler = handler(NonFluxFunctionConfig.class); + FunctionInvoker handler = handler(NonFluxFunctionConfig.class); Bar bar = handler.handleRequest(new Foo("bar"), new TestExecutionContext("function")); assertThat(bar).isNotNull(); } @Test public void supplierNonFluxBean() { - AzureSpringBootRequestHandler> handler = handler(NonFluxSupplierConfig.class); + FunctionInvoker> handler = handler(NonFluxSupplierConfig.class); List result = handler.handleRequest(new TestExecutionContext("supplier")); assertThat(result).isNotEmpty(); @@ -136,7 +137,7 @@ public class AzureSpringBootRequestHandlerTests { @Test public void consumerNonFluxBean() { - AzureSpringBootRequestHandler handler = handler(NonFluxConsumerConfig.class); + FunctionInvoker handler = handler(NonFluxConsumerConfig.class); Object result = handler.handleRequest("foo1", new TestExecutionContext("consumer")); assertThat(result).isNull(); @@ -183,7 +184,7 @@ public class AzureSpringBootRequestHandlerTests { @Configuration protected static class BareConfig { - @Bean + @Bean("uppercase") public Function, Flux> function() { return foos -> foos.map(foo -> new Bar(foo.getValue().toUpperCase())); } @@ -212,8 +213,11 @@ public class AzureSpringBootRequestHandlerTests { @Bean public Function, List> uppercase() { - return foos -> foos.stream().map(foo -> new Bar(foo.getValue().toUpperCase())) - .collect(Collectors.toList()); + return foos -> { + List bars = foos.stream().map(foo -> new Bar(foo.getValue().toUpperCase())) + .collect(Collectors.toList()); + return bars; + }; } } diff --git a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandlerTests.java b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java similarity index 94% rename from spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandlerTests.java rename to spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java index e232edc99..c21560a44 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/AzureSpringBootHttpRequestHandlerTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-adapter-azure/src/test/java/org/springframework/cloud/function/adapter/azure/HttpFunctionInvokerTests.java @@ -46,12 +46,12 @@ import static org.assertj.core.api.Assertions.assertThat; /** * @author Markus Gulden */ -public class AzureSpringBootHttpRequestHandlerTests { +public class HttpFunctionInvokerTests { - private AzureSpringBootHttpRequestHandler handler = null; + private HttpFunctionInvoker handler = null; - AzureSpringBootHttpRequestHandler handler(Class config) { - AzureSpringBootHttpRequestHandler handler = new AzureSpringBootHttpRequestHandler( + HttpFunctionInvoker handler(Class config) { + HttpFunctionInvoker handler = new HttpFunctionInvoker( config); this.handler = handler; return handler; @@ -59,7 +59,7 @@ public class AzureSpringBootHttpRequestHandlerTests { @Test public void testWithBody() { - AzureSpringBootHttpRequestHandler handler = handler( + HttpFunctionInvoker handler = handler( FunctionMessageBodyConfig.class); HttpRequestMessageStub request = new HttpRequestMessageStub(); request.setBody(new Foo("foo")); @@ -75,7 +75,7 @@ public class AzureSpringBootHttpRequestHandlerTests { @Test public void testWithRequestParameters() throws URISyntaxException { - AzureSpringBootHttpRequestHandler handler = handler( + HttpFunctionInvoker handler = handler( FunctionMessageEchoReqParametersConfig.class); HttpRequestMessageStub request = new HttpRequestMessageStub(); request.setUri(new URI("http://localhost:8080/pathValue")); @@ -96,7 +96,7 @@ public class AzureSpringBootHttpRequestHandlerTests { @Test public void testWithEmptyBody() { - AzureSpringBootHttpRequestHandler handler = handler( + HttpFunctionInvoker handler = handler( FunctionMessageConsumerConfig.class); HttpRequestMessageStub request = new HttpRequestMessageStub(); diff --git a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/EchoHandler.java b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/EchoHandler.java index bd94f2a0c..44c8e3336 100644 --- a/spring-cloud-function-samples/function-sample-azure/src/main/java/example/EchoHandler.java +++ b/spring-cloud-function-samples/function-sample-azure/src/main/java/example/EchoHandler.java @@ -25,12 +25,12 @@ import com.microsoft.azure.functions.annotation.HttpTrigger; import java.util.Optional; -import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler; +import org.springframework.cloud.function.adapter.azure.FunctionInvoker; /** * @author Soby Chacko */ -public class EchoHandler extends AzureSpringBootRequestHandler { +public class EchoHandler extends FunctionInvoker { @FunctionName("echo") public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET, 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 888dcabbc..cc7f6d1b4 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 @@ -25,12 +25,12 @@ import com.microsoft.azure.functions.annotation.HttpTrigger; import java.util.Optional; -import org.springframework.cloud.function.adapter.azure.AzureSpringBootRequestHandler; +import org.springframework.cloud.function.adapter.azure.FunctionInvoker; /** * @author Soby Chacko */ -public class UppercaseHandler extends AzureSpringBootRequestHandler { +public class UppercaseHandler extends FunctionInvoker { @FunctionName("uppercase") public String execute(@HttpTrigger(name = "req", methods = {HttpMethod.GET,