jimmy_terra
jimmy_terra

Reputation: 1520

How to resolve multi-module classpath beans in Quarkus?

In Spring it's possible to define bean dependencies in separate modules, which are then resolved via the classpath at runtime. Is it possible to do something similar in Quarkus?

For example, a multi-module setup that looks like this:

- service
- service-test
- service-artifact

In Spring it's possible to define @Configuration in the service module, that resolves concrete dependencies at runtime via the classpath of its current context, either service-test or service-artifact, allowing injection of dummy or test dependencies when under test, and real ones in the production artifact.

For example, a class in service requires an instance of SomeInterface. The implementation of SomeInterface is defined in either the -test or -artifact module. The service module has no direct dependency on either the -test or -artifact modules.

Some code:

In the service module:

@ApplicationScoped
class OrderService(private val repository: OrderRepository) {
    fun process(order: Order) {
        repository.save(order)
    }
}

interface OrderRepository {
    fun save(order: Order)
}

In the service-test module:

class InMemoryOrderRepository : OrderRepository {
    val orders = mutableListOf<Order>()
    override fun save(order: Order) {
        orders.add(order)
    }
}

class OrderServiceTestConfig {
    @ApplicationScoped
    fun orderRepository(): OrderRepository {
        return InMemoryOrderRepository()
    }
}


@QuarkusTest
class OrderServiceTest {

    @Inject
    private lateinit var service: OrderService

    @Test
    fun `injected order service with resolved repository dependency`() {
        // This builds and runs OK            
        service.process(Order("some_test_order"))
    }
}

Where I have tried to replicate a Spring-style setup as above in Quarkus, ArC validation is failing with UnsatisfiedResolutionException on the build of the service module, even though everywhere it is actually consumed provides the correct dependencies; a test successfully resolves the dependency and passes.

How do I achieve the separation of dependency interface from the implementation, and keep ArC validation happy, with Quarkus?

(Note: this behaviour occurs with Java and Maven also.)

I have included a maven example here. Note that ./mvnw install fails with the UnsatisfiedResolutionException but that it's possible to build and run the test successfully using ./mvnw test.

Build files:

root project build.gradle.kts:

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

    plugins {
        kotlin("jvm") version "1.3.72"
        kotlin("plugin.allopen") version "1.3.72"
    }

allprojects {

    group = "my-group"
    version = "1.0.0-SNAPSHOT"

    repositories {
        mavenLocal()
        mavenCentral()
    }
}

subprojects {

    apply {
        plugin("kotlin")
        plugin("kotlin-allopen")
    }

    java {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    
    allOpen {
        annotation("javax.ws.rs.Path")
        annotation("javax.enterprise.context.ApplicationScoped")
        annotation("io.quarkus.test.junit.QuarkusTest")
    }

    apply {
        plugin("kotlin")
    }

    dependencies {
        implementation("org.jetbrains.kotlin:kotlin-reflect")
        implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    }

    tasks.withType<KotlinCompile> {
        kotlinOptions.jvmTarget = JavaVersion.VERSION_11.toString()
        kotlinOptions.javaParameters = true
    }
}

build.gradle.kts for service:

import io.quarkus.gradle.tasks.QuarkusDev

plugins {
    id("io.quarkus") version "1.9.1.Final"
}

apply {
    plugin("io.quarkus")
}

dependencies {
    implementation(project(":common:model"))
    implementation(enforcedPlatform("io.quarkus:quarkus-universe-bom:1.9.1.Final"))
    implementation("io.quarkus:quarkus-kotlin")
}

build.gradle.kts for service-test:

import io.quarkus.gradle.tasks.QuarkusDev

plugins {
    id("io.quarkus") version "1.9.1.Final"
}

apply {
    plugin("io.quarkus")
}

dependencies {
    implementation(project(":service"))
    implementation(enforcedPlatform("io.quarkus:quarkus-universe-bom:1.9.1.Final"))
    implementation("io.quarkus:quarkus-kotlin")
    testImplementation("io.quarkus:quarkus-junit5")
}

Upvotes: 3

Views: 3005

Answers (3)

Nick
Nick

Reputation: 344

Add the descriptor file META-INF/beans.xml to the resources folder of any module where beans should be discovered.

enter image description here

Ref: https://quarkus.io/guides/cdi-reference#bean_discovery

Upvotes: 1

Augustin Ghauratto
Augustin Ghauratto

Reputation: 1520

Unfortunately, Quarkus has a bit different way of creating and injecting beans as in Spring.

It's using "simplified bean discovery", and that means that the beans are scanned on the classpath during the build time, but only those that have annotations considered as "discovery mode", are taken into the account.

Those would be: @ApplicationScoped, @SessionScoped, @ConversationScoped and @RequestScoped, @Interceptor and @Decorator more described here

In addition to that, beans must not have the visibility boundaries.

In you'd like to use beans from other modules, create a configuration class within that module. The configuration class should not have any annotation. In that config class, create beans with @Producer annotation and one of the above scope beans. Example in Kotlin:

class QuarkusConfig {
    @Producer
    @ApplicationScope
    fun myClass(myClassDependency: DependencyClass): MyClass {
        return MyClass(myClassDependency)
    }
}

But notice, despite that, some beans are treated by Quarkus in a special way (ex. all beans that have @Path annotation) and those should be annotated preferably with @ApplicationScope, using either constructor, or field injection. Produced by @Producer methods won't allow for all the magic that Quarkus is doing.

If you'd like some more quarkus dependant beans, ex. the bean that bounds a configuration (using @ConfigMapping annotated beans), in addition you need to have either beans.xml in your META-INF directory, or which seems easier to add the jandex index to your build system:

plugins {
  id("io.quarkus") version "2.14.1.Final"
  id("org.kordamp.gradle.jandex") version "1.0.0"
}

Summary: don't use configuration beans as in spring, only the constructor/field injection, and to have beans discovered from different modules, add the jandex index file using plugin.

Upvotes: 1

Artem Sidorkin
Artem Sidorkin

Reputation: 56

Try to use instance injection (java example):

import javax.enterprise.inject.Instance;
...
@Inject
Instance<MyBeanClass> bean;
...
bean.get(); // for a single bean
bean.stream(); // for a collection

Upvotes: 3

Related Questions