If for some reasons we cannot use Session Scoped beans (last tutorial) then we have another option of working with low level Servlet API to maintain session attributes. In this example we are going to use jakarta.servlet.http.HttpSession as a controller method argument. Spring supports this argument and it is never null. In the background, Spring uses HttpServletRequest#getSession() (check out ServletRequestMethodArgumentResolver #resolveArgument() method) which returns the session associated with the current request, or if the request does not have a session, a new session object is created.
Example
@Controller
public class MyController {
@ResponseBody
@RequestMapping(value = "/", produces = MediaType.TEXT_PLAIN_VALUE)
public String handler(HttpSession httpSession) {
String sessionKey = "sessionCounter";
Object counter = httpSession.getAttribute(sessionKey);
long counterLong = counter == null ? 1L :
((Long) counter) + 1;
httpSession.setAttribute(sessionKey, counterLong);
return "session counter : " + counterLong + "\nsession id: " + httpSession.getId();
}
}
Output
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
Refreshing above page multiple times will increase the counter with same session id, until the session expires.
Integration Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.assertj.MockMvcTester; // New in Spring 6.2+ / 7
import org.springframework.web.context.WebApplicationContext;
import java.io.UnsupportedEncodingException;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {MyWebConfig.class})
@WebAppConfiguration
public class MyControllerIntegrationTest {
@Autowired
private WebApplicationContext wac;
private MockMvcTester mockMvc;
@BeforeEach
public void setup() {
// MockMvcTester can be initialized from a WebApplicationContext
this.mockMvc = MockMvcTester.from(this.wac);
}
@Test
public void firstRequest_ShouldReturnCounterOne() {
assertThat(mockMvc.get().uri("/").exchange())
.hasStatusOk()
.bodyText().contains("session counter : 1", "session id:");
}
@Test
public void subsequentRequests_ShouldIncrementCounter() {
MockHttpSession session = new MockHttpSession();
// First request
assertThat(mockMvc.get().uri("/").session(session).exchange())
.hasStatusOk()
.bodyText().contains("session counter : 1");
// Second request
assertThat(mockMvc.get().uri("/").session(session).exchange())
.hasStatusOk()
.bodyText().contains("session counter : 2");
// Third request
assertThat(mockMvc.get().uri("/").session(session).exchange())
.hasStatusOk()
.bodyText().contains("session counter : 3");
}
@Test
public void differentSessions_ShouldHaveIndependentCounters() {
MockHttpSession sessionA = new MockHttpSession();
MockHttpSession sessionB = new MockHttpSession();
assertThat(mockMvc.get().uri("/").session(sessionA).exchange())
.bodyText().contains("session counter : 1");
assertThat(mockMvc.get().uri("/").session(sessionA).exchange())
.bodyText().contains("session counter : 2");
assertThat(mockMvc.get().uri("/").session(sessionB).exchange())
.bodyText().contains("session counter : 1");
}
@Test
public void response_ShouldContainSessionId() {
MockHttpSession session = new MockHttpSession();
assertThat(mockMvc.get().uri("/").session(session).exchange())
.hasStatusOk()
.bodyText().contains("session id: " + session.getId());
}
@Test
public void differentSessions_ShouldHaveDifferentSessionIds() {
MockHttpSession sessionA = new MockHttpSession();
MockHttpSession sessionB = new MockHttpSession();
var resultA = mockMvc.get().uri("/").session(sessionA).exchange();
var resultB = mockMvc.get().uri("/").session(sessionB).exchange();
try {
assertThat(resultA.getResponse().getContentAsString())
.isNotEqualTo(resultB.getResponse().getContentAsString());
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
}
mvn clean test -Dtest=MyControllerIntegrationTest Output$ mvn clean test -Dtest=MyControllerIntegrationTest [INFO] Scanning for projects... [INFO] [INFO] ----------< com.logicbig.example:http-session-param-example >----------- [INFO] Building http-session-param-example 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ http-session-param-example --- [INFO] Deleting D:\example-projects\spring-mvc\session-handling\http-session-param-example\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ http-session-param-example --- [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\session-handling\http-session-param-example\src\main\resources [INFO] [INFO] --- compiler:3.15.0:compile (default-compile) @ http-session-param-example --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 3 source files with javac [debug target 25] to target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ http-session-param-example --- [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\session-handling\http-session-param-example\src\test\resources [INFO] [INFO] --- compiler:3.15.0:testCompile (default-testCompile) @ http-session-param-example --- [INFO] Recompiling the module because of changed dependency. [INFO] Compiling 1 source file with javac [debug target 25] to target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ http-session-param-example --- [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.MyControllerIntegrationTest [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.938 s -- in com.logicbig.example.MyControllerIntegrationTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 11.527 s [INFO] Finished at: 2026-03-31T12:10:32+08:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 2 ms INFO: Completed initialization in 1 ms INFO: Completed initialization in 2 ms INFO: Completed initialization in 3 ms INFO: Completed initialization in 4 ms
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.9.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)
- 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)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- JDK 25
- Maven 3.9.11
|