cristianprofile / spring-reactive-kotlin-mongo

Learning Spring reactive using flux and mono (Kotlin implementation)

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Concepts learnt in a real reactive project running in production environment

Build Status

This project model a simple application managing pizzas. I am a vintage programmer so I wont try to impress you using DDD (everybody talks about this buzzword but nobody knows how to implement it in real world). I’ll use MVC using classical 3 layers option: Controller+Service+Repository.

Spring mvc reactive+Spring Data Mongo makes easy to create this demo application.

Reactive programming style using Mono and Flux using Spring and Kotlin (Java is too old to improve faster). The repository that will be used is reactive Mongo connector:

  • Non blocking application.

  • All layers must be tested with 100% coverage

  • I will use a lot of different operators using Mono and Flux

  • Code will be improved using Kotlin extensions

Prerequisites

  • Java 8+, Docker and maven 3 installed.

Project Reactor Kotlin support

Thanks to its great Java interoperability and to Kotlin extensions, Reactor Kotlin APIs leverage regular Java APIs and are additionally enhanced by a few Kotlin-specific APIs that are available out of the box within Reactor artifacts.

project modules

Reactor’s basic operator

@Transactional
override fun addPizza(pizza: Pizza): Mono<Pizza> {
    return pizzaRepository.existsByName(pizza.name).logicalOr(pizzaRepository.existsByDescription(pizza.description))
            .filter {!it}
            .flatMap {
                pizzaRepository.save(pizza)
            }
            .switchIfEmpty(PizzaDuplicatedException(pizza.name).toMono())
}
  • flatMap: when you need to convert elements calling to another method returning mono or flux

  • filter: filter elements published if condition is true (example if condition=true then) Similar as if/else in non functional world:

    Non reactive application:
    if (exist pizza with the same name or description)
        {
            PizzaDuplicatedException(pizza.name)
        }
        else
        {
           pizzaRepository.save(pizza)
        }
    Reactive approach
    @Transactional
    override fun addPizza(pizza: Pizza): Mono<Pizza> {
        return pizzaRepository.existsByName(pizza.name).logicalOr(pizzaRepository.existsByDescription(pizza.description))
                .filter {!it}
                .flatMap {
                    pizzaRepository.save(pizza)
                }
                .switchIfEmpty(PizzaDuplicatedException(pizza.name).toMono())
    }
  • filterWhen: filter elements Test each value emitted by this Flux asynchronously using a generated Publisher<Boolean> test. A value is replayed if the first item emitted by its corresponding test is true. It is dropped if its test is either empty or its first emitted value is false. (similar to filter but we do not use value, instead of this it will be used a Publisher (method that returns a mono/flux boolean) )

    @Transactional
        override fun findAllNotExistWithDescriptionUsingWhenOperator(description: String): Flux<PizzaOut> {
            return pizzaRepository.findAll().filterWhen { pizzaRepository.existsByDescriptionNot(description)}
                    .map { it.convertToPizzaOut() }
        }
  • switchIfEmpty: if empty result is published by observer. The code that you execute in switchIfEmpty is expected to be without side-effects. Unfortunately the documentation does not mention it, at the time of writing this blog. switchIfEmpty does eager computation of the value provided to it. So in the below example, even if you find the order in OrdersDB, it will still save in MissingOrdersDB due to it’s eager computation.One way is to use the defer keyword inside switchIfEmpty.

    return ordersDbRepo.findById("order-id")
           .switchIfEmpty(
                   Mono.defer { missingOrdersDb.save(Order("order-id", false)) })

There is also another way to solve it as shown in below code snippet. Notice the switchIfEmpty is an extension function and can be accessed using the import as shown ( Extension function feature is specific to Kotlin.)

return ordersDbRepo.findById("order-id")
       .switchIfEmpty { missingOrdersDb.save(Order("order-id", false)) }
  • doOnNext: if you do not need to change values received by observer. Example logging

  • then: Return a Mono<Void> that completes when this Flux/Mono completes. This will actively ignore the sequence and only replay completion or error signals.

Reactor’s basic operator using Mono<Boolean>/Flux<Boolean>

  • hasElement(): very usefully when you need to return only a boolean if a mono operation ends ok ()

  • hasElement(T): very usefully when you need to return only a boolean if a mono/flux operation has this T element

  • logicalOr: operator used to link 2 observer publishing Mono<Boolean>

  • logicalAnd: operator used to link 2 observer publishing Mono<Boolean>

  • all: operator emits a single boolean true if all values of this sequence match the Predicate.

  • any: operator emits a single boolean true if any of the values of this Flux sequence match the predicate.

Spring data mongo reactive pagination

Spring data mongo reactive does not contain findAll paginated (out of the box). The solution is very easy. I have created 2 different approach: 1 using query method an another using default Query creation from method names (both create the same query)

// 1. query method
@Query("{ id: { \$exists: true }}")
fun findAll(page: Pageable): Flux<Pizza>
// 2. query creation from method names
fun findByIdNotNull(page: Pageable): Flux<Pizza>

Testing controller layer

Testing spring reactive endpoints is very easy:

  • Annotate your test with @WebFluxTest

    @WebFluxTest(controllers = [PizzaController::class])
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class PizzaControllerTests
  • Mock your service layer:

    @MockBean
    private lateinit var pizzaService: PizzaService
  • Use webTestClient to test your controller.

    @Autowired
    private lateinit var webTestClient: WebTestClient
    val pizzas = webTestClient.get()
                 .uri("/pizza")
                 .exchange()
                 .expectStatus().isOk
                 .returnResult<Pizza>().responseBody
  • Use StepVerifier to be able to test your endpoint. This element subscribes to reactive endpoint and will retrieve the elements that you need in your asserts.

Remember that nothing happens until a Subscriber subscribes to a Publisher, so StepVerifier does this work calling verifyComplete/verify

Java Code:

StepVerifier.create(pizzas)
             .expectNext(pizza1)
             .expectNext(pizza2)
             .expectNext(pizza3)
             .verifyComplete()

Using Kotlin extension:

pizzas.test()
              .expectNext(pizza1)
              .expectNext(pizza2)
              .expectNext(pizza3)
              .verifyComplete()

Testing Service layer

Integration test is a my favorite approach testing service layer (It is slower than unit testing but make me feel more comfortable refactoring service methods using repository). Spring makes easy to test your database. A database in memory is a must so I decided to use Flapdoodle.

Remember use block when you need to force operators call (only tests): pizzaService.addPizza(pizza) do nothing pizzaService.addPizza.block() Subscribe to this Mono and it stop the program until a next signal is received.

 <dependency>
          <groupId>de.flapdoodle.embed</groupId>
          <artifactId>de.flapdoodle.embed.mongo</artifactId>
          <scope>test</scope>
</dependency>

Integration test must be annotated using Spring annotation @DataMongoTest:

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@DataMongoTest
@Import(value = [PizzaServiceImpl::class])
class PizzaServiceIntegrationTests

Assert in reactive functions is easy using StepVerifier:

@Test
fun `should add pizza and get by id`() {
    val pizza = easyRandom.nextObject(Pizza::class.java)
    val addedPizza = pizzaService.addPizza(pizza).block()
    val foundPizza = pizzaService.getPizza(addedPizza!!.id)
    foundPizza.test()
            .expectNext(addedPizza)
            .verifyComplete()
}

About

Learning Spring reactive using flux and mono (Kotlin implementation)


Languages

Language:Kotlin 100.0%