GH-238 Added initial support for RoutingFunction
- Added initial implementation of RoutingFunction which is bootstrapped optionally based on setting ‘spring.cloud.function.routing.enabled’ property to true. - Added initial documentation and tests Resolves #238
This commit is contained in:
@@ -23,7 +23,6 @@ import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
@@ -42,6 +41,7 @@ import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||
import org.springframework.cloud.function.context.config.FunctionContextUtils;
|
||||
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||
import org.springframework.context.ApplicationContextInitializer;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
@@ -196,7 +196,6 @@ public abstract class AbstractSpringFunctionAdapterInitializer<C> implements Clo
|
||||
return "";
|
||||
}
|
||||
|
||||
//@SuppressWarnings("unchecked")
|
||||
protected Object convertOutput(Object input, Object output) {
|
||||
return output;
|
||||
}
|
||||
@@ -368,22 +367,19 @@ public abstract class AbstractSpringFunctionAdapterInitializer<C> implements Clo
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.catalog.size() == 1) {
|
||||
Iterator<String> names = this.catalog.getNames(Function.class).iterator();
|
||||
if (names.hasNext()) {
|
||||
this.function = this.catalog.lookup(Function.class, names.next());
|
||||
|
||||
if (this.catalog.size() >= 1 && this.catalog.size() <= 2) { // we may have RoutingFunction function
|
||||
String functionName = this.catalog.getNames(Function.class).stream()
|
||||
.filter(n -> !n.equals(RoutingFunction.FUNCTION_NAME))
|
||||
.findFirst().orElseGet(() -> null);
|
||||
if (functionName != null) {
|
||||
this.function = this.catalog.lookup(Function.class, functionName);
|
||||
return;
|
||||
}
|
||||
|
||||
names = this.catalog.getNames(Consumer.class).iterator();
|
||||
if (names.hasNext()) {
|
||||
this.consumer = this.catalog.lookup(Consumer.class, names.next());
|
||||
return;
|
||||
}
|
||||
|
||||
names = this.catalog.getNames(Supplier.class).iterator();
|
||||
if (names.hasNext()) {
|
||||
this.supplier = this.catalog.lookup(Supplier.class, names.next());
|
||||
functionName = this.catalog.getNames(Supplier.class).stream()
|
||||
.findFirst().orElseGet(() -> null);
|
||||
if (functionName != null) {
|
||||
this.supplier = this.catalog.lookup(Supplier.class, functionName);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.beans.factory.BeanNameAware;
|
||||
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||
import org.springframework.cloud.function.core.FluxConsumer;
|
||||
import org.springframework.cloud.function.core.FluxFunction;
|
||||
import org.springframework.cloud.function.core.FluxSupplier;
|
||||
@@ -152,6 +153,13 @@ public class FunctionRegistration<T> implements BeanNameAware {
|
||||
if (this.type == null) {
|
||||
result = (FunctionRegistration<S>) this;
|
||||
}
|
||||
else if (this.target instanceof RoutingFunction) {
|
||||
S target = (S) this.target;
|
||||
result = new FunctionRegistration<S>(target);
|
||||
result.type(this.type.getType());
|
||||
result = result.target(target).names(this.names)
|
||||
.type(result.type.wrap(Flux.class)).properties(this.properties);
|
||||
}
|
||||
else {
|
||||
S target = (S) this.target;
|
||||
result = new FunctionRegistration<S>(target);
|
||||
@@ -178,7 +186,6 @@ public class FunctionRegistration<T> implements BeanNameAware {
|
||||
else if (target instanceof Function) {
|
||||
target = (S) new FluxedFunction((Function<?, ?>) target);
|
||||
}
|
||||
|
||||
result = result.target(target).names(this.names)
|
||||
.type(result.type.wrap(Flux.class)).properties(this.properties);
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import reactor.core.publisher.Mono;
|
||||
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.config.RoutingFunction;
|
||||
import org.springframework.cloud.function.core.FluxConsumer;
|
||||
import org.springframework.cloud.function.core.FluxSupplier;
|
||||
import org.springframework.cloud.function.core.FluxToMonoFunction;
|
||||
@@ -292,8 +293,11 @@ public abstract class AbstractComposableFunctionRegistry implements FunctionRegi
|
||||
if (lookup.containsKey(name)) {
|
||||
composedFunction = lookup.get(name);
|
||||
}
|
||||
else if (name.equals("") && lookup.size() == 1) {
|
||||
composedFunction = lookup.values().iterator().next();
|
||||
else if (name.equals("") && lookup.size() >= 1 && lookup.size() <= 2) { // we may have RoutingFunction function
|
||||
String functionName = lookup.keySet().stream()
|
||||
.filter(fName -> !fName.equals(RoutingFunction.FUNCTION_NAME))
|
||||
.findFirst().orElseGet(() -> null);
|
||||
composedFunction = lookup.get(functionName);
|
||||
}
|
||||
else {
|
||||
String[] stages = StringUtils.delimitedListToStringArray(name, "|");
|
||||
|
||||
@@ -33,11 +33,12 @@ import org.springframework.messaging.support.MessageBuilder;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @since 2.1
|
||||
*/
|
||||
public class MessageFunction
|
||||
implements Function<Publisher<Message<?>>, Publisher<Message<?>>> {
|
||||
|
||||
private Function<?, ?> delegate;
|
||||
private final Function<?, ?> delegate;
|
||||
|
||||
public MessageFunction(Function<?, ?> delegate) {
|
||||
this.delegate = delegate;
|
||||
@@ -91,5 +92,4 @@ public class MessageFunction
|
||||
value -> MessageBuilder.withPayload(function.apply(value.getPayload()))
|
||||
.copyHeaders(value.getHeaders()).build());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.function.context.config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
@@ -50,6 +51,7 @@ 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.AbstractComposableFunctionRegistry;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionUnregistrationEvent;
|
||||
import org.springframework.cloud.function.json.GsonMapper;
|
||||
import org.springframework.cloud.function.json.JacksonMapper;
|
||||
@@ -62,6 +64,11 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.FilterType;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.core.type.StandardMethodMetadata;
|
||||
import org.springframework.messaging.converter.ByteArrayMessageConverter;
|
||||
import org.springframework.messaging.converter.CompositeMessageConverter;
|
||||
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.messaging.converter.StringMessageConverter;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
@@ -84,6 +91,17 @@ public class ContextFunctionCatalogAutoConfiguration {
|
||||
return new BeanFactoryFunctionCatalog();
|
||||
}
|
||||
|
||||
@Bean(RoutingFunction.FUNCTION_NAME)
|
||||
@ConditionalOnProperty(name = "spring.cloud.function.routing.enabled", havingValue = "true")
|
||||
RoutingFunction gateway(FunctionCatalog functionCatalog, FunctionInspector functionInspector) {
|
||||
Collection<MessageConverter> messageConverters = new ArrayList<MessageConverter>();
|
||||
messageConverters.add(new MappingJackson2MessageConverter());
|
||||
messageConverters.add(new StringMessageConverter());
|
||||
messageConverters.add(new ByteArrayMessageConverter());
|
||||
CompositeMessageConverter messageConverter = new CompositeMessageConverter(messageConverters);
|
||||
return new RoutingFunction(functionCatalog, functionInspector, messageConverter);
|
||||
}
|
||||
|
||||
protected static class BeanFactoryFunctionCatalog
|
||||
extends AbstractComposableFunctionRegistry
|
||||
implements SmartInitializingSingleton, BeanFactoryAware {
|
||||
@@ -96,6 +114,7 @@ public class ContextFunctionCatalogAutoConfiguration {
|
||||
* Will collect all suppliers, functions, consumers and function registration as
|
||||
* late as possible in the lifecycle.
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Override
|
||||
public void afterSingletonsInstantiated() {
|
||||
Map<String, Supplier> supplierBeans = this.beanFactory
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2019-2019 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.context.config;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.SignalType;
|
||||
|
||||
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||
import org.springframework.cloud.function.core.WrappedFunction;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.messaging.support.MessageBuilder;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* An implementation of Function which acts as a gateway/router by actually
|
||||
* delegating incoming invocation to a function specified via `function.name`
|
||||
* message header. <br>
|
||||
* {@link Message} is used as a canonical representation of a request which
|
||||
* contains some metadata and it is the responsibility of the higher level
|
||||
* framework to convert the incoming request into a Message. For example;
|
||||
* spring-cloud-function-web will create Message from HttpRequest copying all
|
||||
* HTTP headers as message headers.
|
||||
*
|
||||
* @author Oleg Zhurakousky
|
||||
* @since 2.1
|
||||
*
|
||||
*/
|
||||
public class RoutingFunction implements Function<Publisher<Message<?>>, Publisher<?>> {
|
||||
|
||||
/**
|
||||
* The name of this function use by BeanFactory.
|
||||
*/
|
||||
public static final String FUNCTION_NAME = "router";
|
||||
|
||||
private final FunctionCatalog functionCatalog;
|
||||
|
||||
private final FunctionInspector functionInspector;
|
||||
|
||||
private final MessageConverter messageConverter;
|
||||
|
||||
RoutingFunction(FunctionCatalog functionCatalog, FunctionInspector functionInspector,
|
||||
MessageConverter messageConverter) {
|
||||
this.functionCatalog = functionCatalog;
|
||||
this.functionInspector = functionInspector;
|
||||
this.messageConverter = messageConverter;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Publisher<?> apply(Publisher<Message<?>> input) {
|
||||
return Flux.from(input)
|
||||
.switchOnFirst((signal, flux) -> {
|
||||
Assert.isTrue(signal.hasValue()
|
||||
&& signal.getType() == SignalType.ON_NEXT, "Signal has no value or wrong type " + signal);
|
||||
Function<Flux<Object>, Publisher<Object>> function = this.getRouteToFunction(signal.get());
|
||||
return flux.map(message -> {
|
||||
Object inputValue = this.convertInput(message, function);
|
||||
return inputValue;
|
||||
}).transform(function);
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressWarnings("rawtypes")
|
||||
private WrappedFunction getRouteToFunction(Message<?> message) {
|
||||
String routeToFunctionName = (String) message.getHeaders().get("function.name");
|
||||
WrappedFunction function = functionCatalog.lookup(routeToFunctionName);
|
||||
Assert.notNull(function, "Failed to locate function specified with 'function.name':"
|
||||
+ message.getHeaders().get("function.name"));
|
||||
return function;
|
||||
}
|
||||
|
||||
private Object convertInput(Message<?> message, Object function) {
|
||||
Class<?> inputType = functionInspector.getInputType(function);
|
||||
Object inputValue = message.getPayload();
|
||||
if (!inputValue.getClass().isAssignableFrom(inputType)) {
|
||||
inputValue = this.messageConverter.fromMessage(message, functionInspector.getInputType(function));
|
||||
}
|
||||
if (this.functionInspector.isMessage(function)) {
|
||||
inputValue = MessageBuilder.createMessage(inputValue, message.getHeaders());
|
||||
}
|
||||
Assert.notNull(inputValue, "Failed to determine input value of type "
|
||||
+ inputType + " from Message '"
|
||||
+ message + "'. No suitable Message Converter found.");
|
||||
return inputValue;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,12 @@
|
||||
"type": "java.lang.String",
|
||||
"description": "Name (e.g., 'foo') or composition instruction (e.g., 'foo|bar') used to resolve default function especially for cases where there is more then once function available in catalog.",
|
||||
"defaultValue": ""
|
||||
},
|
||||
{
|
||||
"name": "spring.cloud.function.routing.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables RoutingFunction which delegates incoming request to a function named via function.name header",
|
||||
"defaultValue": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user