Build Status

Spring Fu (for functional APIs) is an experimental Kotlin micro-framework based on functional configuration intended to test ideas for upcoming Spring Boot releases. It makes it easy to create fast and lightweight Spring-powered applications with functional APIs instead of annotations.

See the reference documentation for more details.

Overview

A simple Spring Fu web application rendering an HTML page and exposing a JSON HTTP endpoint looks like that:

fun main(args: Array<String>) = application {
    bean<UserRepository>()
    bean<UserHandler>()
    webflux {
        val port = if (profiles.contains("test")) 8181 else 8080
        server(netty(port)) {
            mustache()
            codecs {
                jackson()
            }
            router {
                val userHandler = ref<UserHandler>()
                GET("/", userHandler::listView)
                GET("/api/user", userHandler::listApi)
            }
        }
    }
}.run(await = true)

It leverages a subset of Spring Framework, Boot, Data and Security to provide an alternative configuration model for developing applications in Kotlin:

  • Provides a Kotlin idiomatic developer experience

  • Functional configuration via Kotlin DSL instead of annotations

  • Minimal reflection usage, no classpath scanning, no annotation processing required

  • Explicit configuration: minimal core, no conventions, no classpath detection

  • Flexible programmatic configuration: control flow like if, or for can be used to register conditionally your beans

  • Run as native image with instant startup via GraalVM support

  • Coroutines allow to leverage Spring Reactive support (WebFlux, Data) with imperative programming style

  • Spring packages filtering in order to have a minimal classpath and deploy just what you need

  • Optional single language fullstack support with frontend written in Kotlin (compile to JavaScript or WebAssembly)

  • Reactive/Coroutines SQL support

  • Self-sufficient opinionated documentation (Kotlin, constructor injection, functional configuration, WebFlux, Coroutines, etc.)

Please send us your feedback on the #spring channel of Kotlin Slack. Feel free to open issues, contribute fixes or new modules via pull requests.

Getting started

Spring Fu 0.0.1 is currently under active development so no release is available yet, but you have can try 0.0.1.BUILD-SNAPSHOT builds.

Bootstrap

A Spring Fu bootstrap is an archive containing minimal project templates designed to allow you getting started quickly and easily Spring Fu applications. To start a new project, download a bootstrap .zip archive, extract it and follow README.adoc instructions.

Documentation

To start with Spring Fu, you can read the reference documentation.

API documentation will be available shortly.

You can have a look to the sample applications:

Credits

In addition to the whole Spring and Reactor teams, special credits to:

Application configuration

Spring Fu functional configuration is leveraging Kotlin DSL that allows you to configure your application explicitly. Each custom block like configuration, actuators or webflux is in fact a more high level beans {} block with a custom DSL provided for easy configuration. Since this configuration is code, you can use any kind of custom programmatic bean registration without having to implement your own @Conditional annotation.

Here is an example of a typical Spring Fu application functional configuration.

fun main(args: Array<String) = application {
    configuration {
        AppConfiguration(name = env["SYSTEM_ENV"] ?: "default")
    }
    actuators(beans = false, mapping = false)
    logging {
        level(INFO)
        level("org.springframework", DEBUG)
        logback {
            consoleAppender()
            rollingFileAppender(file = File("log.txt"))
        }
    }
    profile("data") {
        bean<UserRepository>()
        bean<ArticleRepository>()
        mongodb(uri = "mongodb://myserver.com/foo")
        listener<ContextStartedEvent> {
            ref<UserRepository>().init()
            ref<ArticleRepository>().init()
        }
    }
    profile("web") {
        bean<HtmlHandler>()
        bean<ApiHandler>()
        webflux {
            val port = if (profiles.contains("test")) 8181 else 8080
            server(netty(port)) {
                cors(origin = "example.com")
                mustache()
                codecs {
                    jackson()
                    protobuf()
                }
                router(routes(ref(), ref()))
                security { // TODO }
            }
            client {
                codecs {
                    jackson()
                }
            }
        }
    }
}.app.run(await = true, profiles = "data, web")

data class AppConfiguration(
    val name: String,
    val remoteUrl: String  = "http://localhost:8080"
)

fun routes(htmlHandler: HtmlHandler, apiHandler: ApiHandler) = router {
    GET("/", htmlHandler::blog)
    GET("/article/{id}", htmlHandler::article)
    "/api".nest {
        GET("/", apiHandler::list)
        POST("/", apiHandler::create)
        PUT("/{id}", apiHandler::update)
        DELETE("/{id}", apiHandler::delete)
    }
}

Beans

Beans are simply defined as follows.

application {
    bean<UserRepository>()
    bean<ArticleRepository>()
    bean<HtmlHandler>()
    bean<ApiHandler>()
}

Since classes with a single constructor have their parameters automatically autowired, it is recommended to use constructor injection with val read-only (and non-nullable when possible) private properties.

class HtmlHandler(
    private val userRepository: UserRepository,
    private val articleRepository: ArticleRepository
) {
    // ...
}

Configuration

Configuration is usually done using data classes instantiation with environment and default values.

application {
    configuration {
        TestConfiguration(name = env["SYSTEM_ENV"] ?: "default")
    }
}

data class TestConfiguration(
        val name: String
)

Listeners

Declare application event Listeners in order to run tasks when ApplicationContextEvent like ContextStartedEvent or ContextRefreshedEvent are emitted.

application {
    listener<ContextStartedEvent> {
        ref<UserRepository>().init()
    }
}

Comparison with annotation-based configuration

JavaConfig

Functional bean definition allows to define beans in an efficient way with minimal reflection usage, no proxy and with a concise Kotlin DSL that takes advantage of reified type parameters to avoid type erasure. The beans {} block is in fact a regular ApplicationContextInitializer.

JavaConfig

Functional bean definition

 @Configuration
 class MyConfiguration {

  @Bean
  fun foo() = Foo()

  @Bean
  fun bar(foo: Foo) = Bar(foo)
}
val myConfiguration = beans {
  bean<Foo>()
  // Implicit autowiring by constructor
  bean<Bar>()
}
Conditional registration

One key characteristic of functional bean definition is that you can register bean programmatically using if, for for or other control flow where in JavaConfig you rely on predefined or custom @Conditional annotations.

JavaConfig

Functional bean definition

  class MyCustomCondition : Condition {

    override fun matches(context: c,
        m: AnnotatedTypeMetadata): Boolean {
      val myCustomLib = MyCustomLib()
      return myCustomLib.checkStatus()
    }
  }

  @Configuration
  class MyConfiguration {

    @Bean
    @Conditional(MyCustomCondition::class)
    fun foo() = Foo()
  }
val testConfiguration = beans {

  val myCustomLib = MyCustomLib()
  if (myCustomLib.checkStatus()) {
    bean<Foo>()
  }
}
Profiles

A profiles shortcut (internally relying on programmatic bean registration) is provided in functional bean definition.

JavaConfig

Functional bean definition

 @Configuration
 @Profile("test")
 class TestConfiguration {

  @Bean
  fun foo() = Foo()

  @Bean
  fun bar(foo: Foo) = Bar(foo)
}
val testConfiguration = beans {
  profile("test") {
    bean<Foo>()
    bean<Bar>()
  }
}

Component scanning

Functional bean definition is explicit, does not imply any classpath scanning and supports constructor parameters autowiring.

@Component scanning

Functional bean definition

@Component
class Foo {
  // ...
}

@Component
class Bar(private val f: Foo) {
  // ...
}
class Foo {
  // ...
}
class Bar(private val f: Foo) {
  // ...
}

beans {
  bean<Foo>()
  bean<Bar>()
}

Controllers

Kotlin WebFlux router provides a simple but powerful way to implement your web application. HTTP API, streaming but also view rendering are supported.

Annotation-based controller

Kotlin WebFlux routes

@RestController
@RequestMapping("/api/article")
class MyController(private val r: MyRepository) {

  @GetMapping("/")
  fun findAll() =
    r.findAll()

  @GetMapping("/{id}")
  fun findOne(@PathVariable id: Long) =
    repository.findById(id)
  }
}
router {
  val r = ref<MyRepository>()
  "/api/article".nest {
    GET("/") {
      r.findAll()
    }
    GET("/{id}") {
      val id = it.pathVariable("id")
      r.findById(id)
    }
  }
}

Modules

Modules are JAR libraries intended to augment Spring Fu minimal core with support for various Spring projects and JVM technologies. To use them, you just need to add the relevant ones to the compile dependencies of the project, as you would do with a Spring Boot starter, to have access to additional capabilities.

build.gradle.kts

dependencies {
    implementation(project("org.springframework.fu.module:module-foobar"))
}

The module configuration DSL is then available as a configuration extension and needs to be configured since with Spring Fu configuration is always explicit.

Application.kt

fun main(args: Array<String>) = application {
    foobar {
        feature1(param = 123)
        feature2(param = "Hello world")
    }
}.run(await = true)

Logging module

This module contains common logging configurations. Actually, logging-logback is the only implementation.

application {
    logging {
        level(INFO)
        level("org.springframework", DEBUG)
        level<DefaultListableBeanFactory>(WARN)

        customLoggingFrameworkToImplement {
            ...
        }
    }
}

Spring Data MongoDB module

This module configures a ReactiveMongoTemplate bean.

application {
    mongodb(uri = "mongodb://myserver.com/foo")
}

Coroutine Spring Data MongoDB module

This module configures a CoroutineMongoTemplate bean.

application {
    mongodb(uri = "mongodb://myserver.com/foo") {
        coroutine()
    }
}

Dynamic configuration module

This module provides an alternative to the regular Spring Fu application configuration, designed to allow writing configuration using Kotlin scripts (*.kts files).

Instead of externalizing the configuration in properties or Yaml files, it allows externalizing it using Kotlin Script files. These files use the same syntax as a regular statically-compiled Kotlin configuration, and benefit from the same type-safety and auto-complete support provided by the IDE. They are thus well suited for dynamic or remote configuration loading for example.

The main drawbacks are the size of the Kotlin compiler dependency (almost 20M) and the time, CPU and memory required for compiling the script. That’s why it is not used by default.

application {
    configuration("myconf.kts")
}

Jackson module

This module configures a Jackson JSON codec on WebFlux server and client.

application {
    webflux {
        server(netty()) {
            codecs {
                jackson()
            }
        }
        client {
            codecs {
                jackson()
            }
        }
    }
}

Mustache module

This module configures a Mustache view resolver.

application {
    webflux {
        server(netty()) {
            mustache()
        }
    }
}

Thymeleaf module

This module configures a Thymeleaf view resolver.

application {
    webflux {
        server(netty()) {
            thymeleaf()
        }
    }
}

WebFlux module

This module configures WebFlux client or server. 0..n clients are supported (you can specify the bean name to differentiate them) while only 0..1 server can ben declared. If multiple servers are needed, create one application per server (as usual in microservices architecture).

application {
    bean<HtmlHandler>()
    bean<ApiHandler>()
    webflux {
        server(netty()) {
            codecs {
                jackson()
            }
            router {
                val htmlHandler = ref<HtmlHandler>()
                val apiHandler = ref<ApiHandler>()
                GET("/", htmlHandler::blog)
                GET("/article/{id}", htmlHandler::article)
                "/api".nest {
                    GET("/", apiHandler::list)
                    POST("/", apiHandler::create)
                    PUT("/{id}", apiHandler::update)
                    DELETE("/{id}", apiHandler::delete)
                }
            }
        }
        client {
            codecs {
                jackson()
            }
        }
    }
}

Coroutine WebFlux module

This module adds support for WebFlux Coroutine API and can create a CoroutineWebClient bean. Be aware that Callable references to suspend functions are not supported yet in Kotlin compiler (KT-16908).

application {
    bean<CoroutineHandler>()
    webflux {
        server(netty()) {
            codecs {
                jackson()
            }
            coRouter {
                val coroutineHandler = ref<CoroutineHandler>()
                GET("/") { coroutineHandler.blog(it))
                GET("/article/{id}") { coroutineHandler.article(it) )
            }
        }
        client {
            coroutine()
            codecs {
                jackson()
            }
        }
    }
}

Test module

This module provides support for testing via JUnit 5 configured by default with per class lifecycle in order to make tests faster and allow using @BeforeClass and @AfterClass on regular non-static methods.

Integration tests

class IntegrationTests {

    private val client = WebTestClient.bindToServer().baseUrl("http://localhost:8080").build()

    @BeforeAll
    fun beforeAll() {
        app.run()
    }

    @Test
    fun `Request HTML endpoint`() {
        client.get().uri("/").exchange()
                .expectStatus().is2xxSuccessful
                .expectHeader().contentType("text/html;charset=UTF-8")
    }

    @Test
    fun `Request HTTP API endpoint`() {
        client.get().uri("/api/user").exchange()
                .expectStatus().is2xxSuccessful
                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
    }

    @Test
    fun `Request conf endpoint`() {
        client.get().uri("/conf").exchange()
                .expectStatus().is2xxSuccessful
                .expectHeader().contentType("text/plain;charset=UTF-8")
    }

    @AfterAll
    fun afterAll() {
        app.stop()
    }
}