Micronaut Patterns - Composite

Learn how to use a Composite Pattern if you have multiple beans of particular type

Authors: Sergio del Amo

Micronaut Version: 3.7.0

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

  • JDK 1.8 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.

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:

src/main/kotlin/example/micronaut/ColorFetcher.kt
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>
}
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.

src/main/kotlin/example/micronaut/HttpHeaderColorFetcher.kt
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.

src/main/kotlin/example/micronaut/PathColorFetcher.kt
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
src/main/kotlin/example/micronaut/ColorController.kt
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.
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.

src/main/kotlin/example/micronaut/CompositeColorFetcher.kt
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.

src/test/kotlin/example/micronaut/CompositeColorFetcherTest.kt
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:

src/test/kotlin/example/micronaut/ColorControllerTest.kt
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

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.