├── .gitignore ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── kotlin │ └── net │ │ └── amarszalek │ │ └── reactivekotlin │ │ ├── BooksHandler.kt │ │ ├── ReactivekotlinApplication.kt │ │ └── Routing.kt └── resources │ └── application.properties └── test └── kotlin └── net └── amarszalek └── reactivekotlin ├── ReactivekotlinApplicationTests.kt └── TestBase.kt /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | .sts4-cache 12 | 13 | ### IntelliJ IDEA ### 14 | .idea 15 | *.iws 16 | *.iml 17 | *.ipr 18 | 19 | ### NetBeans ### 20 | nbproject/private/ 21 | build/ 22 | nbbuild/ 23 | dist/ 24 | nbdist/ 25 | .nb-gradle/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Adrian Marszałek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reactive Kotlin + Spring Boot 2 2 | 3 | Reactive application built using Kotlin and Spring WebFlux. 4 | This is just a showcase project for demonstration of reactive possibilities and Server-Sent Events in Kotlin together with Spring Boot. 5 | 6 | 7 | To learn more, visit this [blog post](http://amarszalek.net/blog/2018/04/02/reactive-web-services-kotlin-spring-boot-2/) 8 | ## Running 9 | 10 | To run this application, simply execute: 11 | 12 | ```shell 13 | mvn spring-boot:run 14 | ``` 15 | Remember that you should have a MongoDB server running on host and port. 16 | 17 | ## Endpoints 18 | 19 | After running the application, the public instance should be available at http://localhost:8080 20 | As for now, it offers the following endpoints: 21 | * `GET /books/{title}` returns a book with given title 22 | * `POST /books` with a request body containg title and author will save a new book to the storage 23 | * `GET /books` will give you all the books in the storage using Server-Sent Event Stream 24 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | net.amarszalek 7 | reactivekotlin 8 | 0.0.1-SNAPSHOT 9 | jar 10 | 11 | reactivekotlin 12 | Demo project for Spring Boot 13 | 14 | 15 | org.springframework.boot 16 | spring-boot-starter-parent 17 | 2.0.0.RELEASE 18 | 19 | 20 | 21 | 22 | UTF-8 23 | UTF-8 24 | 1.8 25 | 1.2.31 26 | 27 | 28 | 29 | 30 | org.springframework.boot 31 | spring-boot-starter-data-mongodb-reactive 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-webflux 36 | 37 | 38 | com.fasterxml.jackson.module 39 | jackson-module-kotlin 40 | 41 | 42 | org.jetbrains.kotlin 43 | kotlin-stdlib-jdk8 44 | 45 | 46 | org.jetbrains.kotlin 47 | kotlin-reflect 48 | 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-test 53 | test 54 | 55 | 56 | io.projectreactor 57 | reactor-test 58 | test 59 | 60 | 61 | org.junit.jupiter 62 | junit-jupiter-api 63 | test 64 | 65 | 66 | 67 | 68 | ${project.basedir}/src/main/kotlin 69 | ${project.basedir}/src/test/kotlin 70 | 71 | 72 | org.springframework.boot 73 | spring-boot-maven-plugin 74 | 75 | 76 | kotlin-maven-plugin 77 | org.jetbrains.kotlin 78 | 79 | 80 | -Xjsr305=strict 81 | 82 | 83 | spring 84 | 85 | 86 | 87 | 88 | org.jetbrains.kotlin 89 | kotlin-maven-allopen 90 | ${kotlin.version} 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /src/main/kotlin/net/amarszalek/reactivekotlin/BooksHandler.kt: -------------------------------------------------------------------------------- 1 | package net.amarszalek.reactivekotlin 2 | 3 | import org.springframework.data.annotation.Id 4 | import org.springframework.data.mongodb.core.mapping.Document 5 | import org.springframework.data.repository.reactive.ReactiveCrudRepository 6 | import org.springframework.stereotype.Component 7 | import org.springframework.stereotype.Repository 8 | import org.springframework.web.reactive.function.server.* 9 | import org.springframework.web.reactive.function.server.ServerResponse.ok 10 | import reactor.core.publisher.Flux 11 | import reactor.core.publisher.Mono 12 | import reactor.core.publisher.toMono 13 | import java.time.Duration 14 | 15 | 16 | @Document 17 | data class Book(@Id val id: String? = null, 18 | val title: String, 19 | val author: String) 20 | 21 | @Component 22 | class BooksHandler(private val repository: BookRepository) { 23 | 24 | fun getAll(request: ServerRequest): Mono { 25 | val interval = Flux.interval(Duration.ofSeconds(1)) 26 | 27 | val books = repository.findAll() 28 | return ok().bodyToServerSentEvents(Flux.zip(interval, books).map({ it.t2 })) 29 | } 30 | 31 | fun getBook(request: ServerRequest): Mono { 32 | val title = request.pathVariable("title") 33 | 34 | return ok().body(repository.findByTitle(title)) 35 | } 36 | 37 | fun addBook(request: ServerRequest): Mono { 38 | val book = request.bodyToMono() 39 | 40 | return ok().body(repository.saveAll(book).toMono()) 41 | } 42 | } 43 | 44 | @Repository 45 | interface BookRepository : ReactiveCrudRepository { 46 | fun findByTitle(name: String): Mono 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/amarszalek/reactivekotlin/ReactivekotlinApplication.kt: -------------------------------------------------------------------------------- 1 | package net.amarszalek.reactivekotlin 2 | 3 | import org.springframework.boot.autoconfigure.SpringBootApplication 4 | import org.springframework.boot.runApplication 5 | 6 | @SpringBootApplication 7 | class ReactivekotlinApplication 8 | 9 | fun main(args: Array) { 10 | runApplication(*args) 11 | } -------------------------------------------------------------------------------- /src/main/kotlin/net/amarszalek/reactivekotlin/Routing.kt: -------------------------------------------------------------------------------- 1 | package net.amarszalek.reactivekotlin 2 | 3 | import org.springframework.context.annotation.Bean 4 | import org.springframework.context.annotation.Configuration 5 | import org.springframework.http.MediaType 6 | import org.springframework.web.reactive.function.server.router 7 | 8 | @Configuration 9 | class Routing { 10 | 11 | @Bean 12 | fun booksRouter(handler: BooksHandler) = router { 13 | ("/books" and accept(MediaType.APPLICATION_JSON)).nest { 14 | GET("/", handler::getAll) 15 | GET("/{title}", handler::getBook) 16 | POST("/", handler::addBook) 17 | } 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adikm/reactive-kotlin-spring-boot/bd3de9d269e36b60c772b15f908e44d4aab126b0/src/main/resources/application.properties -------------------------------------------------------------------------------- /src/test/kotlin/net/amarszalek/reactivekotlin/ReactivekotlinApplicationTests.kt: -------------------------------------------------------------------------------- 1 | package net.amarszalek.reactivekotlin 2 | 3 | import org.junit.Test 4 | import org.springframework.beans.factory.annotation.Autowired 5 | 6 | /** 7 | * If you want to read my story about writing integration tests for WebFlux in Kotlin 8 | * visit: https://amarszalek.net/blog/2018/04/11/rant-integration-tests-spring-webflux-kotlin/ 9 | */ 10 | class ReactivekotlinApplicationTests : TestBase() { 11 | 12 | @Autowired 13 | private lateinit var repository: BookRepository 14 | 15 | @Test 16 | fun `Get book by Title`() { 17 | val bookTitle = "Title5" 18 | repository.save(Book(title = bookTitle, author = "Author")) 19 | 20 | client.get().uri("/books/{title}", bookTitle) 21 | .exchange().expectStatus().isOk 22 | .expectBody() 23 | .jsonPath("$.title").isEqualTo(bookTitle) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/kotlin/net/amarszalek/reactivekotlin/TestBase.kt: -------------------------------------------------------------------------------- 1 | package net.amarszalek.reactivekotlin 2 | 3 | import org.junit.jupiter.api.* 4 | import org.junit.jupiter.api.extension.ExtendWith 5 | import org.springframework.boot.runApplication 6 | import org.springframework.boot.test.context.SpringBootTest 7 | import org.springframework.http.* 8 | import org.springframework.test.context.junit.jupiter.SpringExtension 9 | import org.springframework.test.web.reactive.server.WebTestClient 10 | 11 | /** 12 | * If you want to read my story about writing integration tests for WebFlux in Kotlin 13 | * visit: https://amarszalek.net/blog/2018/04/11/rant-integration-tests-spring-webflux-kotlin/ 14 | */ 15 | @TestInstance(TestInstance.Lifecycle.PER_CLASS) 16 | @SpringBootTest 17 | @ExtendWith(SpringExtension::class) 18 | class TestBase { 19 | 20 | protected val client = WebTestClient.bindToServer() 21 | .baseUrl("http://127.0.0.1:8080") 22 | .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE) 23 | .build() 24 | 25 | @BeforeAll 26 | fun initApplication() { 27 | runApplication() 28 | } 29 | 30 | } 31 | --------------------------------------------------------------------------------