BAEL-2378 Non-blocking Spring Boot with Kotlin Coroutines (#6966)
This commit is contained in:
parent
7909ec9081
commit
328a0d1e47
1
pom.xml
1
pom.xml
|
@ -641,6 +641,7 @@
|
||||||
<!-- <module>spring-boot-gradle</module> --> <!-- Not a maven project -->
|
<!-- <module>spring-boot-gradle</module> --> <!-- Not a maven project -->
|
||||||
<module>spring-boot-jasypt</module>
|
<module>spring-boot-jasypt</module>
|
||||||
<module>spring-boot-keycloak</module>
|
<module>spring-boot-keycloak</module>
|
||||||
|
<module>spring-boot-kotlin</module>
|
||||||
<module>spring-boot-logging-log4j2</module>
|
<module>spring-boot-logging-log4j2</module>
|
||||||
<module>spring-boot-mvc</module>
|
<module>spring-boot-mvc</module>
|
||||||
<module>spring-boot-mvc-birt</module>
|
<module>spring-boot-mvc-birt</module>
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
### Relevant Articles:
|
||||||
|
- [Non-blocking Spring Boot with Kotlin Coroutines](http://www.baeldung.com/non-blocking-spring-boot-with-kotlin-coroutines)
|
|
@ -0,0 +1,166 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<artifactId>spring-boot-kotlin</artifactId>
|
||||||
|
<name>spring-boot-kotlin</name>
|
||||||
|
<packaging>jar</packaging>
|
||||||
|
<description>Demo project showing how to use non-blocking in Kotlin with Spring Boot</description>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<artifactId>parent-kotlin</artifactId>
|
||||||
|
<groupId>com.baeldung</groupId>
|
||||||
|
<version>1.0.0-SNAPSHOT</version>
|
||||||
|
<relativePath>../parent-kotlin</relativePath>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>spring-snapshots</id>
|
||||||
|
<name>Spring Snapshots</name>
|
||||||
|
<url>https://repo.spring.io/snapshot</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>spring-milestones</id>
|
||||||
|
<name>Spring Milestones</name>
|
||||||
|
<url>https://repo.spring.io/milestone</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-dependencies</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
<type>pom</type>
|
||||||
|
<scope>import</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<pluginRepositories>
|
||||||
|
<pluginRepository>
|
||||||
|
<id>spring-snapshots</id>
|
||||||
|
<name>Spring Snapshots</name>
|
||||||
|
<url>https://repo.spring.io/snapshot</url>
|
||||||
|
<snapshots>
|
||||||
|
<enabled>true</enabled>
|
||||||
|
</snapshots>
|
||||||
|
</pluginRepository>
|
||||||
|
<pluginRepository>
|
||||||
|
<id>spring-milestones</id>
|
||||||
|
<name>Spring Milestones</name>
|
||||||
|
<url>https://repo.spring.io/milestone</url>
|
||||||
|
</pluginRepository>
|
||||||
|
</pluginRepositories>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-reflect</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlinx</groupId>
|
||||||
|
<artifactId>kotlinx-coroutines-core</artifactId>
|
||||||
|
<version>${kotlinx-coroutines.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlinx</groupId>
|
||||||
|
<artifactId>kotlinx-coroutines-reactor</artifactId>
|
||||||
|
<version>${kotlinx-coroutines.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
<version>${spring-boot.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.module</groupId>
|
||||||
|
<artifactId>jackson-module-kotlin</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.data</groupId>
|
||||||
|
<artifactId>spring-data-r2dbc</artifactId>
|
||||||
|
<version>${r2dbc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.r2dbc</groupId>
|
||||||
|
<artifactId>r2dbc-h2</artifactId>
|
||||||
|
<version>${h2-r2dbc.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.r2dbc</groupId>
|
||||||
|
<artifactId>r2dbc-spi</artifactId>
|
||||||
|
<version>${r2dbc-spi.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-api</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-engine</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.projectreactor</groupId>
|
||||||
|
<artifactId>reactor-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<args>
|
||||||
|
<arg>-Xjsr305=strict</arg>
|
||||||
|
</args>
|
||||||
|
<compilerPlugins>
|
||||||
|
<plugin>spring</plugin>
|
||||||
|
</compilerPlugins>
|
||||||
|
</configuration>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-allopen</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<kotlin.version>1.3.31</kotlin.version>
|
||||||
|
<r2dbc.version>1.0.0.M1</r2dbc.version>
|
||||||
|
<r2dbc-spi.version>1.0.0.M7</r2dbc-spi.version>
|
||||||
|
<h2-r2dbc.version>1.0.0.BUILD-SNAPSHOT</h2-r2dbc.version>
|
||||||
|
<kotlinx-coroutines.version>1.2.1</kotlinx-coroutines.version>
|
||||||
|
<spring-boot.version>2.2.0.M2</spring-boot.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,11 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines
|
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication.run
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
class SpringApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
run(SpringApplication::class.java, *args)
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.config
|
||||||
|
|
||||||
|
import io.r2dbc.h2.H2ConnectionConfiguration
|
||||||
|
import io.r2dbc.h2.H2ConnectionFactory
|
||||||
|
import io.r2dbc.spi.ConnectionFactory
|
||||||
|
import org.springframework.beans.factory.annotation.Value
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration
|
||||||
|
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableR2dbcRepositories
|
||||||
|
class DatastoreConfig : AbstractR2dbcConfiguration() {
|
||||||
|
@Value("\${spring.datasource.username}")
|
||||||
|
private val userName: String = ""
|
||||||
|
|
||||||
|
@Value("\${spring.datasource.password}")
|
||||||
|
private val password: String = ""
|
||||||
|
|
||||||
|
@Value("\${spring.datasource.dbname}")
|
||||||
|
private val dbName: String = ""
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
override fun connectionFactory(): ConnectionFactory {
|
||||||
|
return H2ConnectionFactory(H2ConnectionConfiguration.builder()
|
||||||
|
.inMemory(dbName)
|
||||||
|
.username(userName)
|
||||||
|
.password(password)
|
||||||
|
.build())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.config
|
||||||
|
|
||||||
|
import com.baeldung.nonblockingcoroutines.handlers.ProductsHandler
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.web.reactive.function.server.coRouter
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class RouterConfiguration {
|
||||||
|
|
||||||
|
@FlowPreview
|
||||||
|
@Bean
|
||||||
|
fun productRoutes(productsHandler: ProductsHandler) = coRouter {
|
||||||
|
GET("/", productsHandler::findAll)
|
||||||
|
GET("/{id}", productsHandler::findOne)
|
||||||
|
GET("/{id}/stock", productsHandler::findOneInStock)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.config
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class WebClientConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
fun webClient() = WebClient.builder().baseUrl("http://localhost:8080").build()
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.controller
|
||||||
|
|
||||||
|
import com.baeldung.nonblockingcoroutines.model.Product
|
||||||
|
import com.baeldung.nonblockingcoroutines.repository.ProductRepository
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import org.springframework.web.reactive.function.client.bodyToMono
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
class ProductController {
|
||||||
|
@Autowired
|
||||||
|
lateinit var webClient: WebClient
|
||||||
|
@Autowired
|
||||||
|
lateinit var productRepository: ProductRepository
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
fun findOne(@PathVariable id: Int): Mono<Product> {
|
||||||
|
return productRepository
|
||||||
|
.getProductById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/stock")
|
||||||
|
fun findOneInStock(@PathVariable id: Int): Mono<ProductStockView> {
|
||||||
|
val product = productRepository.getProductById(id)
|
||||||
|
|
||||||
|
val stockQuantity = webClient.get()
|
||||||
|
.uri("/stock-service/product/$id/quantity")
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.retrieve()
|
||||||
|
.bodyToMono<Int>()
|
||||||
|
return product.zipWith(stockQuantity) { productInStock, stockQty ->
|
||||||
|
ProductStockView(productInStock, stockQty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/stock-service/product/{id}/quantity")
|
||||||
|
fun getStockQuantity(): Mono<Int> {
|
||||||
|
return Mono.just(2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
fun findAll(): Flux<Product> {
|
||||||
|
return productRepository.getAllProducts()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.controller
|
||||||
|
|
||||||
|
import com.baeldung.nonblockingcoroutines.model.Product
|
||||||
|
import com.baeldung.nonblockingcoroutines.repository.ProductRepositoryCoroutines
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.http.MediaType.APPLICATION_JSON
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import org.springframework.web.reactive.function.client.awaitBody
|
||||||
|
import org.springframework.web.reactive.function.client.awaitExchange
|
||||||
|
|
||||||
|
class ProductControllerCoroutines {
|
||||||
|
@Autowired
|
||||||
|
lateinit var webClient: WebClient
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
lateinit var productRepository: ProductRepositoryCoroutines
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
suspend fun findOne(@PathVariable id: Int): Product? {
|
||||||
|
return productRepository.getProductById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/stock")
|
||||||
|
suspend fun findOneInStock(@PathVariable id: Int): ProductStockView {
|
||||||
|
val product: Deferred<Product?> = GlobalScope.async {
|
||||||
|
productRepository.getProductById(id)
|
||||||
|
}
|
||||||
|
val quantity: Deferred<Int> = GlobalScope.async {
|
||||||
|
webClient.get()
|
||||||
|
.uri("/stock-service/product/$id/quantity")
|
||||||
|
.accept(APPLICATION_JSON)
|
||||||
|
.awaitExchange().awaitBody<Int>()
|
||||||
|
}
|
||||||
|
return ProductStockView(product.await()!!, quantity.await())
|
||||||
|
}
|
||||||
|
|
||||||
|
@FlowPreview
|
||||||
|
@GetMapping("/")
|
||||||
|
fun findAll(): Flow<Product> {
|
||||||
|
return productRepository.getAllProducts()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.controller
|
||||||
|
|
||||||
|
import com.baeldung.nonblockingcoroutines.model.Product
|
||||||
|
|
||||||
|
class ProductStockView(product: Product, var stockQuantity: Int) {
|
||||||
|
var id: Int = 0
|
||||||
|
var name: String = ""
|
||||||
|
var price: Float = 0.0f
|
||||||
|
|
||||||
|
init {
|
||||||
|
this.id = product.id
|
||||||
|
this.name = product.name
|
||||||
|
this.price = product.price
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.handlers
|
||||||
|
|
||||||
|
import com.baeldung.nonblockingcoroutines.controller.ProductStockView
|
||||||
|
import com.baeldung.nonblockingcoroutines.model.Product
|
||||||
|
import com.baeldung.nonblockingcoroutines.repository.ProductRepositoryCoroutines
|
||||||
|
import kotlinx.coroutines.Deferred
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import org.springframework.web.reactive.function.client.awaitBody
|
||||||
|
import org.springframework.web.reactive.function.client.awaitExchange
|
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest
|
||||||
|
import org.springframework.web.reactive.function.server.ServerResponse
|
||||||
|
import org.springframework.web.reactive.function.server.bodyAndAwait
|
||||||
|
import org.springframework.web.reactive.function.server.json
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class ProductsHandler(
|
||||||
|
@Autowired var webClient: WebClient,
|
||||||
|
@Autowired var productRepository: ProductRepositoryCoroutines) {
|
||||||
|
|
||||||
|
@FlowPreview
|
||||||
|
suspend fun findAll(request: ServerRequest): ServerResponse =
|
||||||
|
ServerResponse.ok().json().bodyAndAwait(productRepository.getAllProducts())
|
||||||
|
|
||||||
|
suspend fun findOneInStock(request: ServerRequest): ServerResponse {
|
||||||
|
val id = request.pathVariable("id").toInt()
|
||||||
|
|
||||||
|
val product: Deferred<Product?> = GlobalScope.async {
|
||||||
|
productRepository.getProductById(id)
|
||||||
|
}
|
||||||
|
val quantity: Deferred<Int> = GlobalScope.async {
|
||||||
|
webClient.get()
|
||||||
|
.uri("/stock-service/product/$id/quantity")
|
||||||
|
.accept(MediaType.APPLICATION_JSON)
|
||||||
|
.awaitExchange().awaitBody<Int>()
|
||||||
|
}
|
||||||
|
return ServerResponse.ok().json().bodyAndAwait(ProductStockView(product.await()!!, quantity.await()))
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun findOne(request: ServerRequest): ServerResponse {
|
||||||
|
val id = request.pathVariable("id").toInt()
|
||||||
|
return ServerResponse.ok().json().bodyAndAwait(productRepository.getProductById(id)!!)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.model
|
||||||
|
|
||||||
|
data class Product(
|
||||||
|
var id: Int = 0,
|
||||||
|
var name: String = "",
|
||||||
|
var price: Float = 0.0f
|
||||||
|
)
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.repository
|
||||||
|
|
||||||
|
import com.baeldung.nonblockingcoroutines.model.Product
|
||||||
|
import org.springframework.data.r2dbc.function.DatabaseClient
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import reactor.core.publisher.Mono
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class ProductRepository(private val client: DatabaseClient) {
|
||||||
|
|
||||||
|
fun getProductById(id: Int): Mono<Product> {
|
||||||
|
return client.execute().sql("SELECT * FROM products WHERE id = $1")
|
||||||
|
.bind(0, id)
|
||||||
|
.`as`(Product::class.java)
|
||||||
|
.fetch()
|
||||||
|
.one()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addNewProduct(name: String, price: Float): Mono<Void> {
|
||||||
|
return client.execute()
|
||||||
|
.sql("INSERT INTO products (name, price) VALUES($1, $2)")
|
||||||
|
.bind(0, name)
|
||||||
|
.bind(1, price)
|
||||||
|
.then()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllProducts(): Flux<Product> {
|
||||||
|
return client.select().from("products")
|
||||||
|
.`as`(Product::class.java)
|
||||||
|
.fetch()
|
||||||
|
.all()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines.repository
|
||||||
|
|
||||||
|
|
||||||
|
import com.baeldung.nonblockingcoroutines.model.Product
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.reactive.awaitFirstOrNull
|
||||||
|
import kotlinx.coroutines.reactive.flow.asFlow
|
||||||
|
import org.springframework.data.r2dbc.function.DatabaseClient
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
class ProductRepositoryCoroutines(private val client: DatabaseClient) {
|
||||||
|
|
||||||
|
suspend fun getProductById(id: Int): Product? =
|
||||||
|
client.execute().sql("SELECT * FROM products WHERE id = $1")
|
||||||
|
.bind(0, id)
|
||||||
|
.`as`(Product::class.java)
|
||||||
|
.fetch()
|
||||||
|
.one()
|
||||||
|
.awaitFirstOrNull()
|
||||||
|
|
||||||
|
suspend fun addNewProduct(name: String, price: Float) =
|
||||||
|
client.execute()
|
||||||
|
.sql("INSERT INTO products (name, price) VALUES($1, $2)")
|
||||||
|
.bind(0, name)
|
||||||
|
.bind(1, price)
|
||||||
|
.then()
|
||||||
|
.awaitFirstOrNull()
|
||||||
|
|
||||||
|
@FlowPreview
|
||||||
|
fun getAllProducts(): Flow<Product> =
|
||||||
|
client.select()
|
||||||
|
.from("products")
|
||||||
|
.`as`(Product::class.java)
|
||||||
|
.fetch()
|
||||||
|
.all()
|
||||||
|
.log()
|
||||||
|
.asFlow()
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
logging.level.org.springframework.data.r2dbc=DEBUG
|
||||||
|
logging.level.org.springframework.web.reactive.function.client.ExchangeFunctions=TRACE
|
||||||
|
spring.http.log-request-details=true
|
||||||
|
spring.h2.console.enabled=true
|
||||||
|
spring.datasource.username=sa
|
||||||
|
spring.datasource.url=jdbc:h2:mem:testdb
|
||||||
|
spring.datasource.password=
|
||||||
|
spring.datasource.dbname=testdb
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration>
|
||||||
|
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
|
||||||
|
</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="STDOUT" />
|
||||||
|
</root>
|
||||||
|
</configuration>
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.baeldung.nonblockingcoroutines
|
||||||
|
|
||||||
|
import com.baeldung.nonblockingcoroutines.config.RouterConfiguration
|
||||||
|
import com.baeldung.nonblockingcoroutines.handlers.ProductsHandler
|
||||||
|
import com.baeldung.nonblockingcoroutines.model.Product
|
||||||
|
import com.baeldung.nonblockingcoroutines.repository.ProductRepositoryCoroutines
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.reactive.flow.asFlow
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.BDDMockito.given
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
|
import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration
|
||||||
|
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration
|
||||||
|
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest
|
||||||
|
import org.springframework.boot.test.mock.mockito.MockBean
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner
|
||||||
|
import org.springframework.test.web.reactive.server.WebTestClient
|
||||||
|
import org.springframework.test.web.reactive.server.expectBodyList
|
||||||
|
import org.springframework.web.reactive.function.client.WebClient
|
||||||
|
import reactor.core.publisher.Flux
|
||||||
|
import org.springframework.test.context.ContextConfiguration
|
||||||
|
|
||||||
|
@WebFluxTest(
|
||||||
|
excludeAutoConfiguration = [ReactiveUserDetailsServiceAutoConfiguration::class, ReactiveSecurityAutoConfiguration::class]
|
||||||
|
)
|
||||||
|
@RunWith(SpringRunner::class)
|
||||||
|
@ContextConfiguration(classes = [ProductsHandler::class, RouterConfiguration::class])
|
||||||
|
class ProductHandlerTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private lateinit var client: WebTestClient
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private lateinit var webClient: WebClient
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private lateinit var productsRepository: ProductRepositoryCoroutines
|
||||||
|
|
||||||
|
|
||||||
|
@FlowPreview
|
||||||
|
@Test
|
||||||
|
public fun `get all products`() {
|
||||||
|
val productsFlow = Flux.just(
|
||||||
|
Product(1, "product1", 1000.0F),
|
||||||
|
Product(2, "product2", 2000.0F),
|
||||||
|
Product(3, "product3", 3000.0F)
|
||||||
|
).asFlow()
|
||||||
|
given(productsRepository.getAllProducts()).willReturn(productsFlow)
|
||||||
|
client.get()
|
||||||
|
.uri("/")
|
||||||
|
.exchange()
|
||||||
|
.expectStatus()
|
||||||
|
.isOk
|
||||||
|
.expectBodyList<Product>()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue