Внедрение зависимостей и бины
Сейчас статьи хранятся прямо в контроллере. Но контроллер должен только принимать запросы, а работу с данными лучше вынести в отдельный класс — сервис. Как контроллер получит сервис? Не через new: его передаст фреймворк. Это и называется внедрением зависимостей (dependency injection, DI). Объекты, которые создаёт и раздаёт Micronaut, называются бинами (beans).
Сервис статей
Создайте класс сервиса и отметьте его @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 из стандарта Jakarta Inject означает: это бин, и на всё приложение он один. То же можно создать командой mn create-bean com.example.ArticleService.
Внедрение через конструктор
Контроллер просто объявляет сервис параметром конструктора:
// 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()));
}
}Создавая контроллер, Micronaut видит параметр типа ArticleService, находит этот бин и передаёт его. API работает как раньше: POST /articles создаёт статью, GET /articles её показывает. Зато данные теперь живут в сервисе, и им смогут пользоваться другие классы. Внедрение через конструктор — рекомендуемый способ: поле можно сделать final, а в тестах легко передать свой объект.
Области видимости
| Аннотация | Сколько экземпляров |
|---|---|
@Singleton | Один на приложение, создаётся при первом обращении |
@Context | Один, создаётся сразу при запуске |
@Prototype | Новый при каждом внедрении |
@RequestScope | Новый на каждый HTTP-запрос |
Контроллеры по умолчанию тоже синглтоны.
Бин из чужого класса
Класс из библиотеки или JDK аннотацией не отметишь. Для него пишут фабрику — класс с @Factory, методы которого создают бины:
// 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();
}
}Теперь java.time.Clock можно получить в любом конструкторе, например в TimeController(Clock clock). В тестах такой бин легко заменить часами с фиксированным временем.
Частая ошибка
Если забыть @Singleton на сервисе, проект соберётся, а первый запрос к контроллеру вернёт 500. В журнале будет:
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)Строка Path Taken показывает, где оборвалась цепочка. Отметьте класс аннотацией области видимости, например @Singleton.