GH-460 Add support for function filtering

Added support for function filtering in the event there are more then one function in catalog. This is primarily to ensure that
we have a mechanism to specify which functions to export as web enpoints (instead of all)

Resolves #460
This commit is contained in:
Oleg Zhurakousky
2020-03-23 11:19:40 +01:00
parent 919b9902e9
commit 85000ee084
6 changed files with 278 additions and 22 deletions

View File

@@ -24,6 +24,7 @@ import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -51,6 +52,7 @@ import org.springframework.aop.support.AopUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.cloud.function.context.FunctionCatalog;
@@ -94,7 +96,7 @@ import org.springframework.util.StringUtils;
* @since 3.0
*/
public class BeanFactoryAwareFunctionRegistry
implements FunctionRegistry, FunctionInspector, ApplicationContextAware {
implements FunctionRegistry, FunctionInspector, ApplicationContextAware, InitializingBean {
private static Log logger = LogFactory.getLog(BeanFactoryAwareFunctionRegistry.class);
@@ -118,10 +120,22 @@ public class BeanFactoryAwareFunctionRegistry
private final CompositeMessageConverter messageConverter;
private List<String> declaredFunctionDefinitions;
public BeanFactoryAwareFunctionRegistry(ConversionService conversionService,
@Nullable CompositeMessageConverter messageConverter) {
this.conversionService = conversionService;
this.messageConverter = messageConverter;
}
@Override
public void afterPropertiesSet() throws Exception {
String userDefinition = this.applicationContext.getEnvironment().getProperty("spring.cloud.function.definition");
this.declaredFunctionDefinitions = StringUtils.hasText(userDefinition) ? Arrays.asList(userDefinition.split(";")) : Collections.emptyList();
if (this.declaredFunctionDefinitions.contains(RoutingFunction.FUNCTION_NAME)) {
Assert.isTrue(this.declaredFunctionDefinitions.size() == 1, "It is illegal to declare more then one function when using RoutingFunction");
}
}
@Override
@@ -139,9 +153,36 @@ public class BeanFactoryAwareFunctionRegistry
@Override
@SuppressWarnings("unchecked")
public <T> T lookup(String definition, String... acceptedOutputTypes) {
if (!StringUtils.hasText(definition)) {
definition = this.applicationContext.getEnvironment().getProperty("spring.cloud.function.definition");
definition = StringUtils.hasText(definition) ? definition.replaceAll(",", "|") : "";
boolean routing = definition.contains(RoutingFunction.FUNCTION_NAME)
|| this.declaredFunctionDefinitions.contains(RoutingFunction.FUNCTION_NAME);
if (!routing && this.declaredFunctionDefinitions.size() > 0) {
if (StringUtils.hasText(definition)) {
if (this.declaredFunctionDefinitions.size() > 1 &&!this.declaredFunctionDefinitions.contains(definition)) {
logger.warn("Attempted to access un-declared function definition '" + definition + "'. Declared functions are '" + this.declaredFunctionDefinitions
+ "' specified via `spring.cloud.function.definition` property. If the intention is to access "
+ "any function available in FunctionCatalog, please remove `spring.cloud.function.definition` property.");
return null;
}
}
else {
if (this.declaredFunctionDefinitions.size() == 1) {
definition = this.declaredFunctionDefinitions.get(0);
}
else if (this.declaredFunctionDefinitions.size() > 1) {
logger.warn("Default function can not be mapped since multiple functions are declared " + this.declaredFunctionDefinitions);
return null;
}
else {
logger.warn("Default function can not be mapped since multiple functions are available in FunctionCatalog. "
+ "Please use 'spring.cloud.function.definition' property.");
return null;
}
}
}
Object function = this
.proxyInvokerIfNecessary((FunctionInvocationWrapper) this.compose(null, definition, acceptedOutputTypes));
return (T) function;

View File

@@ -28,6 +28,7 @@ import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import reactor.core.publisher.Flux;
@@ -77,6 +78,11 @@ public class BeanFactoryAwareFunctionRegistryTests {
return catalog;
}
@Before
public void before() {
System.clearProperty("spring.cloud.function.definition");
}
@Test
public void testDefaultLookup() throws Exception {
FunctionCatalog catalog = this.configureCatalog();
@@ -84,6 +90,7 @@ public class BeanFactoryAwareFunctionRegistryTests {
assertThat(function).isNull();
//==
System.setProperty("spring.cloud.function.definition", "uppercase");
catalog = this.configureCatalog();
function = catalog.lookup("");
assertThat(function).isNotNull();
Field field = ReflectionUtils.findField(FunctionInvocationWrapper.class, "composed");
@@ -91,6 +98,7 @@ public class BeanFactoryAwareFunctionRegistryTests {
assertThat(((boolean) field.get(function))).isFalse();
//==
System.setProperty("spring.cloud.function.definition", "uppercase|uppercaseFlux");
catalog = this.configureCatalog();
function = catalog.lookup("", "application/json");
Function<Flux<String>, Flux<Message<String>>> typedFunction = (Function<Flux<String>, Flux<Message<String>>>) function;
Object blockFirst = typedFunction.apply(Flux.just("hello")).blockFirst();
@@ -354,7 +362,7 @@ public class BeanFactoryAwareFunctionRegistryTests {
FunctionCatalog catalog = this.configureCatalog(CollectionOutConfiguration.class);
FunctionInvocationWrapper function = catalog.lookup("parseToList", "application/json");
assertThat(function).isNotNull();
Object result = (Message) function.apply(MessageBuilder.withPayload("1, 2, 3".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build());
Object result = function.apply(MessageBuilder.withPayload("1, 2, 3".getBytes()).setHeader(MessageHeaders.CONTENT_TYPE, "text/plain").build());
assertThat(result instanceof Message).isTrue();
function = catalog.lookup("parseToListOfMessages", "application/json");