Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Scopes

A scope defines how long a bean lives and who shares it. A view counter must be shared by all visitors, a reading history must belong to each visitor, and data of the current request must disappear after the response. All of this is set with one annotation on the class.

Kinds of scopes

AnnotationHow long the object livesExample
@ApplicationScopedWhile the application runs, one for everyoneStore, services, counters
@SessionScopedWhile the visitor's HTTP session is aliveCart, viewing history
@RequestScopedOne HTTP requestData of the current request
@DependentAs long as the object it was injected intoHelper objects

A shared counter

// 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();
    }
}

There is one object for the whole application, and it is called from different threads at the same time, so a thread-safe AtomicLong is used instead of long.

A visitor's history

// 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);
    }
}

The container creates a separate object for each HTTP session. The session is tied to the JSESSIONID cookie that the server gives to the browser.

Using both beans

In the posts resource we record every view:

// src/main/java/com/example/blog/PostResource.java (fragment)
@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;
}

And a new resource shows the history and the shared counter:

// 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());
    }
}

Checking it: Anna and Oleg

The curl flags -c and -b save and send cookies — this way curl behaves like a browser. Anna reads posts 1 and 2, Oleg reads only 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]}

The view counter is shared — 3 for both. Each history is separate because Anna and Oleg have different sessions.

Common mistake

Forgetting implements Serializable on a @SessionScoped bean. The server may save sessions to disk or move them between cluster nodes, so CDI requires such beans to be serializable. Without the interface the application does not start:

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

Thanks for reading!

Donate to the site — we work hard for you.