Reputation: 1520
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
Reputation: 344
Add the descriptor file META-INF/beans.xml
to the resources folder of any module where beans should be discovered.
Ref: https://quarkus.io/guides/cdi-reference#bean_discovery
Upvotes: 1
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
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