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.
This commit is contained in:
Dave Syer
2017-03-31 14:15:04 +01:00
committed by markfisher
parent 82e19894b2
commit d67159729d
6 changed files with 427 additions and 38 deletions

View File

@@ -24,6 +24,10 @@
<artifactId>spring-cloud-function-core</artifactId>
<version>${spring-cloud-function.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
@@ -33,5 +37,10 @@
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -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<String, Consumer<?>> consumers = Collections.emptyMap();
@Autowired(required = false)
private Map<String, FunctionRegistration<?>> 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<String, Supplier<?>> wrapSuppliers(ObjectMapper mapper,
Map<String, Supplier<?>> suppliers) {
Map<String, Supplier<?>> 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<FunctionRegistration<?>> merge(
Map<String, FunctionRegistration<?>> initial,
Map<String, Consumer<?>> consumers, Map<String, Supplier<?>> suppliers,
Map<String, Function<?, ?>> functions, ObjectMapper mapper) {
Set<FunctionRegistration<?>> registrations = new HashSet<>();
Map<Object, String> 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<Object> target = new FunctionRegistration<Object>()
.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<Object> target = new FunctionRegistration<Object>()
.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<Object> target = new FunctionRegistration<Object>()
.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<Object> target = (FunctionRegistration<Object>) registration;
String key = targets.get(target.getTarget());
wrap(target, mapper, key);
}
return registrations;
}
private Collection<String> getAliases(String key) {
Collection<String> 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<Object> 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<String, Function<?, ?>> wrapFunctions(ObjectMapper mapper,
Map<String, Function<?, ?>> functions) {
Map<String, Function<?, ?>> 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<String, Consumer<?>> wrapConsumers(ObjectMapper mapper,
Map<String, Consumer<?>> consumers) {
Map<String, Consumer<?>> 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")

View File

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

View File

@@ -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<T> {
private T target;
private Set<String> names = new LinkedHashSet<>();
private Map<String, String> properties = new LinkedHashMap<>();
public FunctionRegistration() {
}
public FunctionRegistration(T target) {
this.target = target;
}
public Map<String, String> getProperties() {
return properties;
}
public Set<String> getNames() {
return names;
}
public void setNames(Set<String> names) {
this.names = names;
}
public T getTarget() {
return target;
}
public FunctionRegistration<T> properties(Map<String, String> properties) {
this.properties.putAll(properties);
return this;
}
public FunctionRegistration<T> target(T target) {
this.target = target;
return this;
}
public FunctionRegistration<T> name(String name) {
this.names.add(name);
return this;
}
public FunctionRegistration<T> names(Collection<String> names) {
this.names.addAll(names);
return this;
}
public FunctionRegistration<T> names(String... names) {
this.names.addAll(Arrays.asList(names));
return this;
}
}

View File

@@ -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<FunctionRegistration<?>> 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 <T> Supplier<T> lookupSupplier(String name) {

View File

@@ -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<Flux<String>> 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<Flux<String>> 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<String> list = new ArrayList<>();
@Bean
public Function<String, String> function() {
return value -> value.toUpperCase();
}
@Bean
public Supplier<String> supplier() {
return () -> "hello";
}
@Bean
public Consumer<String> consumer() {
return value -> list.add(value);
}
}
@EnableAutoConfiguration
@Configuration
protected static class QualifiedConfiguration {
@Bean
@Qualifier("other")
public Function<String, String> function() {
return value -> value.toUpperCase();
}
}
@EnableAutoConfiguration
@Configuration
protected static class AliasConfiguration {
@Bean({"function", "other"})
public Function<String, String> function() {
return value -> value.toUpperCase();
}
}
@EnableAutoConfiguration
@Configuration
protected static class RegistrationConfiguration {
@Bean
public FunctionRegistration<Function<String, String>> registration() {
return new FunctionRegistration<Function<String, String>>(function()).name("other");
}
@Bean
public Function<String, String> function() {
return value -> value.toUpperCase();
}
}
}