spring-cloud / spring-cloud-function

Apache License 2.0
1.04k stars 615 forks source link

Kotlin: FunctionTypeUtils do not support Kotlin functions following functional bean definition approach #1047

Open ArnauAregall opened 1 year ago

ArnauAregall commented 1 year ago

Describe the bug

I am trying to follow the reference documentation section about registering functions with the Functional Bean definition approach, but using Kotlin.

Probably I am doing something wrong, but I could not find a way to define the FunctionRegistration#type either than using org.springframework.cloud.function.context.catalog.FunctionTypeUtils#functionType(), but seems it does not support Kotlin functions.

java.lang.IllegalArgumentException: Must be one of Supplier, Function, Consumer or FunctionRegistration. Was interface kotlin.jvm.functions.Function1
    at org.springframework.util.Assert.isTrue(Assert.java:122) ~[spring-core-6.0.9.jar:6.0.9]
    at org.springframework.cloud.function.context.catalog.FunctionTypeUtils.assertSupportedTypes(FunctionTypeUtils.java:536) ~[spring-cloud-function-context-4.0.3.jar:4.0.3]
    at org.springframework.cloud.function.context.catalog.FunctionTypeUtils.getInputType(FunctionTypeUtils.java:327) ~[spring-cloud-function-context-4.0.3.jar:4.0.3]
    at org.springframework.cloud.function.context.FunctionRegistration.type(FunctionRegistration.java:129) ~[spring-cloud-function-context-4.0.3.jar:4.0.3]
    at tech.aaregall.lab.AppFunctional.initialize(AppFunctional.kt:22) ~[main/:na]

Versions:

Sample

package tech.aaregall.lab

import org.springframework.boot.SpringBootConfiguration
import org.springframework.cloud.function.context.FunctionRegistration
import org.springframework.cloud.function.context.FunctionalSpringApplication
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils
import org.springframework.context.ApplicationContextInitializer
import org.springframework.context.support.GenericApplicationContext

@SpringBootConfiguration
class AppFunctional : ApplicationContextInitializer<GenericApplicationContext> {

    fun uppercase(): (String) -> String {
        return { it.uppercase() }
    }

    override fun initialize(context: GenericApplicationContext) {
        context.registerBean("demo", FunctionRegistration::class.java,
            FunctionRegistration(uppercase())
                .type(FunctionTypeUtils.functionType(String::class.java, String::class.java))
        )
    }
}

fun main(args: Array<String>) {
    FunctionalSpringApplication.run(AppFunctional::class.java, *args)
}
plugins {
    id("org.springframework.boot")
    id("io.spring.dependency-management")
    kotlin("jvm")
    kotlin("plugin.spring")
}

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
    implementation("org.springframework.cloud:spring-cloud-function-web")
    implementation("org.springframework.cloud:spring-cloud-function-kotlin")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.projectreactor:reactor-test")
}

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
    }
}

The Java version works perfectly in the same project with the same approach + same dependencies.

package tech.aaregall.lab;

import org.springframework.boot.SpringBootConfiguration;
import org.springframework.cloud.function.context.FunctionRegistration;
import org.springframework.cloud.function.context.FunctionalSpringApplication;
import org.springframework.cloud.function.context.catalog.FunctionTypeUtils;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.support.GenericApplicationContext;

import java.util.function.Function;

@SpringBootConfiguration
public class AppFunctionalJavaVersion implements ApplicationContextInitializer<GenericApplicationContext> {

    public static void main(String[] args) {
        FunctionalSpringApplication.run(AppFunctionalJavaVersion.class, args);
    }

    public Function<String, String> uppercase() {
        return String::toUpperCase;
    }

    @Override
    public void initialize(GenericApplicationContext context) {
        context.registerBean("demo", FunctionRegistration.class,
                () -> new FunctionRegistration<>(uppercase())
                        .type(FunctionTypeUtils.functionType(String.class, String.class)));

    }
}
ArnauAregall commented 1 year ago

As far as I could test forking the project, and adding kotlin.Function as supported type in FunctionTypeUtils, I believe it will not be enough in terms of developer experience.

I have a working approach but it requires to use org.springframework.cloud.function.context.config.KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper to build the target function, which is particularly ugly as it is a class intended for Spring Boot bean definition approach.

@SpringBootConfiguration
class DemoKotlinFunctionalApplication : ApplicationContextInitializer<GenericApplicationContext> {

    fun uppercase(): (String) -> String  = String::uppercase

    override fun initialize(context: GenericApplicationContext) {
        val target = KotlinLambdaToFunctionAutoConfiguration.KotlinFunctionWrapper(uppercase())
        target.setName("uppercase")
        target.setBeanFactory(context.beanFactory)

        context.registerBean("demo", FunctionRegistration::class.java, Supplier {
            FunctionRegistration(target)
                .type(FunctionTypeUtils.functionType(String::class.java, String::class.java))
        })
    }
}

fun main(args: Array<String>) {
    FunctionalSpringApplication.run(DemoKotlinFunctionalApplication::class.java, *args)
}

Maybe KotlinFunctionWrapper could be externalized somehow from the auto configuration class.

If you have any idea/insights I could open a PR .

polyoxidonium commented 6 hours ago

Any update on this? Because this became major issue in our project after upgrading to kotlin 2.x.x and I had to rewrite lots of beans to use java functional interface.

Versions: spring boot 3.0.13 spring cloud dependencies 2022.0.5

ArnauAregall commented 6 hours ago

Any update on this? Because this became major issue in our project after upgrading to kotlin 2.x.x and I had to rewrite lots of beans to use java functional interface.

Versions: spring boot 3.0.13 spring cloud dependencies 2022.0.5

As OP, I discarded the functional bean definition approach when using Kotlin due this.

Instead, I spent effort on generating GraalVM native images with traditional bean definition, as my need was a fast startup.

polyoxidonium commented 6 hours ago

As OP, I discarded the functional bean definition approach when using Kotlin due this.

Instead, I spent effort on generating GraalVM native images with traditional bean definition, as my need was a fast startup.

Yeah, well, that's not very convenient in terms of interoperability between kotlin and java. I'd expect to get FunctionTypeUtils fixed to be a kotlin-friendly as well. Why/How did you find extending the condition not sufficient?

ArnauAregall commented 4 hours ago

Even if kotlin.Function is added as a supported type, functional bean definition would still need to make use of SCF Boot Autoconfig internal classes, which is a bit contradictory for me.

I coincide that would be nice to see this refactored, although personally I don't plan to contribute any longer to this topic specifically., at least in the short term.