Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Functional web applications with Spring and Kotlin

Functional web applications with Spring and Kotlin

How to build reactive and functional web applications with Spring Framework 5, WebFlux and Kotlin. Video available at https://vimeo.com/221399691. Sources of the reference project are available at https://github.com/mixitconf/mixit/ and the related blog post at https://spring.io/blog/2017/01/04/introducing-kotlin-support-in-spring-framework-5-0.

Sébastien Deleuze

May 19, 2017
Tweet

More Decks by Sébastien Deleuze

Other Decks in Programming

Transcript

  1. Step 1 Switch from Java to Kotlin Step 2 Upgrade

    to Spring 5 Step 3 Migrate to WebFlux Step 4 WebFlux/Bean Kotlin DSLs
  2. Step 1 Switch from Java to Kotlin Step 2 Upgrade

    to Spring 5 Step 3 Migrate to WebFlux Step 4 WebFlux/Bean Kotlin DSLs
  3. Today Spring Boot 1 Spring MVC 8 Tomorrow Step 1

    Spring Boot 1 Spring MVC Kotlin 1.1 5
  4. Kotlin ➔ Created by JetBrains ➔ Elegant and pragmatic language

    ➔ Concise code ➔ Simple and easy to learn ➔ Embrace both functional and object oriented programming ➔ Very good Java interoperability 6
  5. How does it compare with … Swift Keep the good

    parts of Java and toss the bad ones. Kotlin is as statically typed as Java, and more suitable for functional programming than Java 8. Kotlin “stole” some good ideas to Groovy, but is statically typed by design. It also produces cleaner and smaller bytecode. Kotlin is a little bit less powerful than Scala, but much simpler, more pragmatic while still elegant. Kotlin and Swift are very close and both great languages. 7
  6. Why should you switch to Kotlin? This section is freely

    inspired from Magnus Vinther “Why you should totally switch to Kotlin” blog post available at https://goo.gl/Gwxqw3
  7. class Foo { val b: String = "b" // val

    means unmodifiable var i: Int = 0 // var means modifiable fun sum(x: Int, y: Int): Int { return x + y } fun maxOf(a: Float, b: Float) = if (a > b) a else b } Familiar Syntax 12
  8. val a = "abc" // type inferred to String val

    b = 4 // type inferred to Int val c: Double = 0.7 // type declared explicitly val d: List<String> = ArrayList() // type declared explicitly Type Inference 13
  9. val x = 4 val y = 7 print("sum of

    $x and $y is ${x + y}") // sum of 4 and 7 is 11 String interpolation 14
  10. if (obj is String) { print(obj.toUpperCase()) // obj is now

    known to be a String } Smart Casts 15
  11. val john1 = Person("John") val john2 = Person("John") john1 ==

    john2 // true (structural equality) john1 === john2 // false (referential equality) Intuitive Equals 16
  12. fun build(title: String, width: Int = 800, height: Int =

    600) { ... } build("foo") // width = 800, height = 600 build("foo", 400) // width = 400, height = 600 build("foo", 400, 450) // width = 400, height = 450 Default parameters 17
  13. fun build(title: String, width: Int = 800, height: Int =

    600) { ... } build("foo", 800, 300) // equivalent build(title = "foo", height = 300) // equivalent build(height = 300, title = "foo") // equivalent Named parameters 18
  14. when (x) { 1 -> print("x is 1") 2 ->

    print("x is 2") 3, 4 -> print("x is 3 or 4") in 5..10 -> print("x is 5, 6, 7, 8, 9, or 10") else -> print("x is out of range") } The when expression 19
  15. class Frame { var width: Int = 800 var height:

    Int = 600 val pixels: Int get() = width * height } Properties 20
  16. // User defined extensions fun String.format(): String { return this.replace('

    ', '_') } val formatted = str.format() // Kotlin standard library is provided with built-in JDK extensions str.removeSuffix(".txt") str.capitalize() str.substringAfterLast("/") str.replaceAfter(":", "classified") Extension Functions 21
  17. var a: String = "abc" a = null // compile

    error var b: String? = "xyz" b = null // no problem val x = b.length // compile error: b might be null val y = b?.length // type of y is nullable Int val name = ship?.captain?.name ?: "unknown" // Chainable safe calls Null Safety 22
  18. Better Lambdas val sum = { x: Int, y: Int

    -> x + y } // type: (Int, Int) -> Int val res = sum(4,7) // res == 11 numbers.filter({ x -> x.isPrime() }) // equivalent numbers.filter { x -> x.isPrime() } // equivalent numbers.filter { it.isPrime() } // equivalent // Allow to write concise functional code persons .filter { it.age >= 18 } .sortedBy { it.name } .map { it.email } .forEach { print(it) } 23
  19. And much more ➔ Data classes ➔ Type aliases ➔

    Co-routines ➔ Reified type parameters ➔ Underscore for unused parameters ➔ etc. 24
  20. 26 @RestController class UserController(val repo: UserRepository) { @GetMapping("/user/{id}") fun findOne(@PathVariable

    id: String) = repo.findOne(id) @GetMapping("/user") fun findAll() = repo.findAll() @PostMapping("/user") fun save(@RequestBody user: User) = repo.save(user) } interface UserRepository { fun findOne(id: String): User fun findAll(): List<User> fun save(user: User) } Spring MVC controller written in Kotlin Classes and functions are public by default Constructor injection without @Autowired if single constructor Static typing + type inference
  21. kotlin-spring Gradle and Maven plugin Automatically open Spring annotated classes

    and methods @SpringBootApplication open class Application { @Bean open fun foo() = ... @Bean open fun bar() = ... } @SpringBootApplication class Application { @Bean fun foo() = ... @Bean fun bar() = ... } Without kotlin-spring plugin With kotlin-spring plugin 27
  22. 28 noArg { annotation("org.springframework.data.mongodb.core.mapping.Document") } @Document data class User( @Id

    val login: String, val firstname: String, val lastname: String, val email: String, val company: String? = null, val description: Map<Language, String> = emptyMap(), val logoUrl: String? = null, val role: Role = Role.ATTENDEE) Create a synthetic constructor with no argument, useful with JPA, Spring Data ... kotlin-noarg Gradle and Maven plugin
  23. Step 1 Switch from Java to Kotlin Step 2 Upgrade

    to Spring 5 Step 3 Migrate to WebFlux Step 4 WebFlux/Bean Kotlin DSLs
  24. Step 2 Step 1 Spring Boot 1 Spring MVC Spring

    Boot 2 Spring MVC Kotlin 1.1 Kotlin 1.1 31
  25. 33

  26. 34 // "GET /foo" and "GET /foo?bar=baz" are allowed @GetMapping("/foo")

    fun foo(@RequestParam bar: String?) = ... // "GET /foo?bar=baz" is allowed and "GET /foo" will return an error @GetMapping("/foo") fun foo(@RequestParam bar: String) = ... Leveraging Kotlin nullable information To determine @RequestParam required attribute, also works for @Autowired
  27. 35 ➔ Spring Framework 5 ◆ ApplicationContext ◆ Spring MVC

    ◆ Spring WebFlux ◆ RestTemplate ◆ JDBC ➔ Spring Boot 2 ➔ Spring Data Kay release ➔ Reactor 3.1 Spring provides Kotlin specific API via extensions
  28. Extension example : reified type parameters // Java List<User> users

    = mongoTemplate.findAll(User.class); // Spring Data will provide this kind of Kotlin extension, see DATAMONGO-1689 inline fun <reified T : Any> MongoOperations.findAll(): List<T> = findAll(T::class.java) // So in Kotlin we just have to write val users: List<User> = mongoTemplate.findAll() // Or val users = mongoTemplate.findAll<User>() Goodbye type erasure, we are not going to miss you at all! 36
  29. Leveraging JSR 305 meta-annotations for generic tooling support Null safety

    of Spring APIs package org.springframework.lang; @Target({METHOD,PARAMETER}) @Retention(RUNTIME) @Documented @Nonnull(when = MAYBE) @TypeQualifierNickname public @interface Nullable { } 37 package org.springframework.lang; @Target(PACKAGE) @Retention(RUNTIME) @Documented @Nonnull @TypeQualifierDefault({METHOD,PARAMETER}) public @interface NonNullApi { }
  30. See SPR-15540 Null safety of Spring APIs // package-info.java @NonNullApi

    package org.springframework.cache; import org.springframework.lang.NonNullApi; // Cache.java import org.springframework.lang.Nullable; public interface Cache { @Nullable <T> T get(Object key, @Nullable Class<T> type); } 38
  31. Useful for both Java and Kotlin developers Null safety of

    Spring APIs 39 ➔ Comprehensive null-safety of Spring API in Kotlin (KT-10942) ➔ Warnings in Java IDE like IDEA (2017.1.4+) or Eclipse ➔ SonarSource already plan to leverage it ➔ Other Spring projects may provide null-safe API as well ➔ Other Java libraries may follow the same path ...
  32. 40 class SimpleTests { @Nested @DisplayName("a calculator") inner class Calculator

    { val calculator = SampleCalculator() @Test fun `should return the result of adding the first number to the second number`() { val sum = calculator.sum(2, 4) assertEquals(6, sum) } @Test fun `should return the result of subtracting the second number from the first number`() { val subtract = calculator.subtract(4, 2) assertEquals(2, subtract) } } } Specification like tests with Kotlin and JUnit 5 Kotlin support expressive function names between backticks
  33. 42 import io.spring.demo.* """ ${include("header")} <h1>${i18n("title")}</h1> <ul> ${users.joinToLine{ "<li>${i18n("user")} ${it.name}</li>"

    }} </ul> ${include("footer")} """ ➔ Available via Spring MVC & WebFlux JSR-223 support ➔ Regular Kotlin code, no new dialect to learn ➔ Extensible, refactoring and auto-complete support ➔ Need to cache compiled scripts for good performances (work in progress) See https://github.com/sdeleuze/kotlin-script-templating Kotlin type-safe templates
  34. Step 1 Switch from Java to Kotlin Step 2 Upgrade

    to Spring 5 Step 3 Migrate to WebFlux Step 4 WebFlux/Bean Kotlin DSLs
  35. Step 2 Spring Boot 2 Spring MVC Step 3 Spring

    Boot 2 Spring WebFlux Kotlin 1.1 Kotlin 1.1 44
  36. 48 @Controller, @RequestMapping Spring MVC Servlet API Servlet Container Servlet

    3.1, Netty, Undertow Spring WebFlux HTTP / Reactive Streams
  37. Reactive Streams 49 Publisher Subscriber 0..N data then 0..1 (Error

    | Complete) Subscribe then request(n) data (Backpressure)
  38. 54 val location = "Lyon, France" mainService.fetchWeather(location) .timeout(Duration.ofSeconds(2)) .doOnError {

    logger.error(it.getMessage()) } .onErrorResume { backupService.fetchWeather(location) } .map { "Weather in ${it.getLocation()} is ${it.getDescription()}" } .subscribe { logger.info(it) } fun fetchWeather(city: String): Mono<Weather> Reactive APIs are functional
  39. 55 val location = "Lyon, France" mainService.fetchWeather(location) .timeout(Duration.ofSeconds(2)) .doOnError {

    logger.error(it.getMessage()) } .onErrorResume { backupService.fetchWeather(location) } .map { "Weather in ${it.getLocation()} is ${it.getDescription()}" } .subscribe { logger.info(it) } times out and emits an error after 2 sec
  40. 56 val location = "Lyon, France" mainService.fetchWeather(location) .timeout(Duration.ofSeconds(2)) .doOnError {

    logger.error(it.getMessage()) } .onErrorResume { backupService.fetchWeather(location) } .map { "Weather in ${it.getLocation()} is ${it.getDescription()}" } .subscribe { logger.info(it) } logs a message in case of errors
  41. 57 val location = "Lyon, France" mainService.fetchWeather(location) .timeout(Duration.ofSeconds(2)) .doOnError {

    logger.error(it.getMessage()) } .onErrorResume { backupService.fetchWeather(location) } .map { "Weather in ${it.getLocation()} is ${it.getDescription()}" } .subscribe { logger.info(it) } switches to a different service in case of error
  42. 58 val location = "Lyon, France" mainService.fetchWeather(location) .timeout(Duration.ofSeconds(2)) .doOnError {

    logger.error(it.getMessage()) } .onErrorResume { backupService.fetchWeather(location) } .map { "Weather in ${it.getLocation()} is ${it.getDescription()}" } .subscribe { logger.info(it) } transforms a weather instance into a String message
  43. 59 val location = "Lyon, France" mainService.fetchWeather(location) .timeout(Duration.ofSeconds(2)) .doOnError {

    logger.error(it.getMessage()) } .onErrorResume { backupService.fetchWeather(location) } .map { "Weather in ${it.getLocation()} is ${it.getDescription()}" } .subscribe { logger.info(it) } triggers the processing of the chain
  44. 61 @RestController class ReactiveUserController(val repository: ReactiveUserRepository) { @GetMapping("/user/{id}") fun findOne(@PathVariable

    id: String) = repository.findOne(id) @GetMapping("/user") fun findAll() = repository.findAll() @PostMapping("/user") fun save(@RequestBody user: Mono<User>) = repository.save(user) } interface ReactiveUserRepository { fun findOne(id: String): Mono<User> fun findAll(): Flux<User> fun save(user: Mono<User>): Mono<Void> } Spring WebFlux annotation-based
  45. Step 1 Switch from Java to Kotlin Step 2 Upgrade

    to Spring 5 Step 3 Migrate to WebFlux Step 4 WebFlux/Bean Kotlin DSLs
  46. Step 3 Spring Boot 2 Spring WebFlux Step 4 Spring

    Boot 2 Spring WebFlux Functional Kotlin 1.1 Kotlin 1.1 63
  47. 64 @Controller, @RequestMapping Spring MVC Servlet API Servlet Container Servlet

    3.1, Netty, Undertow Spring WebFlux HTTP / Reactive Streams Router functions
  48. 66 // Annotation-based Java @RequestMapping("/quotes/feed", produces = TEXT_EVENT_STREAM_VALUE) public Flux<Quote>

    fetchQuotesStream() { ... } // Functional Java without static imports RouterFunctions.route( RequestPredicates.path("/quotes/feed") .and(RequestPredicates.accept(MediaType.TEXT_EVENT_STREAM)), { ... } )
  49. 67 // Annotation-based Java @RequestMapping("/quotes/feed", produces = TEXT_EVENT_STREAM_VALUE) public Flux<Quote>

    fetchQuotesStream() { ... } // Functional Java with static imports route( path("/quotes/feed").and(accept(TEXT_EVENT_STREAM)), { ... } )
  50. 68 // Annotation-based Java @RequestMapping("/quotes/feed", produces = TEXT_EVENT_STREAM_VALUE) public Flux<Quote>

    fetchQuotesStream() { ... } // Functional Kotlin router { "/quotes/feed" and accept(TEXT_EVENT_STREAM) { ... } }
  51. 69 @Configuration class ApplicationRoutes(val userHandler: UserHandler, val blogHandler: BlogHandler, val

    shopRepository: ShopRepository) { @Bean fun appRouter() = router { GET("/users", userHandler::fetchAll) GET("/users/{id}", userHandler::fetch) } @Bean fun nestedRouter() = router { ... } @Bean fun dynamicRouter() = router { ... } }
  52. 70 @Bean fun nestedRouter() = router { ("/blog" and accept(TEXT_HTML)).nest

    { GET("/", blogHandler::findAllView) GET("/{slug}", blogHandler::findOneView) } ("/api/blog" and accept(APPLICATION_JSON)).nest { GET("/", blogHandler::findAll) GET("/{id}", blogHandler::findOne) POST("/", blogHandler::create) } }
  53. @Bean fun dynamicRouter() = router { shopRepository.findAll() .toIterable() .forEach {

    shop -> GET("/${shop.id}") { req -> shopHandler.homepage(shop, req) } } } 71
  54. @Component class EventHandler(val repository: EventRepository) { fun findOne(req: ServerRequest) =

    ok().json().body(repository.findOne(req.pathVariable("id"))) fun findAll(req: ServerRequest) = ok().json().body(repository.findAll()) } 72
  55. @Component class NewsHandler { fun newsView(req: ServerRequest) = ok().render("news") fun

    newsSse(req: ServerRequest) = ok() .contentType(TEXT_EVENT_STREAM) .body(Flux.interval(ofMillis(100)).map { "Hello $it!" }) } 73
  56. ‣ Reactive and non-blocking ‣ Idiomatic Kotlin code ‣ Functional

    routing DSL ‣ Immutable domain model ‣ Balance between functional and OOP + annotation style ‣ Constructor based and non-intrusive dependency injection ‣ Efficient development mode 75 MiXiT project software design
  57. Step 3 Spring Boot 2 Spring WebFlux Step 4 Microframework

    style Spring WebFlux Functional Kotlin 1.1 Kotlin 1.1 77
  58. Choose the flavour you prefer! 78 Boot + WebFlux WebFlux

    standalone ➔ Automatic server configuration ➔ All Spring Boot goodness! ➔ Bean registration ◆ Annotation-based ◆ Optimized Classpath scanning ➔ MiXiT webapp startup: ◆ 3 seconds ◆ 20 Mbytes heap size after GC ◆ Works with -Xmx32m ➔ Manual server configuration ➔ Bean registration ◆ Functional and lambda-based ◆ No Cglib proxy ◆ No need for kotlin-spring plugin ➔ MiXiT webapp startup: ◆ 1.2 seconds ◆ 10 Mbytes heap size after GC ◆ Works with -Xmx32m
  59. ➔ After XML and JavaConfig, a third major way to

    register your beans ➔ In a nutshell: lambda with Supplier act as a FactoryBean ➔ Very efficient, no reflection, no CGLIB proxies involved 79 beans { bean<Foo>() profile(“profile”) { bean { Bar(it.ref<Foo>()) } bean { Baz(it.env[“baz.name”]) } } Functional bean registration DSL
  60. 80 beans { bean("messageSource") { ReloadableResourceBundleMessageSource().apply { setBasename("messages") setDefaultEncoding("UTF-8") }

    } bean { MustacheViewResolver().apply { setPrefix("classpath:/templates/") setSuffix(".mustache") setCompiler(Mustache.compiler().escapeHTML(false)) setModelCustomizer({ model, exchange -> customizeModel(model, exchange, it.ref<MessageSource>()) }) } } ... https://goo.gl/iGwzw3 See functional-bean-registration MiXiT branch
  61. Kotlin 1.1 supports compiling to JavaScript ➔ Good JavaScript interop

    ➔ Could allow to: ◆ use a single language for your webapp ◆ share code between backend and frontend ➔ Before Kotlin 1.1.3, generates big JavaScript files ➔ Kotlin 1.1.4 will introduce a great Dead Code Elimination plugin! ◆ Hello world = 65 Kb Could replace JavaScript / TypeScript for your frontend code 82
  62. 83 A unique opportunity to build an open and cross-platform

    native application ecosystem! WebAssembly Read “An Abridged Cartoon Introduction To WebAssembly” by Lin Clark for more details https://goo.gl/I0kQsC
  63. Compiling non-JS based languages to JS is and will remains

    a hack WebAssembly compilation target instead of JavaScript? ➔ WebAssembly is currently mostly C/C++ oriented ➔ But incoming features may change that: ◆ DOM and Web API available directly from WASM ◆ Built-in garbage collector ◆ Exception Handling ➔ Kotlin could support WebAssembly via Kotlin Native (LLVM) ➔ You could leverage that to provide smaller and faster frontend code ➔ Fallback via asm.js 84
  64. What can you expect? ➔ Spring + Kotlin guides ➔

    Spring Framework 5 GA in September ➔ Spring Boot 2 GA in november ➔ Experiments on 2 important topics ◆ Kotlin frontend (JS and maybe WebAssembly ...) ◆ Coroutine based API for WebFlux (SPR-15413) 85