From 87c67427dfa35306e0dd2aa78c97401563fc76e4 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Mon, 10 Sep 2018 10:03:05 +0200 Subject: [PATCH] GH-77 added initial Kotlin support Added mixed java/kotlin POM configuration added tests, javadocs Resolves #77 Resolves #204 --- spring-cloud-function-context/pom.xml | 67 ++ .../context/FunctionRegistration.java | 3 +- ...ntextFunctionCatalogAutoConfiguration.java | 929 ++++++++---------- .../context/config/FunctionContextUtils.java | 160 +++ ...tlinLambdaToFunctionAutoConfiguration.java | 157 +++ .../main/resources/META-INF/spring.factories | 3 +- ...FunctionCatalogAutoConfigurationTests.java | 31 + .../kotlin/KotlinLambdasConfiguration.kt | 44 + 8 files changed, 862 insertions(+), 532 deletions(-) create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/FunctionContextUtils.java create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java create mode 100644 spring-cloud-function-context/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt diff --git a/spring-cloud-function-context/pom.xml b/spring-cloud-function-context/pom.xml index 57d49f6f0..30e726bce 100644 --- a/spring-cloud-function-context/pom.xml +++ b/spring-cloud-function-context/pom.xml @@ -13,6 +13,9 @@ spring-cloud-function-parent 2.0.0.BUILD-SNAPSHOT + + 1.2.61 + @@ -42,6 +45,12 @@ jackson-databind true + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + true + org.springframework.boot spring-boot-starter-test @@ -53,4 +62,62 @@ test + + + + kotlin-maven-plugin + org.jetbrains.kotlin + ${kotlin.version} + + + compile + compile + + + ${project.basedir}/src/main/kotlin + ${project.basedir}/src/main/java + + + + + test-compile + test-compile + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + compile + + + java-test-compile + test-compile + testCompile + + + + + 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 index 8fe933c05..f342498be 100644 --- 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 @@ -51,7 +51,7 @@ public class FunctionRegistration { private FunctionType type; /** - * @deprecated as of v1.0.0 in favor of {@link #FunctionRegistration(Object, String, String...)} + * @deprecated as of v1.0.0 in favor of {@link #FunctionRegistration(Object, String...)} */ @Deprecated public FunctionRegistration(T target) { @@ -64,7 +64,6 @@ public class FunctionRegistration { * Creates instance of FunctionRegistration. * * @param target instance of {@link Supplier}, {@link Function} or {@link Consumer} - * @param name initial name for this registration. * @param names additional set of names for this registration. Additional names * can be provided {@link #name(String)} or {@link #names(String...)} operations. */ diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java index 90df6b145..fce90d17e 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java @@ -98,592 +98,463 @@ import org.springframework.util.StringUtils; @ConditionalOnMissingBean(FunctionCatalog.class) public class ContextFunctionCatalogAutoConfiguration { - static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper"; + static final String PREFERRED_MAPPER_PROPERTY = "spring.http.converters.preferred-json-mapper"; - @Autowired(required = false) - private Map> suppliers = Collections.emptyMap(); + @Autowired(required = false) + private Map> suppliers = Collections.emptyMap(); - @Autowired(required = false) - private Map> functions = Collections.emptyMap(); + @Autowired(required = false) + private Map> functions = Collections.emptyMap(); - @Autowired(required = false) - private Map> consumers = Collections.emptyMap(); + @Autowired(required = false) + private Map> consumers = Collections.emptyMap(); - @Autowired(required = false) - private Map> registrations = Collections.emptyMap(); + @Autowired(required = false) + private Map> registrations = Collections.emptyMap(); - @Bean - public FunctionRegistry functionCatalog(ContextFunctionRegistry processor) { - processor.merge(registrations, consumers, suppliers, functions); - return new BeanFactoryFunctionCatalog(processor); - } + @Bean + public FunctionRegistry functionCatalog(ContextFunctionRegistry processor) { + processor.merge(registrations, consumers, suppliers, functions); + return new BeanFactoryFunctionCatalog(processor); + } - @Bean - public FunctionInspector functionInspector(ContextFunctionRegistry processor) { - return new BeanFactoryFunctionInspector(processor); - } + @Bean + public FunctionInspector functionInspector(ContextFunctionRegistry processor) { + return new BeanFactoryFunctionInspector(processor); + } - protected static class BeanFactoryFunctionCatalog implements FunctionRegistry { + protected static class BeanFactoryFunctionCatalog implements FunctionRegistry { - private final ContextFunctionRegistry processor; + private final ContextFunctionRegistry processor; - @Override - public void register(FunctionRegistration registration) { - Assert.notEmpty(registration.getNames(), "'registration' must contain at least one name before it is registered in catalog."); - processor.register(registration); - } + @Override + public void register(FunctionRegistration registration) { + Assert.notEmpty(registration.getNames(), "'registration' must contain at least one name before it is registered in catalog."); + processor.register(registration); + } - @Override - @SuppressWarnings("unchecked") - public T lookup(Class type, String name) { - T function = null; - if (type == null) { - function = (T) processor.lookupFunction(name); - if (function == null) { - function = (T) processor.lookupConsumer(name); - } - if (function == null) { - function = (T) processor.lookupSupplier(name); - } - } - else if (Supplier.class.isAssignableFrom(type)) { - function = (T) processor.lookupSupplier(name); - } - else if (Consumer.class.isAssignableFrom(type)) { - function = (T) processor.lookupConsumer(name); - } - else if (Function.class.isAssignableFrom(type)) { - function = (T) processor.lookupFunction(name); - } - return function; - } + @Override + @SuppressWarnings("unchecked") + public T lookup(Class type, String name) { + T function = null; + if (type == null) { + function = (T) processor.lookupFunction(name); + if (function == null) { + function = (T) processor.lookupConsumer(name); + } + if (function == null) { + function = (T) processor.lookupSupplier(name); + } + } else if (Supplier.class.isAssignableFrom(type)) { + function = (T) processor.lookupSupplier(name); + } else if (Consumer.class.isAssignableFrom(type)) { + function = (T) processor.lookupConsumer(name); + } else if (Function.class.isAssignableFrom(type)) { + function = (T) processor.lookupFunction(name); + } + return function; + } - @Override - public Set getNames(Class type) { - if (Supplier.class.isAssignableFrom(type)) { - return this.processor.getSuppliers(); - } - if (Consumer.class.isAssignableFrom(type)) { - return this.processor.getConsumers(); - } - if (Function.class.isAssignableFrom(type)) { - return this.processor.getFunctions(); - } - return Collections.emptySet(); - } + @Override + public Set getNames(Class type) { + if (Supplier.class.isAssignableFrom(type)) { + return this.processor.getSuppliers(); + } + if (Consumer.class.isAssignableFrom(type)) { + return this.processor.getConsumers(); + } + if (Function.class.isAssignableFrom(type)) { + return this.processor.getFunctions(); + } + return Collections.emptySet(); + } - public BeanFactoryFunctionCatalog(ContextFunctionRegistry processor) { - this.processor = processor; - } + public BeanFactoryFunctionCatalog(ContextFunctionRegistry processor) { + this.processor = processor; + } - } + } - protected class BeanFactoryFunctionInspector implements FunctionInspector { + protected class BeanFactoryFunctionInspector implements FunctionInspector { - private ContextFunctionRegistry processor; + private ContextFunctionRegistry processor; - public BeanFactoryFunctionInspector(ContextFunctionRegistry processor) { - this.processor = processor; - } + public BeanFactoryFunctionInspector(ContextFunctionRegistry processor) { + this.processor = processor; + } - @Override - public FunctionRegistration getRegistration(Object function) { - FunctionRegistration registration = processor.getRegistration(function); - return registration; - } + @Override + public FunctionRegistration getRegistration(Object function) { + FunctionRegistration registration = processor.getRegistration(function); + return registration; + } - } + } - @Configuration - @ConditionalOnClass(Gson.class) - @ConditionalOnBean(Gson.class) - @Conditional(PreferGsonOrMissingJacksonCondition.class) - protected static class GsonConfiguration { - @Bean - public GsonMapper jsonMapper(Gson gson) { - return new GsonMapper(gson); - } - } + @Configuration + @ConditionalOnClass(Gson.class) + @ConditionalOnBean(Gson.class) + @Conditional(PreferGsonOrMissingJacksonCondition.class) + protected static class GsonConfiguration { + @Bean + public GsonMapper jsonMapper(Gson gson) { + return new GsonMapper(gson); + } + } - @Configuration - @ConditionalOnClass(ObjectMapper.class) - @ConditionalOnBean(ObjectMapper.class) - @ConditionalOnProperty(name = ContextFunctionCatalogAutoConfiguration.PREFERRED_MAPPER_PROPERTY, havingValue = "jackson", matchIfMissing = true) - protected static class JacksonConfiguration { - @Bean - public JacksonMapper jsonMapper(ObjectMapper mapper) { - return new JacksonMapper(mapper); - } - } + @Configuration + @ConditionalOnClass(ObjectMapper.class) + @ConditionalOnBean(ObjectMapper.class) + @ConditionalOnProperty(name = ContextFunctionCatalogAutoConfiguration.PREFERRED_MAPPER_PROPERTY, havingValue = "jackson", matchIfMissing = true) + protected static class JacksonConfiguration { + @Bean + public JacksonMapper jsonMapper(ObjectMapper mapper) { + return new JacksonMapper(mapper); + } + } - @Component - protected static class ContextFunctionRegistry { + @Component + protected static class ContextFunctionRegistry { - private Map suppliers = new ConcurrentHashMap<>(); + private Map suppliers = new ConcurrentHashMap<>(); - private Map functions = new ConcurrentHashMap<>(); + private Map functions = new ConcurrentHashMap<>(); - private Map consumers = new ConcurrentHashMap<>(); + private Map consumers = new ConcurrentHashMap<>(); - @Autowired(required = false) - private ApplicationEventPublisher publisher; + @Autowired(required = false) + private ApplicationEventPublisher publisher; - @Autowired - private ConfigurableListableBeanFactory registry; + @Autowired + private ConfigurableListableBeanFactory registry; - private Map names = new ConcurrentHashMap<>(); + private Map names = new ConcurrentHashMap<>(); - private Map types = new ConcurrentHashMap<>(); + private Map types = new ConcurrentHashMap<>(); - public Set getSuppliers() { - return this.suppliers.keySet(); - } + public Set getSuppliers() { + return this.suppliers.keySet(); + } - public FunctionRegistration getRegistration(Object function) { - if (function == null || !names.containsKey(function)) { - return null; - } - return new FunctionRegistration<>(function, names.get(function)) - .type(findType(function).getType()); - } + public FunctionRegistration getRegistration(Object function) { + if (function == null || !names.containsKey(function)) { + return null; + } + return new FunctionRegistration<>(function, names.get(function)) + .type(findType(function).getType()); + } - public Set getConsumers() { - return this.consumers.keySet(); - } + public Set getConsumers() { + return this.consumers.keySet(); + } - public Set getFunctions() { - return this.functions.keySet(); - } + public Set getFunctions() { + return this.functions.keySet(); + } - public Supplier lookupSupplier(String name) { - Object composed = compose(name, this.suppliers, false); - if (composed instanceof Supplier) { - return (Supplier) composed; - } - return null; - } + public Supplier lookupSupplier(String name) { + Object composed = compose(name, this.suppliers, false); + if (composed instanceof Supplier) { + return (Supplier) composed; + } + return null; + } - public Consumer lookupConsumer(String name) { - Object composed = compose(name, this.consumers, true); - if (composed instanceof Consumer) { - return (Consumer) composed; - } - return null; - } + public Consumer lookupConsumer(String name) { + Object composed = compose(name, this.consumers, true); + if (composed instanceof Consumer) { + return (Consumer) composed; + } + return null; + } - public Function lookupFunction(String name) { - Object composed = compose(name, this.functions, true); - if (composed instanceof Function) { - return (Function) composed; - } - return null; - } + public Function lookupFunction(String name) { + Object composed = compose(name, this.functions, true); + if (composed instanceof Function) { + return (Function) composed; + } + return null; + } - private Object compose(String name, Map lookup, - boolean hasInput) { - name = name.replaceAll(",", "|"); - if (lookup.containsKey(name)) { - return lookup.get(name); - } - String[] stages = StringUtils.delimitedListToStringArray(name, "|"); - Map source = !hasInput || stages.length <= 1 ? lookup - : this.functions; - if (stages.length == 0 && source.size() == 1) { - stages = new String[] { source.keySet().iterator().next() }; - } - Object function = stages.length > 0 ? lookup(stages[0], source) : null; - if (function == null) { - return null; - } - Object other = null; - for (int i = 1; i < stages.length - 1; i++) { - other = lookup(stages[i], this.functions); - if (other == null) { - return null; - } - function = compose(function, other); - } - if (stages.length > 1) { - other = lookup(stages[stages.length - 1], - hasInput ? lookup : this.functions); - if (other == null) { - return null; - } - function = compose(function, other); - } - final Object value = function; - lookup.computeIfAbsent(name, key -> value); - if (!types.containsKey(name)) { - if (types.containsKey(stages[0]) - && types.containsKey(stages[stages.length - 1])) { - FunctionType input = types.get(stages[0]); - FunctionType output = types.get(stages[stages.length - 1]); - types.put(name, FunctionType.compose(input, output)); - } - } - names.put(function, name); - return function; - } + private Object compose(String name, Map lookup, + boolean hasInput) { + name = name.replaceAll(",", "|"); + if (lookup.containsKey(name)) { + return lookup.get(name); + } + String[] stages = StringUtils.delimitedListToStringArray(name, "|"); + Map source = !hasInput || stages.length <= 1 ? lookup + : this.functions; + if (stages.length == 0 && source.size() == 1) { + stages = new String[]{source.keySet().iterator().next()}; + } + Object function = stages.length > 0 ? lookup(stages[0], source) : null; + if (function == null) { + return null; + } + Object other = null; + for (int i = 1; i < stages.length - 1; i++) { + other = lookup(stages[i], this.functions); + if (other == null) { + return null; + } + function = compose(function, other); + } + if (stages.length > 1) { + other = lookup(stages[stages.length - 1], + hasInput ? lookup : this.functions); + if (other == null) { + return null; + } + function = compose(function, other); + } + final Object value = function; + lookup.computeIfAbsent(name, key -> value); + if (!types.containsKey(name)) { + if (types.containsKey(stages[0]) + && types.containsKey(stages[stages.length - 1])) { + FunctionType input = types.get(stages[0]); + FunctionType output = types.get(stages[stages.length - 1]); + types.put(name, FunctionType.compose(input, output)); + } + } + names.put(function, name); + return function; + } - private Object lookup(String name, Map lookup) { - Object result = lookup.get(name); - if (result != null) { - findType(result); - } - return result; - } + private Object lookup(String name, Map lookup) { + Object result = lookup.get(name); + if (result != null) { + findType(result); + } + return result; + } - @SuppressWarnings("unchecked") - private Object compose(Object a, Object b) { - if (a instanceof Supplier && b instanceof Function) { - Supplier supplier = (Supplier) a; - Function function = (Function) b; - return (Supplier) () -> function.apply(supplier.get()); - } - else if (a instanceof Function && b instanceof Function) { - Function function1 = (Function) a; - Function function2 = (Function) b; - return function1.andThen(function2); - } - else if (a instanceof Function && b instanceof Consumer) { - Function function = (Function) a; - Consumer consumer = (Consumer) b; - return (Consumer) v -> consumer.accept(function.apply(v)); - } - else { - throw new IllegalArgumentException(String.format( - "Could not compose %s and %s", a.getClass(), b.getClass())); - } - } + @SuppressWarnings("unchecked") + private Object compose(Object a, Object b) { + if (a instanceof Supplier && b instanceof Function) { + Supplier supplier = (Supplier) a; + Function function = (Function) b; + return (Supplier) () -> function.apply(supplier.get()); + } else if (a instanceof Function && b instanceof Function) { + Function function1 = (Function) a; + Function function2 = (Function) b; + return function1.andThen(function2); + } else if (a instanceof Function && b instanceof Consumer) { + Function function = (Function) a; + Consumer consumer = (Consumer) b; + return (Consumer) v -> consumer.accept(function.apply(v)); + } else { + throw new IllegalArgumentException(String.format( + "Could not compose %s and %s", a.getClass(), b.getClass())); + } + } - @SuppressWarnings("unchecked") - public void register(FunctionRegistration function) { - wrap((FunctionRegistration) function, - function.getNames().iterator().next()); - } + @SuppressWarnings("unchecked") + public void register(FunctionRegistration function) { + wrap((FunctionRegistration) function, + function.getNames().iterator().next()); + } - @PreDestroy - public void close() { - if (publisher != null) { - if (!functions.isEmpty()) { - publisher.publishEvent(new FunctionUnregistrationEvent(this, - Function.class, functions.keySet())); - } - if (!consumers.isEmpty()) { - publisher.publishEvent(new FunctionUnregistrationEvent(this, - Consumer.class, consumers.keySet())); - } - if (!suppliers.isEmpty()) { - publisher.publishEvent(new FunctionUnregistrationEvent(this, - Supplier.class, suppliers.keySet())); - } - } - } + @PreDestroy + public void close() { + if (publisher != null) { + if (!functions.isEmpty()) { + publisher.publishEvent(new FunctionUnregistrationEvent(this, + Function.class, functions.keySet())); + } + if (!consumers.isEmpty()) { + publisher.publishEvent(new FunctionUnregistrationEvent(this, + Consumer.class, consumers.keySet())); + } + if (!suppliers.isEmpty()) { + publisher.publishEvent(new FunctionUnregistrationEvent(this, + Supplier.class, suppliers.keySet())); + } + } + } - public Set> merge( - Map> initial, - Map> consumers, Map> suppliers, - Map> functions) { - 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); - } + public Set> merge( + Map> initial, + Map> consumers, Map> suppliers, + Map> functions) { + 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); + } - Stream.concat(consumers.entrySet().stream(), Stream.concat(suppliers.entrySet().stream(), functions.entrySet().stream())) - .forEach(entry -> { - if (!targets.containsKey(entry.getValue())) { - FunctionRegistration target = new FunctionRegistration( - entry.getValue(), getAliases(entry.getKey()).toArray(new String[] {})); - targets.put(target.getTarget(), entry.getKey()); - registrations.add(target); - } - }); - // Wrap the functions so they handle reactive inputs and outputs - registrations.forEach(registration -> wrap(registration, targets.get(registration.getTarget()))); - return registrations; - } + Stream.concat(consumers.entrySet().stream(), Stream.concat(suppliers.entrySet().stream(), functions.entrySet().stream())) + .forEach(entry -> { + if (!targets.containsKey(entry.getValue())) { + FunctionRegistration target = new FunctionRegistration( + entry.getValue(), getAliases(entry.getKey()).toArray(new String[]{})); + targets.put(target.getTarget(), entry.getKey()); + registrations.add(target); + } + }); + // Wrap the functions so they handle reactive inputs and outputs + registrations.forEach(registration -> wrap(registration, targets.get(registration.getTarget()))); + return registrations; + } - private Collection getAliases(String key) { - Collection names = new LinkedHashSet<>(); - String value = getQualifier(key); - if (value.equals(key) && registry != null) { - names.addAll(Arrays.asList(registry.getAliases(key))); - } - names.add(value); - return names; - } + private Collection getAliases(String key) { + Collection names = new LinkedHashSet<>(); + String value = getQualifier(key); + if (value.equals(key) && registry != null) { + names.addAll(Arrays.asList(registry.getAliases(key))); + } + names.add(value); + return names; + } - private void wrap(FunctionRegistration registration, String key) { - Object target = registration.getTarget(); - this.names.put(target, key); - if (registration.getType() != null) { - this.types.put(key, registration.getType()); - } - else { - registration.type(findType(target).getType()); - } - Class type; - registration = transform(registration); - target = registration.getTarget(); - if (target instanceof Supplier) { - type = Supplier.class; - for (String name : registration.getNames()) { - this.suppliers.put(name, registration.getTarget()); - } - } - else if (target instanceof Consumer) { - type = Consumer.class; - for (String name : registration.getNames()) { - this.consumers.put(name, registration.getTarget()); - } - } - else if (target instanceof Function) { - type = Function.class; - for (String name : registration.getNames()) { - this.functions.put(name, registration.getTarget()); - } - } - else { - return; - } - // this.names.remove(target); - this.names.put(registration.getTarget(), key); - if (publisher != null) { - publisher.publishEvent(new FunctionRegistrationEvent( - registration.getTarget(), type, registration.getNames())); - } - } + private void wrap(FunctionRegistration registration, String key) { + Object target = registration.getTarget(); + this.names.put(target, key); + if (registration.getType() != null) { + this.types.put(key, registration.getType()); + } else { + registration.type(findType(target).getType()); + } + Class type; + registration = transform(registration); + target = registration.getTarget(); + if (target instanceof Supplier) { + type = Supplier.class; + for (String name : registration.getNames()) { + this.suppliers.put(name, registration.getTarget()); + } + } else if (target instanceof Consumer) { + type = Consumer.class; + for (String name : registration.getNames()) { + this.consumers.put(name, registration.getTarget()); + } + } else if (target instanceof Function) { + type = Function.class; + for (String name : registration.getNames()) { + this.functions.put(name, registration.getTarget()); + } + } else { + return; + } + // this.names.remove(target); + this.names.put(registration.getTarget(), key); + if (publisher != null) { + publisher.publishEvent(new FunctionRegistrationEvent( + registration.getTarget(), type, registration.getNames())); + } + } - private FunctionRegistration transform(FunctionRegistration registration) { - return fluxify(isolated(registration)); - } + private FunctionRegistration transform(FunctionRegistration registration) { + return fluxify(isolated(registration)); + } - @SuppressWarnings({ "rawtypes", "unchecked" }) - private FunctionRegistration fluxify(FunctionRegistration input) { - FunctionRegistration registration = (FunctionRegistration) input; - Object target = registration.getTarget(); - FunctionType type = registration.getType(); - boolean flux = hasFluxTypes(type); - if (!flux) { - if (target instanceof Supplier) { - target = new FluxSupplier((Supplier) target); - } - else if (target instanceof Function) { - target = new FluxFunction((Function) target); - } - else if (target instanceof Consumer) { - target = new FluxConsumer((Consumer) target); - } - registration.target(target); - } - return registration; - } + @SuppressWarnings({"rawtypes", "unchecked"}) + private FunctionRegistration fluxify(FunctionRegistration input) { + FunctionRegistration registration = (FunctionRegistration) input; + Object target = registration.getTarget(); + FunctionType type = registration.getType(); + boolean flux = hasFluxTypes(type); + if (!flux) { + if (target instanceof Supplier) { + target = new FluxSupplier((Supplier) target); + } else if (target instanceof Function) { + target = new FluxFunction((Function) target); + } else if (target instanceof Consumer) { + target = new FluxConsumer((Consumer) target); + } + registration.target(target); + } + return registration; + } - private boolean hasFluxTypes(FunctionType type) { - return type.isWrapper(); - } + private boolean hasFluxTypes(FunctionType type) { + return type.isWrapper(); + } - @SuppressWarnings({ "rawtypes", "unchecked" }) - private FunctionRegistration isolated(FunctionRegistration input) { - FunctionRegistration registration = (FunctionRegistration) input; - Object target = registration.getTarget(); - boolean isolated = getClass().getClassLoader() != target.getClass() - .getClassLoader(); - if (target instanceof Supplier) { - if (isolated) { - target = new IsolatedSupplier((Supplier) target); - } - } - else if (target instanceof Function) { - if (isolated) { - target = new IsolatedFunction((Function) target); - } - } - else if (target instanceof Consumer) { - if (isolated) { - target = new IsolatedConsumer((Consumer) target); - } - } - registration.target(target); - return registration; - } + @SuppressWarnings({"rawtypes", "unchecked"}) + private FunctionRegistration isolated(FunctionRegistration input) { + FunctionRegistration registration = (FunctionRegistration) input; + Object target = registration.getTarget(); + boolean isolated = getClass().getClassLoader() != target.getClass() + .getClassLoader(); + if (target instanceof Supplier) { + if (isolated) { + target = new IsolatedSupplier((Supplier) target); + } + } else if (target instanceof Function) { + if (isolated) { + target = new IsolatedFunction((Function) target); + } + } else if (target instanceof Consumer) { + if (isolated) { + target = new IsolatedConsumer((Consumer) target); + } + } + registration.target(target); + return registration; + } - private String getQualifier(String key) { - if (registry != null && registry.containsBeanDefinition(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 key; - } + private String getQualifier(String key) { + if (registry != null && registry.containsBeanDefinition(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 key; + } - private FunctionType findType(String name, AbstractBeanDefinition definition) { - Object source = definition.getSource(); - FunctionType param = null; - // Start by assuming output -> Function - if (source instanceof StandardMethodMetadata) { - // Standard @Bean metadata - Type beanType = ((StandardMethodMetadata) source).getIntrospectedMethod() - .getGenericReturnType(); - if (beanType instanceof ParameterizedType) { - ParameterizedType type = (ParameterizedType) beanType; - param = new FunctionType(type); - } - else { - param = new FunctionType(beanType); - } - } - else if (source instanceof MethodMetadataReadingVisitor) { - // A component scan with @Beans - MethodMetadataReadingVisitor visitor = (MethodMetadataReadingVisitor) source; - Type type = findBeanType(definition, visitor); - param = new FunctionType(type); - } - else if (source instanceof Resource) { - Class beanType = this.registry.getType(name); - param = new FunctionType(beanType); - } - else { - ResolvableType resolvable = (ResolvableType) getField(definition, - "targetType"); - if (resolvable != null) { - param = new FunctionType(resolvable.getType()); - } - else { - Class beanClass = definition.getBeanClass(); - if (beanClass != null && !FunctionFactoryMetadata.class - .isAssignableFrom(beanClass)) { - Type type = beanClass; - param = new FunctionType(type); - } - else { - Object bean = this.registry.getBean(name); - if (bean instanceof FunctionFactoryMetadata) { - FunctionFactoryMetadata factory = (FunctionFactoryMetadata) bean; - Type type = factory.getFactoryMethod().getGenericReturnType(); - param = new FunctionType(type); - } - else { - param = new FunctionType(bean.getClass()); - } - } - } - } - return param; - } + private FunctionType findType(Object function) { + String name = names.get(function); + if (types.containsKey(name)) { + return types.get(name); + } + FunctionType param; + if (name == null || registry == null + || !registry.containsBeanDefinition(name)) { + if (function != null) { + param = new FunctionType(function.getClass()); + } else { + param = FunctionType.UNCLASSIFIED; + } + } else { + param = FunctionContextUtils.findType(name, registry); + } + types.computeIfAbsent(name, str -> param); + return param; + } - private Type findBeanType(AbstractBeanDefinition definition, - MethodMetadataReadingVisitor visitor) { - Class factory = ClassUtils - .resolveClassName(visitor.getDeclaringClassName(), null); - Class[] params = getParamTypes(factory, definition); - Method method = ReflectionUtils.findMethod(factory, visitor.getMethodName(), - params); - Type type = method.getGenericReturnType(); - return type; - } + } - private Method[] getCandidateMethods(final Class factoryClass, - final RootBeanDefinition mbd) { - if (System.getSecurityManager() != null) { - return AccessController.doPrivileged(new PrivilegedAction() { - @Override - public Method[] run() { - return (mbd.isNonPublicAccessAllowed() - ? ReflectionUtils.getAllDeclaredMethods(factoryClass) - : factoryClass.getMethods()); - } - }); - } - else { - return (mbd.isNonPublicAccessAllowed() - ? ReflectionUtils.getAllDeclaredMethods(factoryClass) - : factoryClass.getMethods()); - } - } + private static class PreferGsonOrMissingJacksonCondition extends AnyNestedCondition { - private Class[] getParamTypes(Class factory, - AbstractBeanDefinition definition) { - if (definition instanceof RootBeanDefinition) { - RootBeanDefinition root = (RootBeanDefinition) definition; - for (Method method : getCandidateMethods(factory, root)) { - if (root.isFactoryMethod(method)) { - return method.getParameterTypes(); - } - } - } - List> params = new ArrayList<>(); - for (ValueHolder holder : definition.getConstructorArgumentValues() - .getIndexedArgumentValues().values()) { - params.add(ClassUtils.resolveClassName(holder.getType(), null)); - } - return params.toArray(new Class[0]); - } + PreferGsonOrMissingJacksonCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } - private Object getField(Object target, String name) { - Field field = ReflectionUtils.findField(target.getClass(), name); - if (field == null) { - return null; - } - ReflectionUtils.makeAccessible(field); - return ReflectionUtils.getField(field, target); - } + @ConditionalOnProperty(name = ContextFunctionCatalogAutoConfiguration.PREFERRED_MAPPER_PROPERTY, havingValue = "gson", matchIfMissing = false) + static class GsonPreferred { - private FunctionType findType(Object function) { - String name = names.get(function); - if (types.containsKey(name)) { - return types.get(name); - } - FunctionType param; - if (name == null || registry == null - || !registry.containsBeanDefinition(name)) { - if (function != null) { - param = new FunctionType(function.getClass()); - } - else { - param = FunctionType.UNCLASSIFIED; - } - } - else { - param = findType(name, - (AbstractBeanDefinition) registry.getBeanDefinition(name)); - } - types.computeIfAbsent(name, str -> param); - return param; - } + } - } + @ConditionalOnMissingBean(ObjectMapper.class) + static class JacksonMissing { - private static class PreferGsonOrMissingJacksonCondition extends AnyNestedCondition { + } - PreferGsonOrMissingJacksonCondition() { - super(ConfigurationPhase.REGISTER_BEAN); - } - - @ConditionalOnProperty(name = ContextFunctionCatalogAutoConfiguration.PREFERRED_MAPPER_PROPERTY, havingValue = "gson", matchIfMissing = false) - static class GsonPreferred { - - } - - @ConditionalOnMissingBean(ObjectMapper.class) - static class JacksonMissing { - - } - - } + } } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/FunctionContextUtils.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/FunctionContextUtils.java new file mode 100644 index 000000000..4abd2b4e1 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/FunctionContextUtils.java @@ -0,0 +1,160 @@ +/* + * Copyright 2018 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.config; + +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.cloud.function.context.FunctionType; +import org.springframework.cloud.function.core.FunctionFactoryMetadata; +import org.springframework.core.ResolvableType; +import org.springframework.core.io.Resource; +import org.springframework.core.type.StandardMethodMetadata; +import org.springframework.core.type.classreading.MethodMetadataReadingVisitor; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Oleg Zhurakousky + * + * @since 2.0 + */ +abstract class FunctionContextUtils { + + public static FunctionType findType(String name, ConfigurableListableBeanFactory registry) { + AbstractBeanDefinition definition = (AbstractBeanDefinition) registry.getBeanDefinition(name); + Object source = definition.getSource(); + FunctionType param = null; + // Start by assuming output -> Function + if (source instanceof StandardMethodMetadata) { + // Standard @Bean metadata + Type beanType = ((StandardMethodMetadata) source).getIntrospectedMethod() + .getGenericReturnType(); + if (beanType instanceof ParameterizedType) { + ParameterizedType type = (ParameterizedType) beanType; + param = new FunctionType(type); + } + else { + param = new FunctionType(beanType); + } + } + else if (source instanceof MethodMetadataReadingVisitor) { + // A component scan with @Beans + MethodMetadataReadingVisitor visitor = (MethodMetadataReadingVisitor) source; + Type type = findBeanType(definition, visitor); + param = new FunctionType(type); + } + else if (source instanceof Resource) { + Class beanType = registry.getType(name); + param = new FunctionType(beanType); + } + else { + ResolvableType resolvable = (ResolvableType) getField(definition, + "targetType"); + if (resolvable != null) { + param = new FunctionType(resolvable.getType()); + } + else { + Class beanClass = definition.getBeanClass(); + if (beanClass != null && !FunctionFactoryMetadata.class + .isAssignableFrom(beanClass)) { + Type type = beanClass; + param = new FunctionType(type); + } + else { + Object bean = registry.getBean(name); + if (bean instanceof FunctionFactoryMetadata) { + FunctionFactoryMetadata factory = (FunctionFactoryMetadata) bean; + Type type = factory.getFactoryMethod().getGenericReturnType(); + param = new FunctionType(type); + } + else { + param = new FunctionType(bean.getClass()); + } + } + } + } + return param; + } + + private static Type findBeanType(AbstractBeanDefinition definition, + MethodMetadataReadingVisitor visitor) { + Class factory = ClassUtils + .resolveClassName(visitor.getDeclaringClassName(), null); + Class[] params = getParamTypes(factory, definition); + Method method = ReflectionUtils.findMethod(factory, visitor.getMethodName(), + params); + Type type = method.getGenericReturnType(); + return type; + } + + private static Class[] getParamTypes(Class factory, + AbstractBeanDefinition definition) { + if (definition instanceof RootBeanDefinition) { + RootBeanDefinition root = (RootBeanDefinition) definition; + for (Method method : getCandidateMethods(factory, root)) { + if (root.isFactoryMethod(method)) { + return method.getParameterTypes(); + } + } + } + List> params = new ArrayList<>(); + for (ConstructorArgumentValues.ValueHolder holder : definition.getConstructorArgumentValues() + .getIndexedArgumentValues().values()) { + params.add(ClassUtils.resolveClassName(holder.getType(), null)); + } + return params.toArray(new Class[0]); + } + + private static Method[] getCandidateMethods(final Class factoryClass, + final RootBeanDefinition mbd) { + if (System.getSecurityManager() != null) { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Method[] run() { + return (mbd.isNonPublicAccessAllowed() + ? ReflectionUtils.getAllDeclaredMethods(factoryClass) + : factoryClass.getMethods()); + } + }); + } + else { + return (mbd.isNonPublicAccessAllowed() + ? ReflectionUtils.getAllDeclaredMethods(factoryClass) + : factoryClass.getMethods()); + } + } + + private static Object getField(Object target, String name) { + Field field = ReflectionUtils.findField(target.getClass(), name); + if (field == null) { + return null; + } + ReflectionUtils.makeAccessible(field); + return ReflectionUtils.getField(field, target); + } +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java new file mode 100644 index 000000000..5c3008377 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java @@ -0,0 +1,157 @@ +/* + * Copyright 2018 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.config; + +import kotlin.Unit; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.cloud.function.context.FunctionType; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Configuration class which defines the required infrastructure to + * bootstrap Kotlin lambdas as invocable functions within the context of the framework. + * + * @author Oleg Zhurakousky + * + * @since 2.0 + */ +@Configuration +@ConditionalOnClass(name="kotlin.jvm.functions.Function1") +class KotlinLambdaToFunctionAutoConfiguration implements BeanFactoryAware { + + protected final Log logger = LogFactory.getLog(getClass()); + + private ConfigurableListableBeanFactory beanFactory; + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } + + /** + * Will transform all discovered Kotlin's Function1 and Function0 lambdas to + * java Supplier, Function and Consumer, retaining the original Kotlin type characteristics. + * In other words the resulting bean coudl be cast to both java and kotlin + * types (i.e., java Function vs. kotlin Function1) + */ + @Bean + public BeanPostProcessor kotlinPostProcessor() { + return new BeanPostProcessor() { + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof Function1) { + FunctionType functionType = FunctionContextUtils.findType(beanName, beanFactory); + if (Unit.class.isAssignableFrom(functionType.getOutputType())) { + logger.debug("Transforming Kotlin lambda " + beanName + " to java Consumer"); + bean = new KotlinConsumer((Function1) bean); + } + else { + logger.debug("Transforming Kotlin lambda " + beanName + " to java Function"); + bean = new KotlinFunction((Function1) bean); + } + } + else if (bean instanceof Function0) { + logger.debug("Transforming Kotlin lambda " + beanName + " to java Supplier"); + bean = new KotlinSupplier((Function0) bean); + } + return bean; + } + }; + } + + + /** + * Wrapper for Kotlin lambda to be represented as both Java Function as well as Kotlin's Function1 + */ + private static class KotlinFunction implements Function, Function1 { + + private final Function1 kotlinLambda; + + KotlinFunction(Function1 kotlinLambda) { + this.kotlinLambda = kotlinLambda; + } + + @Override + public O apply(I i) { + return this.kotlinLambda.invoke(i); + } + + @Override + public O invoke(I i) { + return this.apply(i); + } + } + + /** + * Wrapper for Kotlin lambda to be represented as both Java Consumer as well as Kotlin's Function1 + */ + private static class KotlinConsumer implements Consumer, Function1 { + + private final Function1 kotlinLambda; + + KotlinConsumer(Function1 kotlinLambda) { + this.kotlinLambda = kotlinLambda; + } + + @Override + public Unit invoke(I i) { + return this.kotlinLambda.invoke(i); + } + + @Override + public void accept(I i) { + this.kotlinLambda.invoke(i); + } + } + + /** + * Wrapper for Kotlin lambda to be represented as both Java Supplier as well as Kotlin's Function0 + */ + private static class KotlinSupplier implements Supplier, Function0 { + + private final Function0 kotlinLambda; + + KotlinSupplier(Function0 kotlinLambda) { + this.kotlinLambda = kotlinLambda; + } + + @Override + public O get() { + return this.invoke(); + } + + @Override + public O invoke() { + return this.kotlinLambda.invoke(); + } + } + +} diff --git a/spring-cloud-function-context/src/main/resources/META-INF/spring.factories b/spring-cloud-function-context/src/main/resources/META-INF/spring.factories index aa9a9c745..4629fb31e 100644 --- a/spring-cloud-function-context/src/main/resources/META-INF/spring.factories +++ b/spring-cloud-function-context/src/main/resources/META-INF/spring.factories @@ -1,5 +1,6 @@ org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ -org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration +org.springframework.cloud.function.context.config.ContextFunctionCatalogAutoConfiguration,\ +org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration org.springframework.cloud.function.context.WrapperDetector=\ org.springframework.cloud.function.context.config.FluxWrapperDetector diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationTests.java index 109b14efb..c4ae9fc65 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfigurationTests.java @@ -27,6 +27,8 @@ import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; +import kotlin.jvm.functions.Function0; +import kotlin.jvm.functions.Function1; import org.junit.After; import org.junit.Test; import org.reactivestreams.Publisher; @@ -48,6 +50,7 @@ import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionScan; import org.springframework.cloud.function.context.catalog.FunctionInspector; import org.springframework.cloud.function.inject.FooConfiguration; +import org.springframework.cloud.function.kotlin.KotlinLambdasConfiguration; import org.springframework.cloud.function.scan.ScannedFunction; import org.springframework.cloud.function.test.GenericFunction; import org.springframework.context.ConfigurableApplicationContext; @@ -372,6 +375,34 @@ public class ContextFunctionCatalogAutoConfigurationTests { } } + @Test + public void kotlinLambdas() { + create(KotlinLambdasConfiguration.class); + + assertThat(context.getBean("kotlinFunction")).isInstanceOf(Function.class); + assertThat(context.getBean("kotlinFunction")).isInstanceOf(Function1.class); + assertThat(catalog.lookup(Function.class, "kotlinFunction")) + .isInstanceOf(Function.class); + assertThat(inspector.getInputType(catalog.lookup(Function.class, "kotlinFunction"))) + .isAssignableFrom(String.class); + assertThat(inspector.getOutputType(catalog.lookup(Function.class, "kotlinFunction"))) + .isAssignableFrom(String.class); + + assertThat(context.getBean("kotlinConsumer")).isInstanceOf(Consumer.class); + assertThat(context.getBean("kotlinConsumer")).isInstanceOf(Function1.class); + assertThat(catalog.lookup(Function.class, "kotlinConsumer")) + .isInstanceOf(Function.class); + assertThat(inspector.getInputType(catalog.lookup(Function.class, "kotlinConsumer"))) + .isAssignableFrom(String.class); + + assertThat(context.getBean("kotlinSupplier")).isInstanceOf(Supplier.class); + assertThat(context.getBean("kotlinSupplier")).isInstanceOf(Function0.class); + assertThat(catalog.lookup(Supplier.class, "kotlinSupplier")) + .isInstanceOf(Supplier.class); + assertThat(inspector.getOutputType(catalog.lookup(Supplier.class, "kotlinSupplier"))) + .isAssignableFrom(String.class); + } + private void create(String jarfile, Class config, String... props) { try { URL[] urls = new URL[] { new ClassPathResource(jarfile).getURL() }; diff --git a/spring-cloud-function-context/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt b/spring-cloud-function-context/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt new file mode 100644 index 000000000..3c9e7d2c1 --- /dev/null +++ b/spring-cloud-function-context/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2018 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.kotlin + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +/** + * @author Oleg Zhurakousky + * + */ +@EnableAutoConfiguration +@Configuration +open class KotlinLambdasConfiguration { + @Bean + open fun kotlinFunction(): (String) -> String { + return { it.toUpperCase() } + } + + @Bean + open fun kotlinConsumer(): (String) -> Unit { + return { } + } + + @Bean + open fun kotlinSupplier(): () -> String { + return { "Hello" } + } +} \ No newline at end of file