From d67159729da9277ba4a1b8a9cb06e150fa98349a Mon Sep 17 00:00:00 2001 From: Dave Syer Date: Fri, 31 Mar 2017 14:15:04 +0100 Subject: [PATCH] Add support for explicit FunctionRegistration A bean of type FunctionRegistration registers the function with user-specified name and other properties, rather than relying on the bean name. Alternatively, function catalog keys can be specified as a @Qualifier, which will be used instead of the bean name if no registration is found. --- spring-cloud-function-context/pom.xml | 9 + ...ntextFunctionCatalogAutoConfiguration.java | 141 +++++++++++----- .../cloud/function/context/FunctionEntry.java | 45 +++++ .../context/FunctionRegistration.java | 86 ++++++++++ .../context/InMemoryFunctionCatalog.java | 25 +++ ...FunctionCatalogAutoConfigurationTests.java | 159 ++++++++++++++++++ 6 files changed, 427 insertions(+), 38 deletions(-) create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionEntry.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java create mode 100644 spring-cloud-function-context/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java diff --git a/spring-cloud-function-context/pom.xml b/spring-cloud-function-context/pom.xml index 683aadd71..8722712d9 100644 --- a/spring-cloud-function-context/pom.xml +++ b/spring-cloud-function-context/pom.xml @@ -24,6 +24,10 @@ spring-cloud-function-core ${spring-cloud-function.version} + + org.springframework + spring-web + com.fasterxml.jackson.core jackson-databind @@ -33,5 +37,10 @@ spring-boot-configuration-processor true + + org.springframework.boot + spring-boot-starter-test + test + diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java index 9b1f503a6..b4effd5fb 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java @@ -19,9 +19,12 @@ package org.springframework.cloud.function.context; import java.lang.reflect.Field; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; import java.util.function.Consumer; @@ -32,6 +35,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.BeansException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -48,6 +52,7 @@ import org.springframework.cloud.function.support.FunctionUtils; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.type.StandardMethodMetadata; import org.springframework.stereotype.Component; import org.springframework.util.ClassUtils; @@ -73,12 +78,14 @@ public class ContextFunctionCatalogAutoConfiguration { @Autowired(required = false) private Map> consumers = Collections.emptyMap(); + @Autowired(required = false) + private Map> registrations = Collections.emptyMap(); + @Bean public FunctionCatalog functionCatalog(ContextFunctionPostProcessor processor, ObjectMapper mapper) { - return new InMemoryFunctionCatalog(processor.wrapSuppliers(mapper, suppliers), - processor.wrapFunctions(mapper, functions), - processor.wrapConsumers(mapper, consumers)); + return new InMemoryFunctionCatalog( + processor.merge(registrations, consumers, suppliers, functions, mapper)); } @Component @@ -96,17 +103,101 @@ public class ContextFunctionCatalogAutoConfiguration { this.registry = registry; } - public Map> wrapSuppliers(ObjectMapper mapper, - Map> suppliers) { - Map> result = new HashMap<>(); - for (String key : suppliers.keySet()) { - Supplier target = target(suppliers.get(key), mapper, key); - result.put(key, target); - for (String name : registry.getAliases(key)) { - result.put(name, target); + public Set> merge( + Map> initial, + Map> consumers, Map> suppliers, + Map> functions, ObjectMapper mapper) { + Set> registrations = new HashSet<>(); + Map targets = new HashMap<>(); + // Replace the initial registrations with new ones that have the right names + for (String key : initial.keySet()) { + FunctionRegistration registration = initial.get(key); + if (registration.getNames().isEmpty()) { + registration.names(getAliases(key)); + } + registrations.add(registration); + targets.put(registration.getTarget(), key); + } + // Add consumers that were not already registered + for (String key : consumers.keySet()) { + if (!targets.containsKey(consumers.get(key))) { + FunctionRegistration target = new FunctionRegistration() + .target(consumers.get(key)).names(getAliases(key)); + targets.put(target.getTarget(), key); + registrations.add(target); } } - return result; + // Add suppliers that were not already registered + for (String key : suppliers.keySet()) { + if (!targets.containsKey(suppliers.get(key))) { + FunctionRegistration target = new FunctionRegistration() + .target(suppliers.get(key)).names(getAliases(key)); + targets.put(target.getTarget(), key); + registrations.add(target); + } + } + // Add functions that were not already registered + for (String key : functions.keySet()) { + if (!targets.containsKey(functions.get(key))) { + FunctionRegistration target = new FunctionRegistration() + .target(functions.get(key)).names(getAliases(key)); + targets.put(target.getTarget(), key); + registrations.add(target); + } + } + // Wrap the functions so they handle reactive inputs and outputs + for (FunctionRegistration registration : registrations) { + @SuppressWarnings("unchecked") + FunctionRegistration target = (FunctionRegistration) registration; + String key = targets.get(target.getTarget()); + wrap(target, mapper, key); + } + return registrations; + } + + private Collection getAliases(String key) { + Collection names = new LinkedHashSet<>(); + String value = getQualifier(key); + if (value.equals(key)) { + names.add(key); + names.addAll(Arrays.asList(registry.getAliases(key))); + } + else { + names.add(value); + } + return names; + } + + private void wrap(FunctionRegistration registration, + ObjectMapper mapper, String key) { + Object target = registration.getTarget(); + if (target instanceof Supplier) { + registration.target(target((Supplier) target, mapper, key)); + } + else if (target instanceof Consumer) { + registration.target(target((Consumer) target, mapper, key)); + } + else if (target instanceof Function) { + registration.target(target((Function) target, mapper, key)); + } + } + + private String getQualifier(String key) { + if (!registry.containsBeanDefinition(key)) { + return key; + } + String value = key; + BeanDefinition beanDefinition = registry.getBeanDefinition(key); + Object source = beanDefinition.getSource(); + if (source instanceof StandardMethodMetadata) { + StandardMethodMetadata metadata = (StandardMethodMetadata) source; + Qualifier qualifier = AnnotatedElementUtils.findMergedAnnotation( + metadata.getIntrospectedMethod(), Qualifier.class); + if (qualifier != null && qualifier.value().length() > 0) { + return qualifier.value(); + } + } + return value; } private Supplier target(Supplier target, ObjectMapper mapper, String key) { @@ -125,19 +216,6 @@ public class ContextFunctionCatalogAutoConfiguration { } } - public Map> wrapFunctions(ObjectMapper mapper, - Map> functions) { - Map> result = new HashMap<>(); - for (String key : functions.keySet()) { - Function target = target(functions.get(key), mapper, key); - result.put(key, target); - for (String name : registry.getAliases(key)) { - result.put(name, target); - } - } - return result; - } - private Function target(Function target, ObjectMapper mapper, String key) { if (this.functions.contains(key)) { @@ -155,19 +233,6 @@ public class ContextFunctionCatalogAutoConfiguration { } } - public Map> wrapConsumers(ObjectMapper mapper, - Map> consumers) { - Map> result = new HashMap<>(); - for (String key : consumers.keySet()) { - Consumer target = target(consumers.get(key), mapper, key); - result.put(key, target); - for (String name : registry.getAliases(key)) { - result.put(name, target); - } - } - return result; - } - private Consumer target(Consumer target, ObjectMapper mapper, String key) { if (this.consumers.contains(key)) { @SuppressWarnings("unchecked") diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionEntry.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionEntry.java new file mode 100644 index 000000000..44acc3306 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionEntry.java @@ -0,0 +1,45 @@ +/* + * Copyright 2017 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 + * + * http://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; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cloud.function.registry.FunctionCatalog; +import org.springframework.core.annotation.AliasFor; + +/** + * A {@link Qualifier} annotation that specifies how to locate a function in the + * {@link FunctionCatalog} (instead of using the bean name). + * + * @author Dave Syer + */ +@Target({ ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, + ElementType.ANNOTATION_TYPE }) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@Documented +@Qualifier +public @interface FunctionEntry { + @AliasFor(annotation=Qualifier.class) + String value() default ""; +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java new file mode 100644 index 000000000..c83939ef0 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionRegistration.java @@ -0,0 +1,86 @@ +/* + * Copyright 2012-2015 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 + * + * http://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; + +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Dave Syer + * + */ +public class FunctionRegistration { + + private T target; + + private Set names = new LinkedHashSet<>(); + + private Map properties = new LinkedHashMap<>(); + + public FunctionRegistration() { + } + + public FunctionRegistration(T target) { + this.target = target; + } + + public Map getProperties() { + return properties; + } + + public Set getNames() { + return names; + } + + public void setNames(Set names) { + this.names = names; + } + + public T getTarget() { + return target; + } + + public FunctionRegistration properties(Map properties) { + this.properties.putAll(properties); + return this; + } + + public FunctionRegistration target(T target) { + this.target = target; + return this; + } + + public FunctionRegistration name(String name) { + this.names.add(name); + return this; + } + + public FunctionRegistration names(Collection names) { + this.names.addAll(names); + return this; + } + + public FunctionRegistration names(String... names) { + this.names.addAll(Arrays.asList(names)); + return this; + } + +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/InMemoryFunctionCatalog.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/InMemoryFunctionCatalog.java index fe30861e9..3c6382d1d 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/InMemoryFunctionCatalog.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/InMemoryFunctionCatalog.java @@ -16,7 +16,9 @@ package org.springframework.cloud.function.context; +import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -43,6 +45,29 @@ public class InMemoryFunctionCatalog implements FunctionCatalog { this.consumers = consumers; } + public InMemoryFunctionCatalog(Set> registrations) { + this.suppliers = new HashMap<>(); + this.functions = new HashMap<>(); + this.consumers = new HashMap<>(); + for (FunctionRegistration registration : registrations) { + if (registration.getTarget() instanceof Consumer) { + for (String name : registration.getNames()) { + consumers.put(name, (Consumer) registration.getTarget()); + } + } + if (registration.getTarget() instanceof Supplier) { + for (String name : registration.getNames()) { + suppliers.put(name, (Supplier) registration.getTarget()); + } + } + if (registration.getTarget() instanceof Function) { + for (String name : registration.getNames()) { + functions.put(name, (Function) registration.getTarget()); + } + } + } + } + @Override @SuppressWarnings("unchecked") public Supplier lookupSupplier(String name) { diff --git a/spring-cloud-function-context/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java b/spring-cloud-function-context/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java new file mode 100644 index 000000000..4dfac0519 --- /dev/null +++ b/spring-cloud-function-context/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2012-2015 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 + * + * http://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; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.junit.After; +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +import reactor.core.publisher.Flux; + +/** + * @author Dave Syer + * + */ +public class ContextFunctionCatalogAutoConfigurationTests { + + private ConfigurableApplicationContext context; + private InMemoryFunctionCatalog catalog; + + @After + public void close() { + if (context != null) { + context.close(); + } + } + + @Test + public void simpleFunction() { + create(SimpleConfiguration.class); + assertThat(context.getBean("function")).isInstanceOf(Function.class); + assertThat(catalog.lookupFunction("function")).isInstanceOf(Function.class); + } + + @Test + public void simpleSupplier() { + create(SimpleConfiguration.class); + assertThat(context.getBean("supplier")).isInstanceOf(Supplier.class); + Supplier> supplier = catalog.lookupSupplier("supplier"); + assertThat(supplier.get().blockFirst()).isEqualTo("hello"); + } + + @Test + public void simpleConsumer() { + create(SimpleConfiguration.class); + assertThat(context.getBean("consumer")).isInstanceOf(Consumer.class); + Consumer> consumer = catalog.lookupConsumer("consumer"); + consumer.accept(Flux.just("foo", "bar")); + assertThat(context.getBean(SimpleConfiguration.class).list).hasSize(2); + } + + @Test + public void qualifiedBean() { + create(QualifiedConfiguration.class); + assertThat(context.getBean("function")).isInstanceOf(Function.class); + assertThat(catalog.lookupFunction("function")).isNull(); + assertThat(catalog.lookupFunction("other")).isInstanceOf(Function.class); + } + + @Test + public void aliasBean() { + create(AliasConfiguration.class); + assertThat(context.getBean("function")).isInstanceOf(Function.class); + assertThat(catalog.lookupFunction("function")).isNotNull(); + assertThat(catalog.lookupFunction("other")).isInstanceOf(Function.class); + } + + @Test + public void registrationBean() { + create(RegistrationConfiguration.class); + assertThat(context.getBean("function")).isInstanceOf(Function.class); + assertThat(catalog.lookupFunction("function")).isNull(); + assertThat(catalog.lookupFunction("registration")).isNull(); + assertThat(catalog.lookupFunction("other")).isInstanceOf(Function.class); + } + + private void create(Class... types) { + context = new SpringApplicationBuilder((Object[]) types).run(); + catalog = context.getBean(InMemoryFunctionCatalog.class); + } + + @EnableAutoConfiguration + @Configuration + protected static class SimpleConfiguration { + private List list = new ArrayList<>(); + @Bean + public Function function() { + return value -> value.toUpperCase(); + } + @Bean + public Supplier supplier() { + return () -> "hello"; + } + @Bean + public Consumer consumer() { + return value -> list.add(value); + } + } + + @EnableAutoConfiguration + @Configuration + protected static class QualifiedConfiguration { + @Bean + @Qualifier("other") + public Function function() { + return value -> value.toUpperCase(); + } + } + + @EnableAutoConfiguration + @Configuration + protected static class AliasConfiguration { + @Bean({"function", "other"}) + public Function function() { + return value -> value.toUpperCase(); + } + } + + @EnableAutoConfiguration + @Configuration + protected static class RegistrationConfiguration { + @Bean + public FunctionRegistration> registration() { + return new FunctionRegistration>(function()).name("other"); + } + @Bean + public Function function() { + return value -> value.toUpperCase(); + } + } + +}