Contents

Introduction to Micronaut IOC: Basics

Robert Pudlik

01 Jun 2023.9 minutes read

Introduction to Micronaut IOC: Basics webp image

This article aims to comprehensively introduce the Micronaut IOC container, covering its details, capabilities, and use cases. It delves into common use cases and highlights best practices for utilizing the container. Additionally, the article includes a section providing tips and sharing personal experience and knowledge, particularly for those with a background in Spring Boot, to assist in the transition to using Micronaut.

Inversion of Control (IOC) container

The role of the IOC container is to create objects by injecting into them previously declared dependencies and their subsequent management. These objects are called beans, they are created based on the provided definition. We can define them by labeling our classes as beans or creating factory methods. In this way, we put control of object initialization in the hands of the container.

The Micronaut IoC container implements the JSR-330, and relies on AOT compilation to minimize the use of reflection and reduce application startup time, making it lightweight and compatible with native compilation. It is an independent module relative to the framework and can be used like the popular Guice or Dagger.

Dependency Injection (DI) types

Micronaut, like most IoC containers, gives us 3 ways to inject dependencies using @Inject annotation

Constructor

class Car {

    private final Engine engine;

    @Inject
    public Car(Engine engine) {
        this.engine = engine;
    } 

We don’t need to use @Inject annotation when the class contains only one constructor because the container will automatically select it. In the case of multiple constructors, we need to annotate one of them.

Method/Setter

class Car {

    private Engine engine;

    @Inject
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

Field

class Car {

    @Inject
    Engine engine;

}

However, the most preferred type of injection is the constructor because you are able to enforce dependencies at object creation, which in combination with the keyword final, provides greater null safety. Injection by constructor also provides easier testability of code, as you can inject mocks in a unit test without IoC container. Setter and field injection should be used as a last resort if we have no other options.

Dependencies

Here are minimal dependencies that you need to run the Micronaut IoC container and examples in this tutorial. You can also create a sample project with micronaut.io/launch. The following examples will use Micronaut version 3.8.2 and JDK 17.

Gradle

plugins {
id 'io.micronaut.library' version '1.3.2'
}

micronaut {
version("3.8.2")
}

...

dependencies {
annotationProcessor "io.micronaut:micronaut-inject-java"
implementation "io.micronaut:micronaut-inject"
implementation "io.micronaut:micronaut-runtime"
testImplementation "io.micronaut.test:micronaut-test-junit5"
testImplementation "org.junit.jupiter:junit-jupiter-engine:5.9.1"
}

Scopes

The lifecycle of beans is defined by Scope according to the JSR-330 specification. This lets the IOC container know when to create an instance of beans and how to manage existing ones according to predefined rules. Micronaut offers 7 scopes, some of which are taken directly from the specification, and the rest are created by the framework's developers.

Available scopes

  • Singleton - scope representing a bean that can have only one instance,
    created lazily on the first injection
  • Context - similar scope to singleton, but with lifecycle bounded to BeanContext, created eagerly at the start of the context. Developers advise using @Singleton by default, you should only use @Context when you need eager initialization because it affects application startup
  • Prototype - default bean scope, a new instance is created for each injection
  • RequestScope - scope representing bean with lifecycle associated with the HTTP request, requires an HTTP module
  • Infrastructure - scope representing a critical bean that cannot be overridden using @Replaces. A good option for developers of libraries and extensions
  • ThreadLocal - scope creating a new bean instance for each thread
  • Refreshable - scope that refreshes bean state via /refresh or publishing RefreshEvent

It is important to note that the default scope bean is a prototype, unlike the popular Spring Boot, whose default scope is a Singleton. We will focus on the last three as I believe they're unique among other popular IoC containers.

Infrastructure

Let's say we have a critical bean that counts the rectangle field, and we don't want someone to replace it because it might result in a malfunctioning application. Therefore, we use the @Infrastructure annotation.

@Infrastructure
public class RectangleAreaCalculator {

    public double calculate(double a, double b) {
        return a * b;
    }

What happens if some user tries to override the bean using @Replaces on subclass with bad implementation?

@Singleton
@Replaces(RectangleAreaCalculator.class)
public class InvalidRectangleAreaCalculator extends RectangleAreaCalculator {

    @Override
    public double calculate(double a, double b) {
    return a - b;
    }

In practice, nothing will happen, Bean InvalidRectangleAreaCalculator will not be created, nor will it replace RectangleAreaCalculator.

@MicronautTest
class InfrastructureTest {

    @Inject
    RectangleAreaCalculator rectangleAreaCalculator;

    @Test
    void infrastructureBeanDoesNotGetReplaced() {
        Assertions.assertInstanceOf(RectangleAreaCalculator.class, rectangleAreaCalculator);
        Assertions.assertEquals(20, rectangleAreaCalculator.calculate(4, 5));
    }

ThreadLocal

Before we touch on the scope itself, it is worth knowing that the ThreadLocal mechanism exists in the Java API. It guarantees that it will create a separate object instance for each thread calling .get() method of ThreadLocal. You can think of it as a HashMap, where the implicit key is a thread.

During initialization, we have to implement the functional interface with a method that returns the initial value.

    ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 27);

Then we’re able to retrieve a unique instance for the current thread using the get() method.

    Integer myUniqueInteger = threadLocal.get();

In our case, the initial value will be our bean definition. Under the hood, the implementation of this scope relies on a proxy that has a ThreadLocal variable in it and redirects method calls to the appropriate instance. This allows a bean to be shared between other beans in the same thread.

The theory is behind us, so let's move on to an example. We create a simple bean whose purpose is only to store a String.

@ThreadLocal
public class ThreadLocalStorage {

    private String text;

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }

According to the ThreadLocal scope assumption, each thread should refer to a separate instance of the bean. So the contents of the "text" field will only be shared within the same thread, which I present in the following test.

@MicronautTest
class ThreadLocalExampleTest {

    @Inject
    ThreadLocalStorage storage;

    @Test
    void test() {
        storage.setText("Main test thread");
        assertEquals("Main test thread", storage.getText());

        runBlockingInNewThread(() -> {
            assertNull(storage.getText()); (1)
            storage.setText("Sub-thread in test");
            assertEquals("Sub-thread in test", storage.getText());

            runBlockingInNewThread(() -> {
                assertNull(storage.getText());
                storage.setText("Sub-sub-thread in test");
                assertEquals("Sub-sub-thread in test", storage.getText());
            });

            assertEquals("Sub-thread in test", storage.getText()); (2)
        });

        assertEquals("Main test thread", storage.getText());
    }

    private void runBlockingInNewThread(Runnable runnable) throws RuntimeException {
        try {
            final var thread = new Thread(runnable);
            thread.start();
            thread.join();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}

(1) In this thread, we should not see the text from the main text thread, so it should be null.

(2) We check that the inner thread didn't override the value.
As we can see in the above test, the order in which methods are called on a given bean does not matter. The state between calls only matters within a single thread.

Refreshable

By using the Refreshable scope, we can refresh the state of our bean. In practice, this means the @PostConstruct annotation will be called on our bean again. The use case for this scope could be to refresh the configuration of some bean, clear the cache or call any other side effect. Would I use this in practice? It is already a topic for another article, but the possibility exists.

As an example, we will create a class named RefreshableSeed, which contains an init() method and latestSeed().

@Refreshable
public class RefreshableSeed {

    private int seed;

    @PostConstruct
    public void init() {
    seed = new Random().nextInt();
    }

    public int latestSeed() {
    return seed;
    }

As I mentioned earlier, we can refresh the state in two ways. Using the endpoint /refresh, which the Micronaut predefines, but this requires an HTTP module

Publishing RefreshEvent using ApplicationEventPublisher
eventPublisher.publishEvent(new RefreshEvent());

For testing and demonstration we will use the method with the event publisher.

@MicronautTest
class RefreshableSeedTest {

    @Inject
    RefreshableSeed refreshableSeed;

    @Inject
    ApplicationEventPublisher<RefreshEvent> eventPublisher;

    @Test
    void refreshableBeanGetsRefreshedWithRefreshEvent() {
        final var firstSeed = refreshableSeed.latestSeed();

        // Should not change before event
        assertEquals(firstSeed, refreshableSeed.latestSeed());

        eventPublisher.publishEvent(new RefreshEvent());

        assertNotEquals(firstSeed, refreshableSeed.latestSeed());
    }

As we can see from the test above, calling an event only affected the state of the bean.

Coming from Spring

Many people are starting to learn about Micronaut and have already had to deal with Spring. Developers are also aware of this, creating special modules such as Micronaut for Spring to help migrate existing applications to the Micronaut ecosystem. This module has the disadvantage of only partially solving the problem because you mix code from two different frameworks. I believe using such modules is not the most optimal solution, so I will point out the crucial differences for Spring Developers.

Default scope is different

The most crucial difference is that Micronaut Container's default scope is Prototype which is an alias for @Bean, not Singleton like in Spring Boot. My advice is to use the actual annotations responsible for the scope, like @Singleton or @Prototype, to be more verbose instead of relying on defaults.

Singleton’s scope is not the same

Singleton scope is not the same as in Spring Boot. Singleton in Micronaut is lazy by default and is not created at the start of the context but on demand. The true equivalent of @Scope(“Singleton”) from Spring Boot is @Context. However, this is intentional; the developers designed it this way to further reduce the startup time of the application. Therefore, eager initialization should be used only when there are actually good reasons or rationale for it.

Spring BootMicronaut
@Component@Context
@Lazy @Component@Singleton

Eager singleton in Micronaut

As I mentioned earlier, lazy singleton initialization is preferred, and we should use it first. However, if you have such a reason, you have a couple of options.

  1. Use @Context scope
  2. By bean context configuration, but it affects all singletons
public class MicronautApp {

    public static void main(String[] args) {
        Micronaut.build(args)
            .eagerInitSingletons(true)
            .mainClass(Application.class)
            .start();
    }
}
  1. Using @Parallel annotation with @Singleton

Annotation Cheat Sheet

I have also created a small cheat-sheet of annotations used frequently in Spring Boot and will help you find Micronaut equivalents.

SpringMicronaut
@Component1:1 Mapping is@Context, but use instead @Singleton. Context should be only used when you really need eager initialization.
@Service1:1 Mapping is@Context, but use instead @Singleton. Context should be only used when you really need eager initialization.
@Repository@Repository
@RestController@Controller
@Bean1:1 Mapping is@Context, but use instead @Singleton. Context should be only used when you really need eager initialization.@Bean is not equivalent because the default scope in Micronaut is @Prototype
@Scope@Singleton @Context @Prototype @Infrastructure @ThreadLocal @Refreshable @RequestScope
@Autowired@Inject
@Value@Value
@Qualifier@Named
@Configuration@Factory
@Profile("test")@Requires(env="test")
@Import@Import
@Primary@Primary

Summary

I hope you found the article helpful and that it was a sufficient basic introduction to IoC in Micronaut, regardless of your previous experience with the Spring Framework. In the next part of the blog, we will look at bean containers, conditional beans, and their use cases. Stay tuned!

Reviewed by: Rafał Maciak, Mateusz Palichleb

Blog Comments powered by Disqus.