├── .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 |
--------------------------------------------------------------------------------