Search code examples
spring-bootkotlinspring-webfluxspring-data-r2dbc

WebFlux and Kotlin use ReactiveCrudRepository in multimodule application


I have an example SpringBoot app in Kotlin and WebFlux. I divided whole application into modules (as I'm used to from asp.net).

Modules:

  • core (models, DTO, helpers, etc...) referenced everywhere
  • data (repositories, tables...) referenced only in business
  • business (services, handlers...) referenced in api
  • api actual SpringBoot application

My problem now is how to properly work with ReactiveCrudRepository<> and repositories in general. I have config class in data module to enable R2dbcRepositories.

@Configuration
@EnableR2dbcRepositories("me.janam.data")
open class RepositoriesConfiguration {
}

Now if I create table and repository

interface IPersonRepository: ReactiveCrudRepository<PersonTable, Long> {
    @Query("SELECT * FROM person.person limit 1")
    fun getOne(): Mono<PersonTable>
}

and try to use it in business module I'm getting an error

Cannot access 'org.springframework.data.repository.reactive.ReactiveCrudRepository' which is a supertype of 'me.janam.data.features.person.repositories.IPersonRepository'. Check your module classpath for missing or conflicting dependencies

Of course if I add

implementation("org.springframework.data:spring-data-commons:2.4.6")

into my business module everything works fine. But somehow this feels strange to me. Is this the right way how to do this?

Also not part of my main question but here is complete config and I like to hear some opinion on it as i'm mainly asp.net dev. Thx.

root - settings.gradle.kts:

rootProject.name = "springboot-example"
include(":api")
include(":business")
include(":data")
include(":core")

root - gradle.properties:

kotlin.code.style=official
kotlin_version=1.4.31
kotlinx_coroutines_reactor_version=1.4.3

r2dbc_postgresql_version=0.8.7.RELEASE
postgresql_version=42.2.19

spring_context_version=5.3.5

root - build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.4.31"
    id("io.spring.dependency-management") version "1.0.11.RELEASE"
}

subprojects {
    apply(plugin = "io.spring.dependency-management" )

    dependencyManagement {
        
    }
}

group = "me.janam"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    //implementation("org.springframework:spring-context:5.3.5")

    testImplementation(kotlin("test-junit5"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "13"
}

core - build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm")
}

group = "me.janam"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    testImplementation(kotlin("test-junit5"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "13"
}

data - build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

val kotlinx_coroutines_reactor_version: String by project
val r2dbc_postgresql_version: String by project
val postgresql_version: String by project
val spring_context_version: String by project

plugins {
    kotlin("jvm")
}

group = "me.janam"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(project(":core"))

    implementation("org.springframework:spring-context:$spring_context_version")

    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc:2.4.4")
    runtimeOnly("io.r2dbc:r2dbc-postgresql:$r2dbc_postgresql_version")
    runtimeOnly("org.postgresql:postgresql:$postgresql_version")

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinx_coroutines_reactor_version")

    testImplementation(kotlin("test-junit5"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "13"
}

business - build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

val kotlinx_coroutines_reactor_version: String by project
val spring_context_version: String by project

plugins {
    kotlin("jvm")
}

group = "me.janam"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(project(":core"))
    implementation(project(":data"))

    implementation("org.springframework:spring-context:$spring_context_version")

    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinx_coroutines_reactor_version")

    implementation("org.springframework.data:spring-data-commons:2.4.6") //TODO

    testImplementation(kotlin("test-junit5"))
    testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "13"
}

api - build.gradle.kts:

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.4.4"
    // Přesunuto do rootu
    id("io.spring.dependency-management")
    kotlin("jvm")
    kotlin("plugin.spring") version "1.4.31"
}

group = "me.janam"
version = "1.0-SNAPSHOT"
//java.sourceCompatibility = JavaVersion.VERSION_13

repositories {
    mavenCentral()
}

dependencies {
    implementation(project(":core"))
    implementation(project(":business"))

    //implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")
    implementation("org.springframework.boot:spring-boot-starter-rsocket")
    implementation("org.springframework.boot:spring-boot-starter-webflux")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("io.projectreactor.kotlin:reactor-kotlin-extensions")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
    //runtimeOnly("io.r2dbc:r2dbc-postgresql")
    //runtimeOnly("org.postgresql:postgresql")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("io.projectreactor:reactor-test")
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "13"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

Solution

  • Adding the same set of dependencies to each subproject may feel odd, but it's totally fine to do. In order to use a given dependency in a given subproject, you'll have to specify it as a dependency for that subproject.

    There are, however, neater ways to accomplish this than actually copy-pasting the import statement to each build file. I would suggest specifying a subprojects section in your root build.gradle.kts and putting shared & common dependencies there:

    subprojects {
        dependencies {
            // Put items here that are used in all/most subprojects:
            implementation("org.springframework:spring-context:$spring_context_version")
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlinx_coroutines_reactor_version")
    
            // You can put this import here or just keep it in the build files
            // for subprojects where it will actually get used
            implementation("org.springframework.data:spring-data-commons:2.4.6")
    
            testImplementation(kotlin("test-junit5"))
            testImplementation("org.junit.jupiter:junit-jupiter-api:5.6.0")
            testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.6.0")
        }
    }
    

    You'll still need to specify in each sub-build file what other subprojects each depends on with implementation(project(":core")) type statements, but the above dependencies block takes care of making the specified libraries accessible in all subprojects.