Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

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.

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.