BAEL-20873 Move Spring Boot Kotlin module to Spring Boot modules

This commit is contained in:
mikr
2020-01-27 16:59:16 +01:00
parent 9fd36e6fe8
commit 1247ff6a02
18 changed files with 2 additions and 3 deletions

View File

@@ -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)
}

View File

@@ -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())
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}

View File

@@ -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()
}
}

View File

@@ -0,0 +1,51 @@
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.CoroutineStart
import kotlinx.coroutines.coroutineScope
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 = coroutineScope {
val product: Deferred<Product?> = async(start = CoroutineStart.LAZY) {
productRepository.getProductById(id)
}
val quantity: Deferred<Int> = async(start = CoroutineStart.LAZY) {
webClient.get()
.uri("/stock-service/product/$id/quantity")
.accept(APPLICATION_JSON)
.awaitExchange().awaitBody<Int>()
}
ProductStockView(product.await()!!, quantity.await())
}
@FlowPreview
@GetMapping("/")
fun findAll(): Flow<Product> {
return productRepository.getAllProducts()
}
}

View File

@@ -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
}
}

View File

@@ -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)!!)
}
}

View File

@@ -0,0 +1,7 @@
package com.baeldung.nonblockingcoroutines.model
data class Product(
var id: Int = 0,
var name: String = "",
var price: Float = 0.0f
)

View File

@@ -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()
}
}

View File

@@ -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()
}

View File

@@ -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

View File

@@ -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>

View File

@@ -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>()
}
}