/spring-fu

Spring Fu is an experimental Kotlin micro-framework that makes it easy to create lightweight Spring-powered applications with functional APIs instead of annotations

Primary LanguageKotlinApache License 2.0Apache-2.0

Spring Fu is an experimental Kotlin micro-framework that makes it easy to create 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 {
		server(netty()) {
			mustache()
			codecs {
				jackson()
			}
			routes(import = ::appRoutes)
		}
	}
}.run(await = true)

fun appRoutes() = routes {
	val userHandler = ref<UserHandler>()
	GET("/", userHandler::listView)
	GET("/api/user", userHandler::listApi)
}

It is a thin but opinionated layer on top of a subset of Spring Framework, Spring Data, Spring Security and other Spring projects that provides an alternative configuration model to Spring Boot for developing applications in Kotlin.

  • Functional bean registration instead of JavaConfig for both internals and application

  • Features are configured explicitly via expressive configuration leveraging Kotlin domain-specific languages (DSL)

  • Flexible programmatic configuration

  • Minimal reflection usage

  • No classpath scanning

  • No annotation processing

  • Modules provide configuration DSL and dependencies for other Spring projects and JVM ecosystem

  • Reactive and coroutines web client and server based on WebFlux functional API

  • Reactive and coroutines persistence via Spring Data (MongoDB supported)

  • No blocking web/persistence support is provided since coroutines provides support for imperative programming.

Upcoming features:

Note

Some features experimented in Spring Fu, like Coroutines support, might be integrated later in other Spring projects.

In addition to the whole Spring and Reactor teams, credits to Thomas Girard for its spring-webflux-kotlin-dsl experiment that initially demonstrated this approach was possible and to Konrad Kaminski for his awesome spring-kotlin-coroutine project.

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 1.0.0.M1 is currently under active development so no release is available yet, but you can clone the repository, import it into Intellij IDEA, try to play with the tests and sample applications.

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:

Functional 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 {
			server(netty()) {
				cors(origin = "example.com")
				mustache()
				codecs {
					jackson()
					protobuf()
				}
				routes(import = ::appRoutes)
				security { // TODO }
			}
			client {
				codecs {
					jackson()
				}
			}
		}
	}
	// Any kind of custom conditional bean definition is possible
	if (env.activeProfiles.any { it.startsWith("foo") }) {
		bean<Foo>()
	}
}.app.run(await = true, profiles = "data, web")

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

fun appRoutes() = routes {
	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)
	}
}

Comparison with 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>()
}

Comparison with @Component

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>()
}

Comparison with 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)
  }
}
routes {
  val r = ref<MyRepository>()
  "/api/article".nest {
    GET("/") {
      r.findAll()
    }
    GET("/{id}") {
      val id = it.pathVariable("id")
      r.findById(id)
    }
  }
}