Encodera

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

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

Оглавление

Области видимости

Область видимости (scope) определяет, сколько живёт бин и кто его делит. Счётчик просмотров должен быть общим для всех посетителей, история чтения — своей у каждого, а данные текущего запроса — исчезать после ответа. Всё это задаётся одной аннотацией на классе.

Виды областей

АннотацияСколько живёт объектПример
@ApplicationScopedПока работает приложение, один на всехХранилище, сервисы, счётчики
@SessionScopedПока жива HTTP-сессия посетителяКорзина, история просмотров
@RequestScopedОдин HTTP-запросДанные текущего запроса
@DependentСтолько же, сколько объект, куда его внедрилиВспомогательные объекты

Общий счётчик

// src/main/java/com/example/blog/ViewCounter.java
package com.example.blog;
 
import jakarta.enterprise.context.ApplicationScoped;
import java.util.concurrent.atomic.AtomicLong;
 
@ApplicationScoped
public class ViewCounter {
 
    private final AtomicLong total = new AtomicLong();
 
    public void increment() {
        total.incrementAndGet();
    }
 
    public long total() {
        return total.get();
    }
}

Объект один на всё приложение, и его вызывают из разных потоков одновременно, поэтому вместо long используется потокобезопасный AtomicLong.

История посетителя

// src/main/java/com/example/blog/ReadingHistory.java
package com.example.blog;
 
import jakarta.enterprise.context.SessionScoped;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
 
@SessionScoped
public class ReadingHistory implements Serializable {
 
    private final List<Long> viewed = new ArrayList<>();
 
    public void add(long postId) {
        if (!viewed.contains(postId)) {
            viewed.add(postId);
        }
    }
 
    public List<Long> viewed() {
        return List.copyOf(viewed);
    }
}

Для каждой HTTP-сессии контейнер создаёт свой объект. Сессия привязана к cookie JSESSIONID, который сервер выдаёт браузеру.

Используем оба бина

В ресурсе записей отмечаем каждый просмотр:

// src/main/java/com/example/blog/PostResource.java (фрагмент)
@Inject
ViewCounter counter;
 
@Inject
ReadingHistory history;
 
@GET
@Path("/{id}")
public Post show(@PathParam("id") long id) {
    Post post = store.get(id);
    counter.increment();
    history.add(id);
    return post;
}

А новый ресурс показывает историю и общий счётчик:

// src/main/java/com/example/blog/HistoryResource.java
package com.example.blog;
 
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
 
@Path("/history")
@Produces(MediaType.APPLICATION_JSON)
public class HistoryResource {
 
    public record History(List<Long> viewed, long totalViews) {
    }
 
    @Inject
    ReadingHistory history;
 
    @Inject
    ViewCounter counter;
 
    @GET
    public History show() {
        return new History(history.viewed(), counter.total());
    }
}

Проверка: Анна и Олег

Флаги curl -c и -b сохраняют и отправляют cookie — так curl ведёт себя как браузер. Анна читает записи 1 и 2, Олег — только 2:

curl -s -c anna.txt -b anna.txt http://localhost:8080/blog/api/posts/1
curl -s -c anna.txt -b anna.txt http://localhost:8080/blog/api/posts/2
curl -s -c oleg.txt -b oleg.txt http://localhost:8080/blog/api/posts/2
curl -b anna.txt http://localhost:8080/blog/api/history
curl -b oleg.txt http://localhost:8080/blog/api/history
{"totalViews":3,"viewed":[1,2]}
{"totalViews":3,"viewed":[2]}

Счётчик просмотров общий — 3 у обоих. История у каждого своя, потому что у Анны и Олега разные сессии.

Частая ошибка

Забыть implements Serializable у бина @SessionScoped. Сервер может сохранять сессии на диск или передавать между узлами кластера, поэтому CDI требует, чтобы такие бины были сериализуемыми. Без интерфейса приложение не запустится:

WELD-000072: Bean declaring a passivating scope must be passivation capable.  Bean:  Managed Bean [class com.example.blog.ReadingHistory] with qualifiers [@Any @Default]

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

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