package example.micronaut
import io.micronaut.core.order.Ordered
import io.micronaut.http.HttpRequest
import java.util.Optional
@FunctionalInterface (1)
interface ColorFetcher : Ordered { (2)
fun favouriteColor(request: HttpRequest<*>): Optional<String>
}
Micronaut Patterns - Composite
Learn how to use a Composite Pattern if you have multiple beans of particular type
Authors: Sergio del Amo
Micronaut Version: 4.6.3
1. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE (e.g. IntelliJ IDEA)
-
JDK 21 or greater installed with
JAVA_HOME
configured appropriately
2. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
3. Composite Pattern
A common pattern while developing Micronaut applications is to create an ordered functional interface. Often, you want to evaluate every implementation in order.
By combining the @Primary
annotation and the injection of a collection of beans of a particular type, you achieve this pattern easily in a Micronaut application.
Imagine you want to create an API to resolve a color:
1 | An interface with one abstract method declaration is known as a functional interface. The compiler verifies that all interfaces annotated with @FunctionInterface really contain one and only one abstract method. |
2 | Implementing the Ordered interface, allows you to easily inject an ordered collection of this type. |
4. Http Header
You may write an implementation which searches for a color in an HTTP Header.
package example.micronaut
import io.micronaut.http.HttpRequest
import jakarta.inject.Singleton
import java.util.Optional
@Singleton (1)
class HttpHeaderColorFetcher : ColorFetcher {
override fun favouriteColor(request: HttpRequest<*>): Optional<String> =
request.headers.get("color", String::class.java)
override fun getOrder(): Int = 10 (2)
}
1 | Use jakarta.inject.Singleton to designate a class as a singleton. |
2 | When you override Ordered::getOrder , the lower the number the higher the precedence. |
5. Path
You could have another naive implementation which uses the HTTP Request’s path.
package example.micronaut
import io.micronaut.http.HttpRequest
import jakarta.inject.Singleton
import java.util.Locale
import java.util.Optional
import java.util.stream.Stream
@Singleton (1)
class PathColorFetcher : ColorFetcher {
override fun favouriteColor(request: HttpRequest<*>): Optional<String> {
return Stream.of(*COLORS)
.filter { request.path.contains(it.lowercase()) }
.map { it.lowercase(Locale.getDefault()) }
.findFirst()
}
override fun getOrder(): Int = 20 (2)
companion object {
private val COLORS = arrayOf(
"Red",
"Blue",
"Green",
"Orange",
"White",
"Black",
"Yellow",
"Purple",
"Silver",
"Brown",
"Gray",
"Pink",
"Olive",
"Maroon",
"Violet",
"Charcoal",
"Magenta",
"Bronze",
"Cream",
"Gold",
"Tan",
"Teal",
"Mustard",
"Navy Blue",
"Coral",
"Burgundy",
"Lavender",
"Mauve",
"Peach",
"Rust",
"Indigo",
"Ruby",
"Clay",
"Cyan",
"Azure",
"Beige",
"Turquoise",
"Amber",
"Mint"
)
}
}
1 | Use jakarta.inject.Singleton to designate a class as a singleton. |
2 | When you override Ordered::getOrder , the lower the number the higher the precedence. |
6. Controller
If you create a controller which injects via constructor injection a bean of type ColorFetcher
,
you will get the following exception:
Message: Multiple possible bean candidates found:
[example.micronaut.PathColorFetcher,
example.micronaut.HttpHeaderColorFetcher]
Path Taken: new ColorController(ColorFetcher colorFetcher)
--> new ColorController([ColorFetcher colorFetcher])
io.micronaut.context.exceptions.DependencyInjectionException:
Failed to inject value for parameter [colorFetcher]
of class: example.micronaut.ColorController
package example.micronaut
import io.micronaut.http.HttpRequest
import io.micronaut.http.MediaType
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Produces
import java.util.Optional
@Controller("/color") (1)
class ColorController(private val colorFetcher: ColorFetcher) { (2)
@Produces(MediaType.TEXT_PLAIN) (3)
@Get("/mint") (4)
fun mint(request: HttpRequest<*>): Optional<String> = (5)
colorFetcher.favouriteColor(request)
@Produces(MediaType.TEXT_PLAIN) (3)
@Get
fun index(request: HttpRequest<*>): Optional<String> = (5)
colorFetcher.favouriteColor(request)
}
1 | The class is defined as a controller with the @Controller annotation mapped to the path /color . |
2 | Use constructor injection to inject a bean of type ColorFetcher . |
3 | By default, a Micronaut response uses application/json as Content-Type . We are returning a String, not a JSON object, so we set it to text/plain with the @Produces annotation. |
4 | The @Get annotation maps the mint method to an HTTP GET request on /mint . |
5 | You can bind the HTTP request as a controller method parameter. |
6.1. Primary
Create a new implementation of ColorFetcher
. It traverses every other implementation of ColorFetcher
in order.
package example.micronaut
import io.micronaut.context.annotation.Primary
import io.micronaut.http.HttpRequest
import jakarta.inject.Singleton
import java.util.Optional
@Primary (1)
@Singleton (2)
class CompositeColorFetcher(private val colorFetcherList: List<ColorFetcher>) : ColorFetcher { (3)
override fun favouriteColor(request: HttpRequest<*>): Optional<String> =
colorFetcherList.stream()
.map { it.favouriteColor(request) }
.filter { it.isPresent }
.map { it.get() }
.findFirst()
}
1 | Primary is a qualifier that indicates that a bean is the primary bean to be selected in the case of multiple interface implementations. |
2 | Use jakarta.inject.Singleton to designate a class as a singleton. |
3 | ColorFetcher implements Ordered . Because of that, you can inject an ordered list of beans of type ColorFetcher . You get every bean of type ColorFetcher but CompositeColorFetcher . |
You can test that CompositeColorFetcher
is primary bean of type ColorFetcher
.
package example.micronaut
import io.micronaut.context.BeanContext
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
@MicronautTest(startApplication = false) (1)
class CompositeColorFetcherTest {
@Inject
lateinit var beanContext: BeanContext
@Test
fun compositeColorFetcherIsThePrimaryBeanOfTypeColorFetcher() {
assertTrue(beanContext.getBean(ColorFetcher::class.java) is CompositeColorFetcher)
}
}
1 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context. This test does not need the embedded server. Set startApplication to false to avoid starting it. |
Moreover, you can test the previous controller with:
package example.micronaut
import io.micronaut.http.HttpRequest
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
@MicronautTest (1)
class ColorControllerTest {
@Inject
@field:Client("/")
lateinit var httpClient: HttpClient (2)
@Test
fun testCompositePattern() {
val client = httpClient.toBlocking()
assertEquals("yellow",
client.retrieve(HttpRequest.GET<Any>("/color").header("color", "yellow"))
)
assertThrows(HttpClientResponseException::class.java) {
client.retrieve(HttpRequest.GET<Any>("/color"))
}
assertEquals("yellow",
client.retrieve(HttpRequest.GET<Any>("/color/mint").header("color", "yellow"))
)
assertEquals("mint",
client.retrieve(HttpRequest.GET<Any>("/color/mint"))
)
}
}
1 | Annotate the class with @MicronautTest so the Micronaut framework will initialize the application context and the embedded server. More info. |
2 | Inject the HttpClient bean and point it to the embedded server. |
7. Next Steps
-
Read more about Primary and Secondary Beans.
The Composite pattern is used in several places within the framework. For example:
8. Help with the Micronaut Framework
The Micronaut Foundation sponsored the creation of this Guide. A variety of consulting and support services are available.
9. License
All guides are released with an Apache license 2.0 license for the code and a Creative Commons Attribution 4.0 license for the writing and media (images…). |