CDI: бины, области видимости и внедрение
Держать данные и логику прямо в ресурсе неудобно: ресурс должен только принимать HTTP-запросы и отдавать ответы. Работу с постами лучше вынести в отдельный класс-сервис. Чтобы ресурс получил готовый объект сервиса, не создавая его через new, используют CDI (Contexts and Dependency Injection) — стандарт внедрения зависимостей. В Quarkus его реализует контейнер ArC из расширения quarkus-arc.
Термины
- Бин (bean) — объект, который создаёт и хранит контейнер.
- Область видимости (scope) — сколько живёт бин: всё время работы приложения или один запрос.
- Внедрение (injection) — контейнер сам кладёт нужный бин в поле или конструктор.
Сервис постов
Создайте файл сервиса. Аннотация @ApplicationScoped делает класс бином, который существует в одном экземпляре на всё приложение:
// src/main/java/com/example/blog/PostService.java
package com.example.blog;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@ApplicationScoped
public class PostService {
private final Map<Long, Post> posts = new ConcurrentHashMap<>();
private final AtomicLong nextId = new AtomicLong(1);
public PostService() {
create("Привет, Quarkus", "Первый пост блога.");
create("Режим разработки", "Меняйте код и обновляйте страницу.");
}
public List<Post> findAll() {
return posts.values().stream()
.sorted((a, b) -> Long.compare(a.id(), b.id()))
.toList();
}
public Optional<Post> findById(long id) {
return Optional.ofNullable(posts.get(id));
}
public Post create(String title, String body) {
long id = nextId.getAndIncrement();
Post post = new Post(id, title, body);
posts.put(id, post);
return post;
}
public boolean delete(long id) {
return posts.remove(id) != null;
}
}Сервис хранит посты и умеет их искать, создавать и удалять. О HTTP он ничего не знает.
Внедрение в ресурс
Ресурс получает сервис через @Inject, а поля posts и nextId из него удаляются:
// src/main/java/com/example/blog/PostResource.java
@Path("/posts")
public class PostResource {
@Inject
PostService postService;
@GET
public List<Post> list(@QueryParam("search") String search,
@QueryParam("limit") @DefaultValue("10") int limit) {
return postService.findAll().stream()
.filter(p -> search == null || p.title().toLowerCase().contains(search.toLowerCase()))
.limit(limit)
.toList();
}
@GET
@Path("/{id}")
public Post get(@PathParam("id") long id) {
return postService.findById(id)
.orElseThrow(NotFoundException::new);
}
@POST
public Response create(NewPost newPost) {
Post post = postService.create(newPost.title(), newPost.body());
return Response.created(URI.create("/posts/" + post.id()))
.entity(post)
.build();
}
}Импорт аннотации — jakarta.inject.Inject. Метод delete переписывается так же, через postService.delete(id). Проверим поиск из прошлых уроков:
curl "http://localhost:8080/posts?search=quarkus"[{"id":1,"title":"Привет, Quarkus","body":"Первый пост блога."}]
Поле без private — привычный стиль Quarkus: так контейнеру не нужна рефлексия. Можно внедрять и через конструктор — если он один, @Inject писать не обязательно:
// src/main/java/com/example/blog/PostResource.java
private final PostService postService;
public PostResource(PostService postService) {
this.postService = postService;
}Области видимости
| Аннотация | Сколько живёт бин |
|---|---|
@ApplicationScoped | Один на приложение, создаётся при первом обращении |
@Singleton | Один на приложение, но без прокси-обёртки |
@RequestScoped | Свой экземпляр на каждый HTTP-запрос |
@Dependent | Новый экземпляр для каждого места внедрения |
Для сервисов почти всегда подходит @ApplicationScoped.
Частая ошибка
Если забыть аннотацию области видимости, класс не станет бином, и приложение не запустится:
UnsatisfiedResolutionException: Unsatisfied dependency for type com.example.blog.PostService and qualifiers [@Default]
- injection target: com.example.blog.PostResource#postService
The following classes match by type, but have been skipped during discovery:
- com.example.blog.PostService has no bean defining annotation (scope, stereotype, etc.)
Последняя строка прямо называет причину: у класса нет аннотации, определяющей бин. Добавьте @ApplicationScoped.