Dependency injection and beans
Right now the articles are stored in the controller itself. But a controller should only accept requests; working with data belongs in a separate class, a service. How does the controller get the service? Not through new: the framework hands it over. This is called dependency injection (DI). The objects Micronaut creates and hands out are called beans.
The article service
Create a service class and mark it with @Singleton:
// src/main/java/com/example/ArticleService.java
package com.example;
import jakarta.inject.Singleton;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@Singleton
public class ArticleService {
private final Map<Long, Article> articles = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(1);
public Collection<Article> findAll() {
return articles.values();
}
public Optional<Article> findById(Long id) {
return Optional.ofNullable(articles.get(id));
}
public Article create(ArticleForm form) {
Article article = new Article(nextId.getAndIncrement(), form.title(), form.body());
articles.put(article.id(), article);
return article;
}
}@Singleton from the Jakarta Inject standard means: this is a bean, and there is one of it per application. The same class can be generated with mn create-bean com.example.ArticleService.
Constructor injection
The controller simply declares the service as a constructor parameter:
// src/main/java/com/example/ArticleController.java
@Controller("/articles")
public class ArticleController {
private final ArticleService articleService;
public ArticleController(ArticleService articleService) {
this.articleService = articleService;
}
@Get
public Collection<Article> list() {
return articleService.findAll();
}
@Get("/{id}")
public Optional<Article> show(Long id) {
return articleService.findById(id);
}
@Post
public HttpResponse<Article> create(@Body ArticleForm form) {
Article article = articleService.create(form);
return HttpResponse.created(article, URI.create("/articles/" + article.id()));
}
}When creating the controller, Micronaut sees a parameter of type ArticleService, finds that bean and passes it in. The API works as before: POST /articles creates an article, GET /articles shows it. But the data now lives in the service, and other classes can use it too. Constructor injection is the recommended style: the field can be final, and in tests you can easily pass your own object.
Scopes
| Annotation | How many instances |
|---|---|
@Singleton | One per application, created on first use |
@Context | One, created right at startup |
@Prototype | A new one for every injection |
@RequestScope | A new one for every HTTP request |
Controllers are singletons by default too.
A bean from a third-party class
You cannot annotate a class from a library or the JDK. For such a class you write a factory: a class marked @Factory whose methods create beans:
// src/main/java/com/example/ClockFactory.java
package com.example;
import io.micronaut.context.annotation.Factory;
import jakarta.inject.Singleton;
import java.time.Clock;
@Factory
public class ClockFactory {
@Singleton
Clock clock() {
return Clock.systemDefaultZone();
}
}Now java.time.Clock can be received in any constructor, for example in TimeController(Clock clock). In tests such a bean is easy to replace with a clock frozen at a fixed time.
Common mistake
If you forget @Singleton on the service, the project builds, but the first request to the controller returns 500. The log shows:
ERROR i.m.http.server.RouteExecutor - Unexpected error occurred: Failed to inject value for parameter [articleService] of class: com.example.ArticleController
Message: No bean of type [com.example.ArticleService] exists.
Path Taken:
new c.e.ArticleController(ArticleService articleService)The Path Taken line shows where the chain broke. Mark the class with a scope annotation such as @Singleton.