Micronaut Tutorial: Beans and Scopes
In this article, we learn how to use beans and scopes in our microservice's Java code via the Micronaut framework.
Join the DZone community and get the full member experience.
Join For FreeMicronaut is a relatively new JVM-based framework. It is especially designed for building modular, easy testable microservice applications. Micronaut is heavily inspired by Spring and Grails frameworks, which is not a surprise, if we consider it has been developed by the creators of Grails framework. It is based on Java's annotation processing, IoC (Inversion of Control) and DI (Dependency Injection).
Micronaut implements the JSR-330 ( java.inject
) specification for dependency injection. It supports constructor injection, field injection, JavaBean, and method parameter injection. In this part of tutorial I'm going to give some tips on how to:
- define and register beans in the application context.
- use built-in scopes.
- inject configuration to your application.
- automatically test your beans during application build with JUnit 5.
Prequirements
Before we proceed to the development we need to create sample project with dependencies. Here's the list of Maven dependencies used in this the application created for this tutorial:
<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-runtime</artifactId>
</dependency>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.micronaut.test</groupId>
<artifactId>micronaut-test-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
We will use the newest stable version of Micronaut - 1.1.0:
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-bom</artifactId>
<version>1.1.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
The sample application source code is available on GitHub in the repository https://github.com/piomin/sample-micronaut-applications.git.
Scopes
Micronaut provides 6 built-in scopes for beans. Following JSR-330 additional scopes can be added by defining a @Singleton
bean that implements the CustomScope
interface. Here's the list of built-in scopes:
- Singleton - singleton pattern for the bean.
- Prototype - a new instance of the bean is created each time it is injected. It is default scope for the bean.
- ThreadLocal - is a custom scope that associates a bean per thread via a
ThreadLocal
- Context - a bean is created at the same time as the ApplicationContext.
- Infrastructure - the
@Context
bean cannot be replaced. - Refreshable - a custom scope that allows a bean's state to be refreshed via the
/refresh
endpoint.
Thread Local
Two of those scopes are really interesting. Let's begin with the @ThreadLocal
scope. This is something that is not available for beans in Spring. We can associate beans with threads using single annotation.
How does it work? First, let's define the bean with@ThreadLocal
scope. It holds the single value in the field correlationId
. The main function of this bean is to pass the same id between different singleton beans within a single thread. Here's our sample bean:
@ThreadLocal
public class MiddleService {
private String correlationId;
public String getCorrelationId() {
return correlationId;
}
public void setCorrelationId(String correlationId) {
this.correlationId = correlationId;
}
}
Every singleton bean injects our bean annotated with @ThreadLocal
. There are two sample singleton beans defined:
@Singleton
public class BeginService {
@Inject
MiddleService service;
public void start(String correlationId) {
service.setCorrelationId(correlationId);
}
}
@Singleton
public class FinishService {
@Inject
MiddleService service;
public String finish() {
return service.getCorrelationId();
}
}
Testing
Testing with Micronaut and JUnit 5 is very simple. We have already included micronaut-test-junit5
dependency to our pom.xml
. Now, we only have to annotate test class with @MicronautTest
.
Here's our test. I have run 20 threads which uses@ThreadLocal
through BeginService
and FinishService
singletons. Each thread sets randomly generated correlation id and checks if those two singleton beans use the same correlationId
.
@MicronautTest
public class ScopesTests {
@Inject
BeginService begin;
@Inject
FinishService finish;
@Test
public void testThreadLocalScope() {
final Random r = new Random();
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
executor.execute(() -> {
String correlationId = "abc" + r.nextInt(10000);
begin.start(correlationId);
Assertions.assertEquals(correlationId, finish.finish());
});
}
executor.shutdown();
while (!executor.isTerminated()) {
}
System.out.println("Finished all threads");
}
}
Refreshable
@Refreshable
is another interesting scope offered by Micronaut. You can refresh the state of a bean by calling the HTTP endpoint /refresh
or by publishing RefreshEvent
to the application context. Because we don't use an HTTP server, the second option is the right choice for us. First, let's define the bean with @Refreshable
scope. It injects a value from the configuration property and returns it:
@Refreshable
public class RefreshableService {
@Property(name = "test.property")
String testProperty;
@PostConstruct
public void init() {
System.out.println("Property: " + testProperty);
}
public String getTestProperty() {
return testProperty;
}
}
To test it we should first replace the value of test.property
. After injecting ApplicationContext
into the test we may add new property source programmatically by calling method addPropertySource
. Because this type of property has a higher loading priority than the properties from application.yml
it would be overridden. Now, we just need to publish new refresh events to tge context, and call the method from sample beans one more time:
@Inject
ApplicationContext context;
@Inject
RefreshableService refreshable;
@Test
public void testRefreshableScope() {
String testProperty = refreshable.getTestProperty();
Assertions.assertEquals("hello", testProperty);
context.getEnvironment().addPropertySource(PropertySource.of(CollectionUtils.mapOf("test.property", "hi")));
context.publishEvent(new RefreshEvent());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
testProperty = refreshable.getTestProperty();
Assertions.assertEquals("hi", testProperty);
}
Beans
In the previous section we have already been defining simple beans with different scopes. Micronaut provides some more advanced features, that can be used while defining new beans. You can create conditional beans, define replacements for existing beans, or different methods of injecting configuration into the bean.
Conditions and Replacements
In order to define conditions for newly created beans we need to annotate it with @Requires
. Micronaut offers many possibilities of defining configuration requirements. You will always use the same annotation, but the different field for each option. You can require:
- the presence of one more classes -
@Requires(classes=...)
- the absence of one more classes -
@Requires(missing=...)
- the presence one or more beans -
@Requires(beans=...)
- the absence of one or more beans -
@Requires(missingBeans=...)
- a property with an optional value -
@Requires(property="...")
- a property to not be part of the configuration -
@Requires(missingProperty="...")
- the presence of one of more files in the file system -
@Requires(resources="...")
- the presence of one of more classpath resources -
@Requires(resources="...")
And some others. Now, let's consider the simple sample including some selected conditional strategies. Here's the class that requires the property test.property
to be available in the environment.
@Prototype
@Requires(property = "test.property")
public class TestPropertyRequiredService {
@Property(name = "test.property")
String testProperty;
public String getTestProperty() {
return testProperty;
}
}
Here's another bean definition. It requires that property test.property2
is not available in the environment. The following bean is being replaced by the another bean through annotation @Replaces(bean = TestPropertyRequiredValueService.class)
.
@Prototype
@Requires(missingProperty = "test.property2")
@Replaces(bean = TestPropertyRequiredValueService.class)
public class TestPropertyNotRequiredService {
public String getTestProperty() {
return "None";
}
}
Here's the last sample bean declaration. There is one interesting option related to conditional beans that's dependent on the property. You can require the property to be a certain value, not be a certain value, and use a default in those checks if its not set. Also, the following bean is replacing the TestPropertyNotRequiredService
bean.
@Prototype
@Requires(property = "test.property", value = "hello", defaultValue = "Hi!")
public class TestPropertyRequiredValueService {
@Property(name = "test.property")
String testProperty;
public String getTestProperty() {
return testProperty;
}
}
The result of the following test is predictable:
@Inject
TestPropertyRequiredService service1;
@Inject
TestPropertyNotRequiredService service2;
@Inject
TestPropertyRequiredValueService service3;
@Test
public void testPropertyRequired() {
String testProperty = service1.getTestProperty();
Assertions.assertNotNull(testProperty);
Assertions.assertEquals("hello", testProperty);
}
@Test
public void testPropertyNotRequired() {
String testProperty = service2.getTestProperty();
Assertions.assertNotNull(testProperty);
Assertions.assertEquals("None", testProperty);
}
@Test
public void testPropertyValueRequired() {
String testProperty = service3.getTestProperty();
Assertions.assertNotNull(testProperty);
Assertions.assertEquals("hello", testProperty);
}
Application Configuration
Configuration in Micronaut takes inspiration from both Spring Boot and Grails, integrating configuration properties from multiple sources directly into the core IoC container. Configuration can by default be provided in either Java properties, YAML, JSON or Groovy files. There are 7 levels of priority for property sources (for comparison - Spring Boot provides 17 levels):
- Command line arguments.
- Properties from SPRING_APPLICATION_JSON.
- Properties from MICRONAUT_APPLICATION_JSON.
- Java System Properties.
- OS environment variables.
- Enviroment-specific properties from
application-{environment}.{extension}
- Application-specific properties from
application.{extension}
One of the more interesting options related to Micronaut configuration is @EachProperty
and @EachBean
. Both of these annotations are used for defining multiple instances of a bean, each with their own distinct configuration.
In order to show you the sample use case for those annotations, we should first imagine that we are building a simple client-side load balancer that connects with multiple instances of the service. The configuration is available under property test.url.*
and contains only target URL:
@EachProperty("test.url")
public class ClientConfig {
private String name;
private String url;
public ClientConfig(@Parameter String name) {
this.name = name;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Assuming we have the following configuration properties, Micronaut creates three instances of our configuration under the names: client1
, client2
and client3
.
test:
url:
client1.url: http://localhost:8080
client2.url: http://localhost:8090
client3.url: http://localhost:8100
Using @EachProperty
annotation was only the first step. We also ClientService
responsible for performing interaction with the target service.
public class ClientService {
private String url;
public ClientService(String url) {
this.url = url;
}
public String connect() {
return url;
}
}
The ClientService
is still not registered as a bean, since it is not annotated. Our goal is to inject three beans ClientConfig
containing the distinct configuration, and register three instances of ClientService
bean. That's why we will define bean factory with the method annotated with @EachBean
. In Micronaut, factory usually allows you to register the bean, which is not a part of your codebase, but it is also useful in that case.
@Factory
public class ClientFactory {
@EachBean(ClientConfig.class)
ClientService client(ClientConfig config) {
String url = config.getUrl();
return new ClientService(url);
}
}
Finally, we may proceed to the test. We have injected all the three instances of ClientService
. Each of them contains configuration injected from different instances of the ClientConfig
bean. If you don't set any qualifiers, Micronaut injects the bean containing configurations defined first. For injecting other instances of a bean, we should use a qualifier, which is the name of configuration property.
@Inject
ClientService client;
@Inject
@Named("client2")
ClientService client2;
@Inject
@Named("client3")
ClientService client3;
@Test
public void testClient() {
String url = client.connect();
Assertions.assertEquals("http://loalhost:8080", url);
url = client2.connect();
Assertions.assertEquals("http://loalhost:8090", url);
url = client3.connect();
Assertions.assertEquals("http://loalhost:8100", url);
}
Published at DZone with permission of Piotr Mińkowski, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments