package example.micronaut
import io.micronaut.core.annotation.NonNull
import io.micronaut.core.order.Ordered
import io.micronaut.http.HttpRequest
@FunctionalInterface (1)
interface ColorFetcher extends Ordered { (2)
@NonNull
Optional<String> favouriteColor(@NonNull HttpRequest<?> request)
}
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 17 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 groovy.transform.CompileStatic
import io.micronaut.core.annotation.NonNull
import io.micronaut.http.HttpRequest
import jakarta.inject.Singleton
@CompileStatic
@Singleton (1)
class HttpHeaderColorFetcher implements ColorFetcher {
@Override
@NonNull
Optional<String> favouriteColor(@NonNull HttpRequest<?> request) {
return request.headers.get('color', String)
}
@Override
int getOrder() { (2)
return 10
}
}
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 groovy.transform.CompileStatic
import io.micronaut.http.HttpRequest
import jakarta.inject.Singleton
@CompileStatic
@Singleton (1)
class PathColorFetcher implements ColorFetcher {
private static final String[] COLORS = [
'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'
]
@Override
Optional<String> favouriteColor(HttpRequest<?> request) {
return Optional.ofNullable(
COLORS.findAll { request.path.contains(it.toLowerCase(Locale.ROOT)) }
.collect { it.toLowerCase() }[0]
)
}
@Override
int getOrder() { (2)
return 20
}
}
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 groovy.transform.CompileStatic
import io.micronaut.core.annotation.NonNull
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
@CompileStatic
@Controller('/color') (1)
class ColorController {
private final ColorFetcher colorFetcher
ColorController(ColorFetcher colorFetcher) { (2)
this.colorFetcher = colorFetcher
}
@Produces(MediaType.TEXT_PLAIN) (3)
@Get('/mint') (4)
Optional<String> mint(@NonNull HttpRequest<?> request) { (5)
return colorFetcher.favouriteColor(request)
}
@Produces(MediaType.TEXT_PLAIN) (3)
@Get
Optional<String> index(@NonNull HttpRequest<?> request) { (5)
return 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 groovy.transform.CompileStatic
import io.micronaut.context.annotation.Primary
import io.micronaut.http.HttpRequest
import jakarta.inject.Singleton
@CompileStatic
@Primary (1)
@Singleton (2)
class CompositeColorFetcher implements ColorFetcher {
private final List<ColorFetcher> colorFetcherList
CompositeColorFetcher(List<ColorFetcher> colorFetcherList) { (3)
this.colorFetcherList = colorFetcherList
}
@Override
Optional<String> favouriteColor(HttpRequest<?> request) {
return Optional.ofNullable(colorFetcherList.collect { it.favouriteColor(request) }
.findAll { it.isPresent() }
.collect { it.get() }[0])
}
}
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.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification
@MicronautTest(startApplication = false) (1)
class CompositeColorFetcherSpec extends Specification {
@Inject
BeanContext beanContext
void compositeColorFetcherIsThePrimaryBeanOfTypeColorFetcher() {
expect:
beanContext.getBean(ColorFetcher) instanceof 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.BlockingHttpClient
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.spock.annotation.MicronautTest
import jakarta.inject.Inject
import spock.lang.Specification
@MicronautTest (1)
class ColorControllerSpec extends Specification {
@Inject
@Client('/')
HttpClient httpClient (2)
void testCompositePattern() {
when:
BlockingHttpClient client = httpClient.toBlocking()
then:
'yellow' == client.retrieve(HttpRequest.GET('/color').header('color', 'yellow'))
'yellow' == client.retrieve(HttpRequest.GET('/color/mint').header('color', 'yellow'))
'mint' == client.retrieve(HttpRequest.GET('/color/mint'))
when:
client.retrieve(HttpRequest.GET('/color'))
then:
thrown(HttpClientResponseException)
}
}
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…). |