Области видимости
Область видимости (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]