Skip to content

6. Data Scopes

A web application serves many users at once, and each of them makes numerous requests. Data can therefore be shared at three levels, known as its scope:

  • application scope: the data is shared by all users throughout the server’s lifetime (the list of doctors, for example);
  • request scope: the data exists only for the duration of a request (a view’s model);
  • client scope: the data is specific to a user and must persist from one request to the next (the selected language, the logged-in user).

Web applications have long used sessions for this last scope: a memory space, maintained by the server, specific to each browser, and retrieved via an identification cookie ([JSESSIONID] in [Java]). [Spring MVC] still uses this by default for Flash attributes (Example 20). However, the case study does not use it, nor do the examples in this document (except for the Flash attributes in a few examples, such as Examples 20 and 21): client-side data is stored by the browser in cookies. The server does not store anything between two requests from the same user: it is said to be stateless ([stateless]). This is what makes it possible, for example, to run multiple identical servers behind a load balancer. The trade-off: a cookie is under the user’s control, and the user can read and modify it. Therefore, only non-sensitive data—or data signed or encrypted by the server, which can then detect any modifications—is stored there: this is the case with the authentication token (Chapter 8).

On the server side, the application and request scopes correspond to the scopes of the beans in [Spring] (see Example 06). A bean is created and maintained by [Spring]; its scope specifies how many instances are created and for how long. Three of these are of interest to us:

Annotation
Scope
One instance...
(none)
singleton
for the entire lifetime of the application, shared by all requests. This is the default scope
[@RequestScope]
request
per request HTTP, shared by all who request it during that request
[@Scope("prototype")]
prototype
Each time the bean is requested from [Spring]: two requests, two instances

These are the lifetimes [Singleton], [Scoped], and [Transient] of [ASP.NET Core]. There is also a session scope ([@SessionScope]), which we will not use.

6.1. Example 23: Three scopes

23-scopes/
├── src/main/java/example23/
│   ├── config/
│   │   ├── StartFilter.java
│   │   └── WebConfig.java
│   ├── controllers/
│   │   └── ScopesController.java
│   ├── services/
│   │   ├── CounterService.java
│   │   ├── ToolService.java
│   │   └── RequestService.java
│   └── Example23Application.java
├── src/main/resources/
│   ├── templates/scopes/
│   │   └── scopes.html
│   └── application.properties
└── pom.xml

The files [pom.xml], [application.properties], [Example23Application.java], and [WebConfig.java] (which simply serves the files from [Bootstrap]) are the ones from the previous examples.

6.1.1. The three beans

The [services/CounterService.java] file:

package example23.services;

import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.stereotype.Service;

// APPLICATION scope (singleton, the default scope of beans): a single
// instance, shared by ALL the requests of ALL the users
@Service
public class CounterService {

    // several requests can run AT THE SAME TIME: AtomicInteger
    // guarantees that two simultaneous increments are not lost
    private final AtomicInteger total = new AtomicInteger();

    public int increment() {
        return total.incrementAndGet();
    }
}
  • line 9: a bean, with no further specification: it is a singleton. Its field [total] is therefore application-scoped;
  • Lines 14–18: A web server processes multiple requests simultaneously across multiple threads ([threads]). If two requests execute [total++] at the same time, they may both read the same value, and an increment is lost. A [AtomicInteger] is an integer that the [incrementAndGet] method reads, adds to, and writes back in a single, indivisible operation. A singleton bean must always be wary of such concurrent accesses.

The file [services/RequestService.java]:

package example23.services;

import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.stereotype.Service;
import org.springframework.web.context.annotation.RequestScope;

// REQUEST scope (@RequestScope): a NEW instance for each HTTP request,
// shared by all those who ask for it DURING this request
@Service
@RequestScope
public class RequestService {

    // static: a counter shared by all the instances
    private static final AtomicInteger CREATED_INSTANCES = new AtomicInteger();

    private final int number = CREATED_INSTANCES.incrementAndGet();

    public int getNumber() {
        return number;
    }
}
  • line 11: the annotation [@RequestScope] gives the bean the "request" scope;
  • line 15: a [static] field belongs to the class, not to an instance; it is shared by all instances. It counts the number of instances created;
  • line 17: Each instance is assigned a number upon creation. Since the bean has request scope, the number changes with each request.

The file [services/ToolService.java]:

package example23.services;

import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Service;

// PROTOTYPE scope: a NEW instance EACH time the bean is requested from Spring
@Service
@Scope("prototype")
public class ToolService {

    private static final AtomicInteger CREATED_INSTANCES = new AtomicInteger();

    private final int number = CREATED_INSTANCES.incrementAndGet();

    public int getNumber() {
        return number;
    }
}

The same instance counter, for a bean with scope [prototype] (line 10).

6.1.2. Request-scoped data: the request attributes

The file [config/StartFilter.java]:

package example23.config;

import java.io.IOException;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

// a filter (see chapter 7): run for each request, BEFORE Spring MVC.
// REQUEST scope: the attributes of the request are born and die with it.
@Component
public class StartFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain next)
            throws ServletException, IOException {
        request.setAttribute("start", LocalTime.now().format(DateTimeFormatter.ofPattern("HH:mm:ss.SSS")));
        next.doFilter(request, response); // we hand over to the rest
    }
}
  • Lines 17–18: a servlet filter (the next chapter describes them in detail). It processes each request before [Spring MVC]. The [OncePerRequestFilter] class of [Spring] ensures that it is executed only once per request; the [@Component] annotation is sufficient for [Spring Boot] to add it to the filter chain;
  • Line 23: The filter stores the request’s reception time in a request attribute. The [HttpServletRequest] object contains a dictionary of attributes, which is created and destroyed with the request: anything that processes it (filters, controller, view) can read from and write to it. This is the equivalent of [HttpContext.Items] from [ASP.NET Core];
  • line 24: the filter passes control to the next step: the other filters, then [Spring MVC].

6.1.3. The controller

The file [controllers/ScopesController.java]:

package example23.controllers;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.GetMapping;

import example23.services.CounterService;
import example23.services.ToolService;
import example23.services.RequestService;
import jakarta.servlet.http.HttpServletResponse;

// a controller is itself a SINGLETON bean: it is created only once
@Controller
public class ScopesController {

    private final CounterService counter;
    // a request-scoped bean injected into a singleton: Spring injects a
    // "proxy", which forwards each call to the instance of the CURRENT request
    private final RequestService request;
    // a prototype injected into a singleton: created ONCE, with the controller
    private final ToolService injectedTool;
    // to get a new prototype each time: ask Spring for it
    private final ObjectProvider<ToolService> tools;

    public ScopesController(CounterService counter, RequestService request, ToolService injectedTool,
            ObjectProvider<ToolService> tools) {
        this.counter = counter;
        this.request = request;
        this.injectedTool = injectedTool;
        this.tools = tools;
    }

    public record ScopesModel(int total, int instance, int injectedTool, int requestedTool, int myVisits) {
    }

    @GetMapping("/")
    public String scopes(@CookieValue(name = "myVisits", required = false) String cookie,
            HttpServletResponse response, Model model) {
        // CLIENT scope: a piece of data kept by the BROWSER (cookie),
        // sent back to the server with each request
        int myVisits = toInteger(cookie) + 1;
        ResponseCookie newCookie = ResponseCookie.from("myVisits", String.valueOf(myVisits))
                .path("/").httpOnly(true).sameSite("Lax").build();
        response.addHeader(HttpHeaders.SET_COOKIE, newCookie.toString());
        model.addAttribute("model", new ScopesModel(counter.increment(), request.getNumber(),
                injectedTool.getNumber(), tools.getObject().getNumber(), myVisits));
        return "scopes/scopes";
    }

    // "41" -> 41; null or "abc" -> 0
    private static int toInteger(String text) {
        try {
            return Integer.parseInt(text);
        } catch (NumberFormatException e) {
            return 0;
        }
    }
}

Let’s comment on this code:

  • Lines 16–17: A controller is itself a bean, and therefore a singleton: [Spring] creates it only once and injects its dependencies at that moment, once and for all (lines 29–35). This is where things get interesting;
  • line 20: a singleton injected into a singleton—nothing out of the ordinary;
  • line 23: a request-scoped bean injected into a singleton. At startup, there are no active requests, and therefore no instance of [RequestService] to inject. Instead, [Spring] injects a proxy (an intermediary): an object that has the same methods as [RequestService], and which forwards each call to the instance of the current request, created as needed. This is what [@RequestScope] does;
  • line 25: a prototype bean injected into a singleton. [Spring] creates an instance... just one, during injection. The controller will retain it for its entire lifetime: this field never changes. This is the pitfall of prototype scope: it creates a new instance only with each request to the [Spring] container, and a singleton requests its dependencies only once;
  • line 27: to obtain a new instance each time, the controller receives a [ObjectProvider<ToolService>]—an object capable of requesting a [ToolService] from [Spring] whenever needed, via its [getObject()] method (line 50);
  • line 37: the view model, a record (see Example 03);
  • Line 41: The client scope, a counter stored in a cookie. The annotation [@CookieValue] links the parameter to the cookie [myVisits] sent by the browser; [required = false]: the parameter is set to [null] if the cookie is missing (on the first visit);
  • line 45: the browser counter, incremented by 1. The method [toInteger] (lines 55–61) converts the cookie text to an integer; [Integer.parseInt] throws a [NumberFormatException] if the text is not an integer, or if it is equal to [null]: the counter then resets to 0;
  • lines 46–48: the cookie is re-set with its new value. [ResponseCookie] is a class of [Spring] that generates the value of a [Set-Cookie] header: the methods that set its characteristics (path, [HttpOnly], [SameSite]) are called sequentially, then [build()] creates the object. The header is added to the response by [response.addHeader];
  • lines 49–50: Values are passed to the view: the application counter, the instance number of [RequestService] for the current request, the instance number of the injected prototype, the instance number of a prototype requested at that moment, and the client counter.

6.1.4. The view

The file [templates/scopes/scopes.html]:

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
  <meta charset="utf-8">
  <title>Scopes</title>
  <link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css">
</head>
<body>
  <div class="container py-3" th:object="${model}">
    <h1 class="h3">The scopes of data</h1>
    <!--/* @requestService, @toolService: the view also asks Spring for the beans,
           during the SAME request */-->
    <ul>
      <li>application: [[*{total}]] visit(s) to the site, all browsers combined</li>
      <li>request: instance no. [[*{instance}]] of RequestService (no. [[${@requestService.number}]] in the view),
        request received at [[${start}]]</li>
      <li>prototype: instance no. [[*{injectedTool}]] (injected into the controller),
        no. [[*{requestedTool}]] (requested by the controller), no. [[${@toolService.number}]] (requested by the view)</li>
      <li>client: [[*{myVisits}]] visit(s) from this browser</li>
    </ul>
  </div>
</body>
</html>
  • line 9: [th:object] (see examples 18 and 19) refers to the object [model]; the expressions [*{...}] in the child tags refer to its properties: [*{total}] is equal to [${model.total}];
  • line 14: [[[...]]] inserts the value of an expression into the text, escaped, just as [th:text] would (see example 14);
  • Lines 15 and 18: The expression [${@name}] refers to the bean with that name (see Example 16). The view, in turn, requests the beans [requestService] and [toolService] from [Spring] during the same request as the action: we will be able to compare the instances received by the controller and by the view;
  • line 16: the request attribute set by the filter. For [Thymeleaf], the request attributes are variables just like any others.

6.1.5. Execution

A page, at the URLhttp://localhost:5000/:

Image

Let’s simulate two browsers using [curl] and two cookie files, [a.txt] and [b.txt]: The [-b] option sends the cookies from the file, while the [-c] option saves the cookies set by the server to the file. Browser A made two requests, then browser B made one, and then A made one last request (the server has just been restarted):

1
2
3
4
curl -c a.txt -b a.txt http://localhost:5000/
curl -c a.txt -b a.txt http://localhost:5000/
curl -c b.txt -b b.txt http://localhost:5000/
curl -c a.txt -b a.txt http://localhost:5000/
browser A:
<li>application: 1 visit(s) to the site, all browsers combined</li>
<li>request: instance no. 1 of RequestService (no. 1 in the view),
request received at 18:47:52.627</li>
<li>prototype: instance no. 1 (injected into the controller),
no. 2 (requested by the controller), no. 3 (requested by the view)</li>
<li>client: 1 visit(s) from this browser</li>

browser A:
<li>application: 2 visit(s) to the site, all browsers combined</li>
<li>request: instance no. 2 of RequestService (no. 2 in the view),
request received at 18:47:53.015</li>
<li>prototype: instance no. 1 (injected into the controller),
no. 4 (requested by the controller), no. 5 (requested by the view)</li>
<li>client: 2 visit(s) from this browser</li>

browser B:
<li>application: 3 visit(s) to the site, all browsers combined</li>
<li>request: instance no. 3 of RequestService (no. 3 in the view),
request received at 18:47:53.099</li>
<li>prototype: instance no. 1 (injected into the controller),
no. 6 (requested by the controller), no. 7 (requested by the view)</li>
<li>client: 1 visit(s) from this browser</li>

browser A:
<li>application: 4 visit(s) to the site, all browsers combined</li>
<li>request: instance no. 4 of RequestService (no. 4 in the view),
request received at 18:47:53.133</li>
<li>prototype: instance no. 1 (injected into the controller),
no. 8 (requested by the controller), no. 9 (requested by the view)</li>
<li>client: 3 visit(s) from this browser</li>

(only the items in the list are shown). We observe that:

  • the application counter counts all visits: 1, 2, 3, 4;
  • each request has its own instance of [RequestService]: 1, 2, 3, 4. During a request, the controller (via its proxy) and the view access the same instance;
  • the prototype injected into the controller is always No. 1: created at startup with the controller, it will never be replaced. The requested prototypes are new each time: the controller and the view receive two different ones, and each request creates two new ones;
  • the client counter is unique to each browser: A has 3 visits, B has 1.

The file [a.txt], maintained by [curl], contains browser A’s cookie:

1
2
3
4
5
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_localhost    FALSE    /    FALSE    0    myVisits    3

This cookie is controlled by the client, which can send whatever value it wants:

curl -i -b "myVisits=41" http://localhost:5000/
HTTP/1.1 200
Set-Cookie: myVisits=42; Path=/; HttpOnly; SameSite=Lax
Content-Type: text/html;charset=UTF-8

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Scopes</title>
  <link rel="stylesheet" href="/bootstrap/css/bootstrap.min.css">
</head>
<body>
  <div class="container py-3">
    <h1 class="h3">The scopes of data</h1>

    <ul>
      <li>application: 5 visit(s) to the site, all browsers combined</li>
      <li>request: instance no. 5 of RequestService (no. 5 in the view),
        request received at 18:47:53.178</li>
      <li>prototype: instance no. 1 (injected into the controller),
        no. 10 (requested by the controller), no. 11 (requested by the view)</li>
      <li>client: 42 visit(s) from this browser</li>
    </ul>
  </div>
</body>
</html>

For a visitor counter, this is irrelevant. For sensitive data (the user’s identity, their role), the server must be able to detect tampering: Chapter 8 shows how.

A rule follows from these scopes: a bean must not retain a bean with a shorter scope. A singleton that directly received a request-scope instance would retain it indefinitely, well beyond the request for which it was created. [ASP.NET Core] checks for this rule at startup, in development mode, and refuses to start if it is violated. [Spring] takes a different approach: for the request scope, [@RequestScope] injects a proxy, which makes the injection safe; for the prototype scope, it performs no checks, and we’ve just seen the result: a “prototype” that behaves like a singleton. When a new instance is actually needed, it is requested from [Spring] via a [ObjectProvider].

In the case study:

Scope
Data
Where?
Application
lists of doctors and clients, dictionaries, pending CAPTCHAs, attempt counters
Singleton beans ([ApplicationModel], [Translator], [CaptchaService], [AttemptLimiterService]...); the business layer and data access are also stateless singletons
query
view model, logged-in user
the model ([Model]), supplemented by [CommonModel]; the security context of [Spring Security]
client
authentication token, language, flash message
cookies