package example.micronaut;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.order.Ordered;
import io.micronaut.http.HttpRequest;
import java.util.Optional;
@FunctionalInterface (1)
public 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 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.core.annotation.NonNull;
import io.micronaut.http.HttpRequest;
import jakarta.inject.Singleton;
import java.util.Optional;
@Singleton (1)
public class HttpHeaderColorFetcher implements ColorFetcher {
@Override
@NonNull
public Optional<String> favouriteColor(@NonNull HttpRequest<?> request) {
return request.getHeaders().get("color", String.class);
}
@Override
public 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 io.micronaut.http.HttpRequest;
import jakarta.inject.Singleton;
import java.util.Locale;
import java.util.Optional;
import java.util.stream.Stream;
@Singleton (1)
public 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
public Optional<String> favouriteColor(HttpRequest<?> request) {
return Stream.of(COLORS)
.filter(c -> request.getPath().contains(c.toLowerCase(Locale.ROOT)))
.map(String::toLowerCase)
.findFirst();
}
@Override
public 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 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;
import java.util.Optional;
@Controller("/color") (1)
public class ColorController {
private final ColorFetcher colorFetcher;
public 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 io.micronaut.context.annotation.Primary;
import io.micronaut.http.HttpRequest;
import jakarta.inject.Singleton;
import java.util.List;
import java.util.Optional;
@Primary (1)
@Singleton (2)
public class CompositeColorFetcher implements ColorFetcher {
private final List<ColorFetcher> colorFetcherList;
public CompositeColorFetcher(List<ColorFetcher> colorFetcherList) { (3)
this.colorFetcherList = colorFetcherList;
}
@Override
public Optional<String> favouriteColor(HttpRequest<?> request) {
return colorFetcherList.stream()
.map(colorFetcher -> colorFetcher.favouriteColor(request))
.filter(Optional::isPresent)
.map(Optional::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.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;
@MicronautTest(startApplication = false) (1)
class CompositeColorFetcherTest {
@Inject
BeanContext beanContext;
@Test
void compositeColorFetcherIsThePrimaryBeanOfTypeColorFetcher() {
assertTrue(beanContext.getBean(ColorFetcher.class) 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.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest (1)
class ColorControllerTest {
@Inject
@Client("/")
HttpClient httpClient; (2)
@Test
void testCompositePattern() {
BlockingHttpClient client = httpClient.toBlocking();
assertEquals("yellow", client.retrieve(HttpRequest.GET("/color").header("color", "yellow")));
assertThrows(HttpClientResponseException.class, () -> client.retrieve(HttpRequest.GET("/color")));
assertEquals("yellow", client.retrieve(HttpRequest.GET("/color/mint").header("color", "yellow")));
assertEquals("mint", client.retrieve(HttpRequest.GET("/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…). |