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:
Oleg Zhurakousky
2019-05-03 23:15:40 +02:00
parent abeb652830
commit 4d9cdb9750
15 changed files with 557 additions and 187 deletions

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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, "|");

View File

@@ -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());
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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
}
]
}