From 11ac6cd679bc869017b2588f9e0289a4d6a770a5 Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Fri, 6 Sep 2019 20:04:24 +0200 Subject: [PATCH] GH-364 Fix type discovery on Kotlin lambdas Added FunctionRegistration for each Kotlin lambdas with the correct types Resolve #364 --- .../BeanFactoryAwareFunctionRegistry.java | 28 ++- ...tlinLambdaToFunctionAutoConfiguration.java | 237 +++++++++--------- ...onCatalogAutoConfigurationKotlinTests.java | 21 +- .../kotlin/KotlinLambdasConfiguration.kt | 6 + 4 files changed, 157 insertions(+), 135 deletions(-) diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java index e4120d204..49670dff9 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/BeanFactoryAwareFunctionRegistry.java @@ -175,9 +175,28 @@ public class BeanFactoryAwareFunctionRegistry if (function == null) { function = this.registrationsByName.get(name); } + + if (function != null && this.isKotlin(function.getClass())) { + function = this.applicationContext.getBean("_" + name, FunctionRegistration.class); + } return function; } + private boolean isKotlin(Class functionClass) { + if (functionClass != null) { + if ("kotlin.jvm.internal.Lambda".equals(functionClass.getName())) { + return true; + } + else { + return this.isKotlin(functionClass.getSuperclass()); + } + } + else { + return false; + } + } + + private Type discoverFunctionType(Object function, String... names) { boolean beanDefinitionExists = false; for (int i = 0; i < names.length && !beanDefinitionExists; i++) { @@ -194,11 +213,11 @@ public class BeanFactoryAwareFunctionRegistry private String discoverDefaultDefinitionIfNecessary(String definition) { if (StringUtils.isEmpty(definition)) { - String[] functionNames = this.applicationContext.getBeanNamesForType(Function.class); - String[] consumerNames = this.applicationContext.getBeanNamesForType(Consumer.class); - String[] supplierNames = this.applicationContext.getBeanNamesForType(Supplier.class); + String[] functionNames = Stream.of(this.applicationContext.getBeanNamesForType(Function.class)).filter(n -> !n.startsWith("_")).toArray(String[]::new); + String[] consumerNames = Stream.of(this.applicationContext.getBeanNamesForType(Consumer.class)).filter(n -> !n.startsWith("_")).toArray(String[]::new); + String[] supplierNames = Stream.of(this.applicationContext.getBeanNamesForType(Supplier.class)).filter(n -> !n.startsWith("_")).toArray(String[]::new); /* - * we may need to add BiFunction and BuConsumer at some point + * we may need to add BiFunction and BiConsumer at some point */ List names = Stream .concat(Stream.of(functionNames), Stream.concat(Stream.of(consumerNames), Stream.of(supplierNames))).collect(Collectors.toList()); @@ -251,6 +270,7 @@ public class BeanFactoryAwareFunctionRegistry FunctionRegistration registration; Type currentFunctionType = null; + if (function instanceof FunctionRegistration) { registration = (FunctionRegistration) function; currentFunctionType = currentFunctionType == null ? registration.getType().getType() : currentFunctionType; diff --git a/spring-cloud-function-kotlin/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java b/spring-cloud-function-kotlin/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java index 212502db6..bf9e43350 100644 --- a/spring-cloud-function-kotlin/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java +++ b/spring-cloud-function-kotlin/src/main/java/org/springframework/cloud/function/context/config/KotlinLambdaToFunctionAutoConfiguration.java @@ -16,17 +16,26 @@ package org.springframework.cloud.function.context.config; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import kotlin.Unit; + import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; +import kotlin.jvm.functions.Function3; +import kotlin.jvm.functions.Function4; 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.BeanNameAware; +import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -34,10 +43,11 @@ import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.cloud.function.context.FunctionType; +import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.core.type.MethodMetadata; +import org.springframework.core.ResolvableType; +import org.springframework.util.ObjectUtils; /** * Configuration class which defines the required infrastructure to bootstrap Kotlin @@ -52,11 +62,12 @@ class KotlinLambdaToFunctionAutoConfiguration { protected final Log logger = LogFactory.getLog(getClass()); + /** - * Will transform all discovered Kotlin's Function1 and Function0 lambdas to java + * Will transform all discovered Kotlin's Function lambdas to java * Supplier, Function and Consumer, retaining the original Kotlin type - * characteristics. In other words the resulting bean could be cast to both java and - * kotlin types (i.e., java Function<I,O> vs. kotlin Function1<I,O>) + * characteristics. + * * @return the bean factory post processor */ @Bean @@ -69,134 +80,122 @@ class KotlinLambdaToFunctionAutoConfiguration { String[] beanDefinitionNames = beanFactory.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { - BeanDefinition beanDefinition = beanFactory - .getBeanDefinition(beanDefinitionName); - Object source = beanDefinition.getSource(); - if (source instanceof MethodMetadata) { - String returnTypeName = ((MethodMetadata) source) - .getReturnTypeName(); - if (returnTypeName.startsWith("kotlin.jvm.functions.Function")) { - FunctionType functionType = new FunctionType( - FunctionContextUtils.findType(beanDefinitionName, - beanFactory)); - if (returnTypeName.equals("kotlin.jvm.functions.Function1")) { - if (Unit.class - .isAssignableFrom(functionType.getOutputType())) { - KotlinLambdaToFunctionAutoConfiguration.this.logger - .debug("Transforming Kotlin lambda " - + beanDefinitionName - + " to java Consumer"); - this.register(beanDefinitionName, beanDefinition, - KotlinConsumer.class, - (BeanDefinitionRegistry) beanFactory); - } - else { - KotlinLambdaToFunctionAutoConfiguration.this.logger - .debug("Transforming Kotlin lambda " - + beanDefinitionName - + " to java Function"); - this.register(beanDefinitionName, beanDefinition, - KotlinFunction.class, - (BeanDefinitionRegistry) beanFactory); - } - } - else { - KotlinLambdaToFunctionAutoConfiguration.this.logger.debug( - "Transforming Kotlin lambda " + beanDefinitionName - + " to java Supplier"); - this.register(beanDefinitionName, beanDefinition, - KotlinSupplier.class, - (BeanDefinitionRegistry) beanFactory); - } - } + BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanDefinitionName); + + ResolvableType rt = beanDefinition.getResolvableType(); + if (rt.getType().getTypeName().startsWith("kotlin.jvm.functions.Function")) { + RootBeanDefinition cbd = new RootBeanDefinition(KotlinFunctionWrapper.class); + ConstructorArgumentValues ca = new ConstructorArgumentValues(); + ca.addGenericArgumentValue(beanDefinition); + cbd.setConstructorArgumentValues(ca); + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("_" + beanDefinitionName, cbd); } } } - - private void register(String originalName, BeanDefinition originalDefinition, - Class clazz, BeanDefinitionRegistry registry) { - RootBeanDefinition cbd = new RootBeanDefinition(clazz); - ConstructorArgumentValues ca = new ConstructorArgumentValues(); - ca.addGenericArgumentValue(originalDefinition); - cbd.setConstructorArgumentValues(ca); - registry.removeBeanDefinition(originalName); - registry.registerBeanDefinition(originalName, cbd); - } }; } - /** - * Wrapper for Kotlin lambda to be represented as both Java Function<I,O> as - * well as Kotlin's Function1<I,O>. - */ - private static final class KotlinFunction - implements Function, Function1 { - private final Function1 kotlinLambda; + @SuppressWarnings({ "unchecked", "rawtypes" }) + private static final class KotlinFunctionWrapper implements Function, Supplier, Consumer, + Function0, Function1, Function2, + Function3, Function4, + FactoryBean, + BeanNameAware, + BeanFactoryAware { - private KotlinFunction(Function1 kotlinLambda) { - this.kotlinLambda = kotlinLambda; + private final Object kotlinLambdaTarget; + + private String name; + + private ConfigurableListableBeanFactory beanFactory; + + private KotlinFunctionWrapper(Object kotlinLambdaTarget) { + this.kotlinLambdaTarget = kotlinLambdaTarget; } @Override - public O apply(I i) { - return this.kotlinLambda.invoke(i); + public Object apply(Object input) { + if (ObjectUtils.isEmpty(input)) { + return this.invoke(); + } + else if (ObjectUtils.isArray(input)) { + return null; + } + else { + return this.invoke(input); + } } @Override - public O invoke(I i) { - return this.apply(i); + public Object invoke(Object arg0, Object arg1, Object arg2, Object arg3) { + return ((Function4) this.kotlinLambdaTarget).invoke(arg0, arg1, arg2, arg3); } + @Override + public Object invoke(Object arg0, Object arg1, Object arg2) { + return ((Function3) this.kotlinLambdaTarget).invoke(arg0, arg1, arg2); + } + + @Override + public Object invoke(Object arg0, Object arg1) { + return ((Function2) this.kotlinLambdaTarget).invoke(arg0, arg1); + } + + @Override + public Object invoke(Object arg0) { + return ((Function1) this.kotlinLambdaTarget).invoke(arg0); + } + + @Override + public Object invoke() { + return ((Function0) this.kotlinLambdaTarget).invoke(); + } + + @Override + public void accept(Object[] input) { + this.apply(input); + } + + @Override + public Object get() { + return this.apply(null); + } + + @Override + public FunctionRegistration getObject() throws Exception { + Type functionType = FunctionContextUtils.findType(this.name.substring(1), this.beanFactory); + FunctionRegistration registration = new FunctionRegistration<>(this, name); + Type[] types = ((ParameterizedType) functionType).getActualTypeArguments(); + + if (functionType.getTypeName().contains("Function0")) { + functionType = ResolvableType.forClassWithGenerics(Supplier.class, ResolvableType.forType(types[0])) + .getType(); + } + else if (functionType.getTypeName().contains("Function1")) { + functionType = ResolvableType.forClassWithGenerics(Function.class, ResolvableType.forType(types[0]), + ResolvableType.forType(types[1])).getType(); + } + else { + throw new UnsupportedOperationException("Multi argument Kotlin functions are not currently supported"); + } + registration = registration.type(functionType); + return registration; + } + + @Override + public Class getObjectType() { + return FunctionRegistration.class; + } + + @Override + public void setBeanName(String name) { + this.name = name; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ConfigurableListableBeanFactory) beanFactory; + } } - - /** - * Wrapper for Kotlin lambda to be represented as both Java Consumer<I> as well - * as Kotlin's Function1<I,Unit>. - */ - private static final class KotlinConsumer - implements Consumer, Function1 { - - private final Function1 kotlinLambda; - - private KotlinConsumer(Function1 kotlinLambda) { - this.kotlinLambda = kotlinLambda; - } - - @Override - public U 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<O> as well - * as Kotlin's Function0<O>. - */ - private static final class KotlinSupplier implements Supplier, Function0 { - - private final Function0 kotlinLambda; - - private 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-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinTests.java b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinTests.java index 38e6ab08a..2e84e86f1 100644 --- a/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinTests.java +++ b/spring-cloud-function-kotlin/src/test/java/org/springframework/cloud/function/kotlin/ContextFunctionCatalogAutoConfigurationKotlinTests.java @@ -16,16 +16,13 @@ package org.springframework.cloud.function.kotlin; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; import org.junit.After; -import org.junit.Ignore; import org.junit.Test; -import reactor.core.publisher.Flux; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; @@ -57,12 +54,10 @@ public class ContextFunctionCatalogAutoConfigurationKotlinTests { } @Test - @Ignore public void kotlinLambdas() { create(new Class[] { KotlinLambdasConfiguration.class, SimpleConfiguration.class }); - assertThat(this.context.getBean("kotlinFunction")).isInstanceOf(Function.class); assertThat(this.context.getBean("kotlinFunction")).isInstanceOf(Function1.class); assertThat((Function) this.catalog.lookup(Function.class, "kotlinFunction")) .isInstanceOf(Function.class); @@ -73,7 +68,6 @@ public class ContextFunctionCatalogAutoConfigurationKotlinTests { .getOutputType(this.catalog.lookup(Function.class, "kotlinFunction"))) .isAssignableFrom(String.class); - assertThat(this.context.getBean("kotlinConsumer")).isInstanceOf(Consumer.class); assertThat(this.context.getBean("kotlinConsumer")).isInstanceOf(Function1.class); assertThat((Function) this.catalog.lookup(Function.class, "kotlinConsumer")) .isInstanceOf(Function.class); @@ -81,21 +75,24 @@ public class ContextFunctionCatalogAutoConfigurationKotlinTests { .getInputType(this.catalog.lookup(Function.class, "kotlinConsumer"))) .isAssignableFrom(String.class); - assertThat(this.context.getBean("kotlinSupplier")).isInstanceOf(Supplier.class); assertThat(this.context.getBean("kotlinSupplier")).isInstanceOf(Function0.class); - Supplier> supplier = this.catalog.lookup(Supplier.class, - "kotlinSupplier"); - assertThat(supplier.get().blockFirst()).isEqualTo("Hello"); + Supplier supplier = this.catalog.lookup(Supplier.class, "kotlinSupplier"); + assertThat(supplier.get()).isEqualTo("Hello"); assertThat((Supplier) this.catalog.lookup(Supplier.class, "kotlinSupplier")) .isInstanceOf(Supplier.class); assertThat(this.inspector .getOutputType(this.catalog.lookup(Supplier.class, "kotlinSupplier"))) .isAssignableFrom(String.class); - Function, Flux> function = this.catalog + Function function = this.catalog .lookup(Function.class, "kotlinFunction|function2"); - assertThat(function.apply(Flux.just("Hello")).blockFirst()) + assertThat(function.apply("Hello")) .isEqualTo("HELLOfunction2"); + + Function javaFunction = this.catalog + .lookup(Function.class, "javaFunction"); + assertThat(javaFunction.apply("Hello")) + .isEqualTo("Hello"); } private void create(Class[] types, String... props) { diff --git a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt index 0d9babef4..a8271a1e7 100644 --- a/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt +++ b/spring-cloud-function-kotlin/src/test/kotlin/org/springframework/cloud/function/kotlin/KotlinLambdasConfiguration.kt @@ -19,6 +19,7 @@ package org.springframework.cloud.function.kotlin import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import java.util.function.Function /** * @author Oleg Zhurakousky @@ -41,4 +42,9 @@ open class KotlinLambdasConfiguration { open fun kotlinSupplier(): () -> String { return { "Hello" } } + + @Bean + open fun javaFunction(): Function { + return Function { x -> x } + } }