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:

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/java/example/micronaut/ColorFetcher.java
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);
}
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/java/example/micronaut/HttpHeaderColorFetcher.java
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.

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

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

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

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

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…​).