Why Spring loves Kotlin

Why Spring loves Kotlin

Slides of my Devoxx 2017 & SpringOne Platform talk, video available at https://www.youtube.com/watch?v=kbMXAjWEft0. Let's discovers Spring official Kotlin support with a journey from Spring Boot 1 with Java to Spring Boot 2 with Kotlin.

5778521f67d80de0ee3b213e4f159a59?s=128

Sébastien Deleuze

November 08, 2017
Tweet

Transcript

  1. Why Spring Kotlin Sébastien Deleuze @sdeleuze

  2. 2 Most popular way to build web applications + Spring

    Boot
  3. 3 Let’s see how far we can go with ...

    Kotlin + Spring Boot
  4. 4

  5. 5 Migrating a typical Boot application to Kotlin https://github.com/sdeleuze/spring-kotlin-deepdive

  6. 6 Step 1 Kotlin

  7. 7 Step 2 Spring Boot 1 Spring Boot 2 based

    on Spring Framework 5
  8. 8 Step 3 Spring MVC Spring WebFlux

  9. 9 Step 4 Spring WebFlux Functional API & Kotlin DSL

    Spring WebFlux @nnotations
  10. From Java to Kotlin Step 1 Kotlin Step 2 Boot

    2 Step 3 WebFlux Step 4 Kotlin DSL & Functional API
  11. 11 https://start.spring.io/#!language=kotlin

  12. 12 Domain model @Document data class Article( @Id val slug:

    String, val title: String, val headline: String, val content: String, @DBRef val author: User, val addedAt: LocalDateTime = now()) @Document data class User( @Id val login: String, val firstname: String, val lastname: String, val description: String? = null) @Document public class Article { @Id private String slug; private String title; private LocalDateTime addedAt; private String headline; private String content; @DBRef private User author; public Article() { } public Article(String slug, String title, String headline, String content, User author) { this(slug, title, headline, content, author, LocalDateTime.now()); } public Article(String slug, String title, String headline, String content, User author, LocalDateTime addedAt) { this.slug = slug; this.title = title; this.addedAt = addedAt; this.headline = headline; this.content = content; this.author = author; } public String getSlug() { return slug; } public void setSlug(String slug) { this.slug = slug; } public String getTitle() { return title; }
  13. 13 Expressive test function names with backticks class EmojTests {

    @Test fun `Why Spring ❤ Kotlin?`() { println("Because I can use emoj in function names \uD83D\uDE09") } } > Because I can use emoj in function names
  14. 14 @RestController public class UserController { private final UserRepository userRepository;

    public UserController(UserRepository userRepository) { this.userRepository = userRepository; } @GetMapping("/user/{login}") public User findOne(@PathVariable String login) { return userRepository.findOne(login); } @GetMapping("/user") public Iterable<User> findAll() { return userRepository.findAll(); } @PostMapping("/user") public User save(@RequestBody User user) { return userRepository.save(user); } } Spring MVC controller written in Java
  15. 15 @RestController class UserController(private 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) } Spring MVC controller written in Kotlin
  16. 16 Inferred type hints in IDEA Settings Editor General Appearance

    Show parameter name hints Select Kotlin Check “Show function/property/local value return type hints”
  17. kotlin-spring compiler plugin Automatically open Spring annotated classes and methods

    @SpringBootApplication open class Application { @Bean open fun foo() = Foo() @Bean open fun bar(foo: Foo) = Bar(foo) } @SpringBootApplication class Application { @Bean fun foo() = Foo() @Bean fun bar(foo: Foo) = Bar(foo) } Without kotlin-spring plugin With kotlin-spring plugin 17
  18. From Java to Kotlin Step 1 Kotlin Step 2 Boot

    2 Step 3 WebFlux Step 4 Kotlin DSL & Functional API
  19. Spring Kotlin and officially supports it Spring Framework 5 Spring

    Boot 2.0 (M7 available, GA early 2018) Reactor Core 3.1 Spring Data Kay
  20. 20 Kotlin support out of the box

  21. 21 Kotlin support documentation https://goo.gl/uwyjQn

  22. Running Spring Boot 1 application with Kotlin 22 @SpringBootApplication class

    Application fun main(args: Array<String>) { SpringApplication.run(Application::class.java, *args) }
  23. Running Spring Boot 2 application with Kotlin 23 @SpringBootApplication class

    Application fun main(args: Array<String>) { runApplication<FooApplication>(*args) }
  24. Declaring additional beans 24 @SpringBootApplication class Application { @Bean fun

    foo() = Foo() @Bean fun bar(foo: Foo) = Bar(foo) } fun main(args: Array<String>) { runApplication<FooApplication>(*args) }
  25. Customizing SpringApplication 25 @SpringBootApplication class Application { @Bean fun foo()

    = Foo() @Bean fun bar(foo: Foo) = Bar(foo) } fun main(args: Array<String>) { runApplication<FooApplication>(*args) { setBannerMode(OFF) } }
  26. Array-like Kotlin extension for Model operator fun Model.set(attributeName: String, attributeValue:

    Any) { this.addAttribute(attributeName, attributeValue) } @GetMapping("/") public String blog(Model model) { model.addAttribute("title", "Blog"); model.addAttribute("articles", repository.findAll()); return "blog"; } @GetMapping("/") fun blog(model: Model): String { model["title"] = "Blog" model["articles"] = repository.findAll() return "blog" } 26
  27. Reified type parameters Kotlin extension inline fun <reified T: Any>

    RestOperations.exchange(url: String, method: HttpMethod, requestEntity: HttpEntity<*>? = null) = exchange(url, method, requestEntity, object : ParameterizedTypeReference<T>() {}) List<Article> list = restTemplate .exchange("/api/article/", GET, null, new ParameterizedTypeReference<List<Article>>(){}) .getBody(); val list: List<Article> = restTemplate .exchange("/api/article/", GET) .getBody(); Goodbye type erasure, we are not going to miss you at all! 27
  28. Null safety 28 Nothing enforced at type system No check

    at compile time NullPointerException at runtime Optional only usable for return values Default is non-null, ? suffix for nullable types Full check at compile time No NullPointerException !!! Functional constructs on nullable values
  29. By default, Kotlin interprets Java types as platform types (unknown

    nullability) Null safety of Spring APIs public interface RestOperations { URI postForLocation(String url, Object request, Object ... uriVariables) } 29 postForLocation(url: String!, request: Any!, varags uriVariables: Any!): URI!
  30. Nullability annotations meta annotated with JSR 305 for generic tooling

    support Null safety of Spring APIs @NonNullApi package org.springframework.web.client; public interface RestOperations { @Nullable URI postForLocation(String url, @Nullable Object request, Object... uriVariables) } 30 postForLocation(url: String, request: Any?, varargs uriVariables: Any): URI? freeCompilerArgs = ["-Xjsr305=strict"] +
  31. 31 @Controller // Mandatory Optional class FooController(val foo: Foo, val

    bar: Bar?) { @GetMapping("/") // Equivalent to @RequestParam(required=false) fun foo(@RequestParam baz: String?) = ... } Leveraging Kotlin nullable information To determine @RequestParam or @Autowired required attribute
  32. @ConfigurationProperties @ConfigurationProperties("foo") data class FooProperties( var baseUri: String? = null,

    var admin = Credentials()) { data class Credential( var username: String? = null, var password: String? = null) } One of the remaining pain points in Kotlin
  33. 33 JUnit 5 supports non-static @BeforeAll @AfterAll class IntegrationTests {

    private val application = Application(8181) private val client = WebClient.create("http://localhost:8181") @BeforeAll fun beforeAll() { application.start() } @Test fun test1() { // ... } @Test fun test2() { // ... } @AfterAll fun afterAll() { application.stop() } } With “per class” lifecycle defined via junit-platform.properties or @TestInstance
  34. 34 JUnit 5 supports constructor based injection @ExtendWith(SpringExtension::class) @SpringBootTest(webEnvironment =

    RANDOM_PORT) class HtmlTests(@Autowired val restTemplate: TestRestTemplate) { @Test fun test1() { // ... } @Test fun test2() { // ... } } Allows to use val instead of lateinit var in tests
  35. 35 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
  36. From Java to Kotlin Step 1 Kotlin Step 2 Boot

    2 Step 3 WebFlux Step 4 Kotlin DSL & Functional API
  37. Spring Framework 5 introduces a new web stack Spring MVC

    Blocking Servlet Spring WebFlux Non-blocking Reactive Streams
  38. 38 ∞ Streaming Scalability Latency ∞

  39. Servlet Reactive Functional endpoint @Controller Reactive client @Controller RestTemplate Netty

    Undertow Tomcat Jetty Servlet 3.1 container Tomcat Jetty Servlet container
  40. 40 RxJava ? WebFlux supports various non-blocking API CompletableFuture Flow.Publisher

    Reactor Akka Reactive Streams
  41. 41 Let’s focus on Reactor for now RxJava CompletableFuture Flow.Publisher

    Reactor Akka Reactive Streams ?
  42. 42 Mono is a reactive type for 0..1 element

  43. 43 Flux is for reactive collection and stream

  44. 44 @RestController class ReactiveUserController(val repository: ReactiveUserRepository) { @GetMapping("/user/{id}") fun findOne(@PathVariable

    id: String): Mono<User> = repository.findOne(id) @GetMapping("/user") fun findAll(): Flux<User> = repository.findAll() @PostMapping("/user") fun save(@RequestBody user: Mono<User>): Mono<Void> = repository.save(user) } interface ReactiveUserRepository { fun findOne(id: String): Mono<User> fun findAll(): Flux<User> fun save(user: Mono<User>): Mono<Void> } Spring WebFlux with annotations Spring Data Kay provides Reactive support for MongoDB, Redis, Cassandra and Couchbase
  45. 45 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 = functional programming
  46. Reactor Kotlin extensions Java Kotlin with extensions Mono.just("foo") "foo".toMono() Flux.fromIterable(list)

    list.toFlux() Mono.error(new RuntimeException()) RuntimeException().toMono() flux.ofType(User.class) flux.ofType<User>() StepVerifier.create(flux).verifyComplete() flux.test().verifyComplete() MathFlux.averageDouble(flux) flux.average()
  47. From Java to Kotlin Step 1 Kotlin Step 2 Boot

    2 Step 3 WebFlux Step 4 Kotlin DSL & Functional API
  48. 48 val router = router { val users = …

    accept(TEXT_HTML).nest { "/" { ok().render("index") } "/sse" { ok().render("sse") } "/users" { ok().render("users", mapOf("users" to users.map { it.toDto() })) } } ("/api/users" and accept(APPLICATION_JSON)) { ok().body(users) } ("/api/users" and accept(TEXT_EVENT_STREAM)) { ok().bodyToServerSentEvents(users.repeat().delayElements(ofMillis(100))) } } WebFlux functional API with Kotlin DSL
  49. 49 @SpringBootApplication class Application { @Bean fun router(htmlHandler: HtmlHandler, userHandler:

    UserHandler, articleHandler: ArticleHandler) = router { accept(APPLICATION_JSON).nest { "/api/user".nest { GET("/", userHandler::findAll) GET("/{login}", userHandler::findOne) } "/api/article".nest { GET("/", articleHandler::findAll) GET("/{slug}", articleHandler::findOne) POST("/", articleHandler::save) DELETE("/{slug}", articleHandler::delete) } } (GET("/api/article/notifications") and accept(TEXT_EVENT_STREAM)).invoke(articleHandler::notifications) accept(TEXT_HTML).nest { GET("/", htmlHandler::blog) (GET("/{slug}") and !GET("/favicon.ico")).invoke(htmlHandler::article) } } } Functional router within Boot
  50. @Component class HtmlHandler(private val userRepository: UserRepository, private val converter: MarkdownConverter)

    { fun blog(req: ServerRequest) = ok().render("blog", mapOf( "title" to "Blog", "articles" to articleRepository.findAll() .flatMap { it.toDto(userRepository, converter) } )) } @Component class ArticleHandler(private val articleRepository: ArticleRepository, private val articleEventRepository: ArticleEventRepository) { fun findAll(req: ServerRequest) = ok().body(articleRepository.findAll()) fun notifications(req: ServerRequest) = ok().bodyToServerSentEvents(articleEventRepository.findWithTailableCursorBy()) } 50 Functional handlers within Boot
  51. Under construction ... Fixing remaining pain points Coroutines Functional bean

    definition Multi-platform
  52. Immutable non-nullable @ConfigurationProperties @ConfigurationProperties("foo") data class FooProperties( val baseUri: String,

    val admin: Credential) { data class Credential( val username: String, val password: String) } Boot 2.1 ? See issue #8762 for more details @ConfigurationProperties("foo") data class FooProperties( lateinit var baseUri: String, var admin = Credentials()) { data class Credential( lateinit var username: String, lateinit var password: String) } Currently N ot yet available
  53. WebTestClient Unusable in Kotlin due to a type inference issue,

    see KT-5464 otlin 1.3 For now, please use: - WebClient in Kotlin - WebTestClient in Java
  54. Under construction ... Fixing remaining pain points Coroutines Boot +

    functional bean DSL Multi-platform
  55. 55 Kotlin Coroutines RxJava CompletableFuture Flow.Publisher Reactor Akka Reactive Streams

    Coroutines Experimental
  56. Spring & Kotlin Coroutines 56 • Coroutines are Kotlin lightweight

    threads • Allows non-blocking imperative code • Less powerful than Reactive API (backpressure, streaming, operators, etc.) • kotlinx.coroutines: Reactive Streams and Reactor support • spring-kotlin-coroutine: experimental Spring support (community driven) • Warning ◦ Coroutine are still experimental ◦ No official Spring support yet, see SPR-15413 ◦ Ongoing evaluation of performances and back-pressure interoperability Experim ental https://github.com/konrad-kaminski/spring-kotlin-coroutine
  57. Reactive Coroutines interop 57 Operation Reactive Coroutines Async value fun

    foo(): Mono<T> suspend fun foo(): T? Async collection fun foo(): Mono<List<T>> suspend fun foo(): List<T> Stream fun foo(): Flux<T> suspend fun foo(): ReceiveChannel<T> Async completion fun foo(): Mono<Void> suspend fun foo()
  58. 58 @RestController class CoroutineUserController(val repository: CoroutineUserRepository) { @GetMapping("/user/{id}") suspend fun

    findOne(@PathVariable id: String): User = repository.findOne(id) @GetMapping("/user") suspend fun findAll(): List<User> = repository.findAll() @PostMapping("/user") suspend fun save(@RequestBody user: User) = repository.save(user) } interface CoroutineUserRepository { suspend fun findOne(id: String): User suspend fun findAll(): List<User> suspend fun save(user: User) } Spring WebFlux with Coroutines Experim ental https://github.com/sdeleuze/spring-kotlin-deepdive/tree/step3-coroutine
  59. Under construction ... Fixing remaining pain points Coroutines Boot +

    functional bean DSL Multi-platform
  60. Spring Framework 5 introduces Functional bean definition Very efficient, no

    reflection, no CGLIB proxy Lambdas instead of annotations Both declarative and programmatic
  61. 61 Functional bean definition Kotlin DSL val databaseContext = beans

    { bean<ArticleEventListener>() bean<ArticleEventRepository>() bean<ArticleRepository>() bean<UserRepository>() environment( { !activeProfiles.contains("cloud") } ) { bean { InitializingBean { initializeDatabase(ref(), ref(), ref()) } } } } fun initializeDatabase(ops: MongoOperations, userRepository: UserRepository, articleRepository: ArticleRepository) { // ... }
  62. 62 Beans + router DSL fit well together val context

    = beans { bean { val userHandler = ref<UserHandler>() router { accept(APPLICATION_JSON).nest { "/api/user".nest { GET("/", userHandler::findAll) GET("/{login}", userHandler::findOne) } } } bean { Mustache.compiler().escapeHTML(false).withLoader(ref()) } bean<HtmlHandler>() bean<ArticleHandler>() bean<UserHandler>() bean<MarkdownConverter>() }
  63. 63 Bean DSL is extensible and composable ! webfluxApplication(Server.NETTY) {

    // or TOMCAT // group routers routes { router { routerApi(ref()) } router(routerStatic()) } router { routerHtml(ref(), ref()) } // group beans beans { bean<UserHandler>() bean<Baz>() // Primary constructor injection } bean<Bar>() mustacheTemplate() profile("foo") { bean<Foo>() } } See https://github.com/tgirard12/spring-webflux-kotlin-dsl POC
  64. Functional bean definition with Spring Boot 64 @SpringBootApplication class Application

    fun main(args: Array<String>) { runApplication<FooApplication>(*args) { addInitializers(databaseContext, webContext) } } This nice syntax currently works only for running the application, not tests
  65. Functional bean definition with Spring Boot 65 class ContextInitializer :

    ApplicationContextInitializer<GenericApplicationContext> { override fun initialize(context: GenericApplicationContext) { databaseContext.initialize(context) webContext.initialize(context) } } context.initializer.classes=io.spring.deepdive.ContextInitializer This more verbose one works for running both application and tests
  66. Functional bean definition with Spring Boot 66 Come discussing next

    steps with us
  67. Under construction ... Fixing remaining pain points Coroutines Boot +

    functional bean DSL Multi-platform
  68. Kotlin is multi-platform Kotlin/JVM: JVM and Android Kotlin/JS: transpile to

    JavaScript Kotlin/Native: run without any VM (LLVM toolchain)
  69. 69 Kotlin for frontend today with Kotlin/JS

  70. 70 Original JavaScript code if (Notification.permission === "granted") { Notification.requestPermission().then(function(result)

    { console.log(result); }); } let eventSource = new EventSource("/api/article/notifications"); eventSource.addEventListener("message", function(e) { let article = JSON.parse(e.data); let notification = new Notification(article.title); notification.onclick = function() { window.location.href = "/" + article.slug; }; });
  71. 71 Kotlin to Javascript data class Article(val slug: String, val

    title: String) fun main(args: Array<String>) { if (Notification.permission == NotificationPermission.GRANTED) { Notification.requestPermission().then { console.log(it) } } EventSource("/api/article/notifications").addEventListener("message", { val article = JSON.parse<Article>(it.data()); Notification(article.title).addEventListener("click", { window.location.href = "/${article.slug}" }) }) } fun Event.data() = (this as MessageEvent).data as String // See KT-20743 Type safety, null safety, only 10 Kbytes with Dead Code Elimination tool
  72. 72 Kotlin for frontend tomorrow with WebAssembly Read “An Abridged

    Cartoon Introduction To WebAssembly” by Lin Clark for more details https://goo.gl/I0kQsC A sandboxed native platform for the Web (W3C, no plugin involved)
  73. 73 Compiling Kotlin to WebAssembly + ➔ Kotlin supports WebAssembly

    via Kotlin/Native ➔ Better compilation target than JS: bytecode, performance, memory ➔ DOM API and GC are coming ... ➔ A Kotlin/Native Frontend ecosystem could arise Experim ental
  74. Kotlin 1.2 allows sharing code between platforms Multi-platform data types

    and serialization are coming
  75. Thanks! https://speakerdeck.com/sdeleuze/why-spring-loves-kotlin Follow me on @sdeleuze for fresh Spring +

    Kotlin news https://github.com/mixitconf/mixit https://github.com/sdeleuze/spring-kotlin-fullstack https://github.com/sdeleuze/spring-kotlin-deepdive https://github.com/sdeleuze/spring-kotlin-functional