A backing object can be autowired/injected to a Spring MVC controller with predefined scopes. These scopes are 'request', 'session' and 'application' scopes.
To understand how to use session scope object, let's see an example.
Example
To avoid narrower scope bean DI problem as we saw when injecting Prototype bean into a Singleton Bean, we will preferably use JSR 330 Provider approach. checkout other options here.
Registering a bean with session scope
package com.logicbig.example;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Scope;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import jakarta.servlet.http.HttpServletRequest;
@EnableWebMvc
@Configuration
@Import(MyViewConfig.class)
public class MyWebConfig {
@Bean
public TradeController tradeController () {
return new TradeController();
}
@Bean
@Scope(WebApplicationContext.SCOPE_SESSION)
public Visitor visitor(HttpServletRequest request){
return new Visitor(request.getRemoteAddr());
}
}
Injecting the session bean
We are going to inject our session bean wrapped in Provider interface:
@Controller
@RequestMapping("/trades")
public class TradeController {
@Autowired
private Provider<Visitor> visitorProvider;
@RequestMapping("/**")
public String handleRequestById (Model model, HttpServletRequest request) {
model.addAttribute("msg", "trades request, serving page " + request.getRequestURI());
visitorProvider.get()
.addPageVisited(request.getRequestURI());
return "traders-page";
}
}
JSP page
src/main/webapp/WEB-INF/views/traders-page.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<body>
<h2>Trades </h2>
<h3> Message : ${msg} <h3>
<br/>
<h4>Current visitor history</h4>
<p>ip : ${visitor.ip}:</p>
<c:forEach items="${visitor.pagesVisited}" var="page">
<p>${page}</p>
</c:forEach>
</body>
</html>
How it works?
The factory method of @Configuration class, MyWebConfig#visitor, is called at the beginning of each new HTTP session.
The returned object from the factory method is stored in HTTPSession and is kept there till the session ends.
visitorProvider.get() returns the same instance for the same session.
Output
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
The outputs shows that all visited urls are added to a single instance of our Visitor object, hence it is kept in the HttpSession object.
Integration Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
@SpringJUnitWebConfig(MyWebConfig.class)
public class TradeControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvcTester;
@BeforeEach
public void setup() {
mockMvcTester = MockMvcTester.from(wac);
}
@Test
public void testVisitorSessionPopulatedAcrossMultipleRequests() {
MockHttpSession session = new MockHttpSession();
// Visit /trades
mockMvcTester.get().uri("/trades").session(session).exchange()
.assertThat().hasStatusOk()
.hasViewName("traders-page");
// Visit /trades/info
mockMvcTester.get().uri("/trades/info").session(session).exchange()
.assertThat().hasStatusOk()
.hasViewName("traders-page");
// Visit /trades/info/other
mockMvcTester.get().uri("/trades/info/other").session(session)
.exchange()
.assertThat().hasStatusOk()
.hasViewName("traders-page");
// Retrieve the actual Visitor instance from the MockHttpSession
Visitor visitor = (Visitor) session.getAttribute("visitor");
assertThat(visitor)
.as("Visitor should have been created in session")
.isNotNull();
assertThat(visitor.getPagesVisited())
.hasSize(3)
.containsExactly("/trades",
"/trades/info",
"/trades/info/other");
}
@Test
public void testVisitorSessionIsolatedPerSession() {
MockHttpSession sessionA = new MockHttpSession();
MockHttpSession sessionB = new MockHttpSession();
// Session A visits two pages
mockMvcTester.get().uri("/trades").session(sessionA).exchange()
.assertThat().hasStatusOk();
mockMvcTester.get().uri("/trades/info").session(sessionA).exchange()
.assertThat().hasStatusOk();
// Session B visits one page
mockMvcTester.get().uri("/trades/info/other").session(sessionB)
.exchange().assertThat().hasStatusOk();
// Assert Session A
Visitor visitorA = (Visitor) sessionA.getAttribute("visitor");
assertThat(visitorA).isNotNull();
assertThat(visitorA.getPagesVisited())
.hasSize(2)
.containsExactly("/trades", "/trades/info");
// Assert Session B
Visitor visitorB = (Visitor) sessionB.getAttribute("visitor");
assertThat(visitorB).isNotNull();
assertThat(visitorB.getPagesVisited())
.hasSize(1)
.containsExactly("/trades/info/other");
}
}
mvn test -Dtest="TradeControllerTest" Output$ mvn test -Dtest="TradeControllerTest" [INFO] Scanning for projects... [INFO] [INFO] ---------< com.logicbig.example:spring-session-scoped-object >---------- [INFO] Building spring-session-scoped-object 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ spring-session-scoped-object --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-session-scoped-object\src\main\resources [INFO] [INFO] --- compiler:3.15.0:compile (default-compile) @ spring-session-scoped-object --- [INFO] Nothing to compile - all classes are up to date. [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-session-scoped-object --- [WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent! [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\spring-session-scoped-object\src\test\resources [INFO] [INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ spring-session-scoped-object --- [INFO] Nothing to compile - all classes are up to date. [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ spring-session-scoped-object --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [WARNING] file.encoding cannot be set as system property, use <argLine>-Dfile.encoding=...</argLine> instead [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.TradeControllerTest [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.651 s -- in com.logicbig.example.TradeControllerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.164 s [INFO] Finished at: 2026-03-31T12:07:09+08:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 1 ms INFO: Completed initialization in 1 ms
Other ways to handle session attributes
- Using @SessionAttributes (example here).
- Using
HttpSession as a controller method argument (next tutorial).
Example ProjectExample ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.3.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 7.0.6 (Spring TestContext Framework)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- jakarta.servlet.jsp.jstl 3.0.1 (Jakarta Standard Tag Library Implementation)
- jakarta.inject-api 2.0.1 (Jakarta Dependency Injection)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- assertj-core 3.26.3 (Rich and fluent assertions for testing in Java)
- JDK 25
- Maven 3.9.11
|