Close

Spring MVC - Store Model attributes in HTTP session with @SessionAttributes

[Last Updated: Apr 1, 2026]

The annotation @SessionAttributes declares which model attributes should be stored in the HTTP session for the duration of a controller's conversation, allowing those attributes to survive across multiple requests without being recreated.

The annotation SessionAttributes is used on class level. Typically it's used on the @Controller class. It's 'value' element is of type String[] whose values are the matching names used in @ModelAttribute either on method level or on handler's method parameter level.

Definition of SessionAttributes

Version: 7.0.6
 package org.springframework.web.bind.annotation;
 @Target({ ElementType.TYPE })
 @Retention(RetentionPolicy.RUNTIME)
 @Inherited
 @Documented
 public @interface SessionAttributes {
     @AliasFor("names")
     String[] value() default {}; 1
     @AliasFor("value")
     String[] names() default {}; 2
     Class<?>[] types() default {}; 3
 }
1Alias for #names.
2The names of session attributes in the model that should be stored in the session or some conversational storage. (Since 4.2)
3The types of session attributes in the model that should be stored in the session or some conversational storage.


Example

Let's consider following arrangement

@Controller
@SessionAttributes("visitor")
@RequestMapping("/trades")
public class TradeController {

    @ModelAttribute("visitor")
    public Visitor getVisitor (....) {
        return new Visitor(....);
    }
  ....
}

In above code, when a request comes in, the first thing Spring will do is to notice @SessionAttributes('visitor') and then attempt to find the value of 'visitor' in jakarta.servlet.http.HttpSession. If it doesn't find the value, then the method with @ModelAttribute having the same name 'visitor' (method getVisitor()) will be invoked. The returned value from such method will be used to populate the session with name 'visitor'.

Now that 'visitor' with it's value is already in HttpSession, let's consider following arrangement:

@Controller
@SessionAttributes("visitor")
@RequestMapping("/trades")
public class TradeController {

    @RequestMapping("/**")
    public String handleRequestById (@ModelAttribute("visitor") Visitor visitor,
                                     Model model,
                                     HttpServletRequest request) {
        model.addAttribute("msg", "trades request, serving page " +
                                                 request.getRequestURI());
        visitor.addPageVisited(request.getRequestURI());
        return "traders-page";
    }
   .......
}

On finding @ModelAttribute("visitor") in the target handler method, Spring will retrieve the value of 'visitor' from the session and will assign the value to the Visitor parameter and will invoke the method. At this step, Spring doesn't care how the session was populated with 'visitor', Whether it was populated using the last arrangement or some other way, it doesn't matter, Spring only requires the annotation @SessionAttributes('visitor'), the handler method parameter with @ModelAttribute("visitor") and the value of 'visitor' in HttpSession. If spring can't find it in the session and last arrangement is not available then following exception will be thrown:

org.springframework.web.HttpSessionRequiredException: Expected session attribute 'visitor'

In other words: when the target handler method is invoked first time in a given session, then method with @ModelAttribute('visitor) is invoked, the returned value is populated in HttpSession and the handler method with the same value is invoked. For the subsequent requests within the same HTTPSession, Spring retrieves the same object from the session and doesn't call the method with @ModelAttribute('visitor') again till the end of the session

Following is the complete controller class:

package com.logicbig.example;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;
import jakarta.servlet.http.HttpServletRequest;

@Controller
@SessionAttributes("visitor")
@RequestMapping
public class TradeController {

    @RequestMapping("/trades/**")
    public String handleRequestById(@ModelAttribute("visitor") Visitor visitor,
                                    Model model,
                                    HttpServletRequest request,
                                    SessionStatus sessionStatus) {

        model.addAttribute("msg",
                           "trades request, serving page " + request.getRequestURI());
        if (request.getRequestURI()
                   .endsWith("clear")) {
            sessionStatus.setComplete();
            return "clear-page";
        } else {
            visitor.addPageVisited(request.getRequestURI());
            return "traders-page";
        }
    }

    @ModelAttribute("visitor")
    public Visitor getVisitor(HttpServletRequest request) {
        return new Visitor(request.getRemoteAddr());
    }
}

In above example we are tracking the user visited pages within the same HTTP session. We are using the wildcard '/**' so that all request starting with 'trades' will map to this controller.


Removing Model Attribute from the session

Once the value returned from @ModelAttribute method is populated in the HTTPSession, it remains there till the end of the session.

We can, however, remove the attribute manually from the session by using org.springframework.web.bind.support.SessionStatus as handler method parameter and conditionally calling SessionStatus#setComplete in the handler method. This method signals that the session related processing is completed. Spring then removes the session attribute (using HTTPSession#removeAttribute).

@RequestMapping("/trades/**")
 public String handleRequestById (@ModelAttribute("visitor") Visitor visitor,
                                  Model model,
                                  HttpServletRequest request,
                                  SessionStatus sessionStatus) {

        model.addAttribute("msg", "trades request, serving page " +
                                                    request.getRequestURI());
        if (request.getRequestURI()
                   .endsWith("clear")) {
            sessionStatus.setComplete();
            return "clear-page";
        } else {
            visitor.addPageVisited(request.getRequestURI());
            return "traders-page";
        }
    }

Above example causes the Visitor object to be removed from HTTPSession, only if the requested URL ends with 'clear'. Within the same session, subsequent call to the same handler will re-create the session attribute again(@ModelAttribute method will be invoked again).



traders-page.jsp

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>

<a href="<%=request.getContextPath()%>/trades/clear">Clear History</a>
</body>
</html>



clear-page.jsp

src/main/webapp/WEB-INF/views/clear-page.jsp

<%@ page language="java"
    contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<html>
<body>
<p>Visitor history cleared</p>
<a href="<%=request.getContextPath()%>/trades">Go back</a>
</body>
</html>

Running example

To try examples, run embedded Jetty (configured in pom.xml of example project below):

mvn jetty:run



Clicking 'Clear History'

$ curl -s http://localhost:8080/trades


<html>
<body>
<h2>Trades </h2>
<h3> Message : trades request, serving page /trades <h3>
<br/>
<h4>Current visitor history</h4>
<p>ip : [0:0:0:0:0:0:0:1]:</p>

<p>/trades</p>


<a href="/trades/clear">Clear History</a>
</body>
</html>

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.MockMvc;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class TradeControllerTest {

    @Autowired
    private WebApplicationContext wac;

    private MockMvc mockMvc;

    @BeforeEach
    public void setup() {
        DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.wac);
        this.mockMvc = builder.build();
    }

    @Test
    public void testUserController() throws Exception {
        MockHttpSession session = new MockHttpSession();

        this.mockMvc.perform(get("/trades").session(session))
                    .andExpect(status().isOk());

        this.mockMvc.perform(get("/trades/today").session(session))
                    .andExpect(status().isOk());

        this.mockMvc.perform(get("/trades/yesterday").session(session))
                    .andExpect(status().isOk());

        Object attribute = session.getAttribute("visitor");
        assertTrue(attribute instanceof Visitor);
        Visitor visitor = (Visitor) attribute;
        assertEquals(3, visitor.getPagesVisited().size());
        assertEquals("/trades",
                     visitor.getPagesVisited().get(0));
        assertEquals("/trades/today",
                     visitor.getPagesVisited().get(1));
        assertEquals("/trades/yesterday",
                     visitor.getPagesVisited().get(2));

        this.mockMvc.perform(get("/trades/clear").session(session))
                    .andExpect(status().isOk());

        attribute = session.getAttribute("visitor");
        assertNull(attribute);

    }
}
mvn clean test -Dtest="TradeControllerTest"

Output

$ mvn clean test -Dtest="TradeControllerTest"
[INFO] Scanning for projects...
[WARNING]
[WARNING] Some problems were encountered while building the effective model for com.logicbig.example:spring-model-attribute-with-session:war:1.0-SNAPSHOT
[WARNING] 'build.plugins.plugin.version' for org.apache.maven.plugins:maven-war-plugin is missing. @ line 43, column 21
[WARNING]
[WARNING] It is highly recommended to fix these problems because they threaten the stability of your build.
[WARNING]
[WARNING] For this reason, future Maven versions might no longer support building such malformed projects.
[WARNING]
[INFO]
[INFO] ------< com.logicbig.example:spring-model-attribute-with-session >------
[INFO] Building spring-model-attribute-with-session 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ war ]---------------------------------
[INFO]
[INFO] --- clean:3.2.0:clean (default-clean) @ spring-model-attribute-with-session ---
[INFO] Deleting D:\example-projects\spring-mvc\spring-model-attribute-with-session\target
[INFO]
[INFO] --- resources:3.3.1:resources (default-resources) @ spring-model-attribute-with-session ---
[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-model-attribute-with-session\src\main\resources
[INFO]
[INFO] --- compiler:3.5.1:compile (default-compile) @ spring-model-attribute-with-session ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 5 source files to D:\example-projects\spring-mvc\spring-model-attribute-with-session\target\classes
[INFO]
[INFO] --- resources:3.3.1:testResources (default-testResources) @ spring-model-attribute-with-session ---
[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-model-attribute-with-session\src\test\resources
[INFO]
[INFO] --- compiler:3.5.1:testCompile (default-testCompile) @ spring-model-attribute-with-session ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to D:\example-projects\spring-mvc\spring-model-attribute-with-session\target\test-classes
[INFO]
[INFO] --- surefire:3.2.5:test (default-test) @ spring-model-attribute-with-session ---
[INFO] Using auto detected provider org.apache.maven.surefire.junit4.JUnit4Provider
[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: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.493 s -- in com.logicbig.example.TradeControllerTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.092 s
[INFO] Finished at: 2026-03-31T21:18:37+08:00
[INFO] ------------------------------------------------------------------------

Example Project

Dependencies and Technologies Used:

  • spring-webmvc 7.0.6 (Spring Web MVC)
     Version Compatibility: 3.2.9.RELEASE - 7.0.6Version List
    ×

    Version compatibilities of spring-webmvc with this example:

      javax.servlet-api:3.x
    • 3.2.9.RELEASE
    • 3.2.10.RELEASE
    • 3.2.11.RELEASE
    • 3.2.12.RELEASE
    • 3.2.13.RELEASE
    • 3.2.14.RELEASE
    • 3.2.15.RELEASE
    • 3.2.16.RELEASE
    • 3.2.17.RELEASE
    • 3.2.18.RELEASE
    • 4.0.0.RELEASE
    • 4.0.1.RELEASE
    • 4.0.2.RELEASE
    • 4.0.3.RELEASE
    • 4.0.4.RELEASE
    • 4.0.5.RELEASE
    • 4.0.6.RELEASE
    • 4.0.7.RELEASE
    • 4.0.8.RELEASE
    • 4.0.9.RELEASE
    • 4.1.0.RELEASE
    • 4.1.1.RELEASE
    • 4.1.2.RELEASE
    • 4.1.3.RELEASE
    • 4.1.4.RELEASE
    • 4.1.5.RELEASE
    • 4.1.6.RELEASE
    • 4.1.7.RELEASE
    • 4.1.8.RELEASE
    • 4.1.9.RELEASE
    • 4.2.0.RELEASE
    • 4.2.1.RELEASE
    • 4.2.2.RELEASE
    • 4.2.3.RELEASE
    • 4.2.4.RELEASE
    • 4.2.5.RELEASE
    • 4.2.6.RELEASE
    • 4.2.7.RELEASE
    • 4.2.8.RELEASE
    • 4.2.9.RELEASE
    • 4.3.0.RELEASE
    • 4.3.1.RELEASE
    • 4.3.2.RELEASE
    • 4.3.3.RELEASE
    • 4.3.4.RELEASE
    • 4.3.5.RELEASE
    • 4.3.6.RELEASE
    • 4.3.7.RELEASE
    • 4.3.8.RELEASE
    • 4.3.9.RELEASE
    • 4.3.10.RELEASE
    • 4.3.11.RELEASE
    • 4.3.12.RELEASE
    • 4.3.13.RELEASE
    • 4.3.14.RELEASE
    • 4.3.15.RELEASE
    • 4.3.16.RELEASE
    • 4.3.17.RELEASE
    • 4.3.18.RELEASE
    • 4.3.19.RELEASE
    • 4.3.20.RELEASE
    • 4.3.21.RELEASE
    • 4.3.22.RELEASE
    • 4.3.23.RELEASE
    • 4.3.24.RELEASE
    • 4.3.25.RELEASE
    • 4.3.26.RELEASE
    • 4.3.27.RELEASE
    • 4.3.28.RELEASE
    • 4.3.29.RELEASE
    • 4.3.30.RELEASE
    • 5.0.0.RELEASE
    • 5.0.1.RELEASE
    • 5.0.2.RELEASE
    • 5.0.3.RELEASE
    • 5.0.4.RELEASE
    • 5.0.5.RELEASE
    • 5.0.6.RELEASE
    • 5.0.7.RELEASE
    • 5.0.8.RELEASE
    • 5.0.9.RELEASE
    • 5.0.10.RELEASE
    • 5.0.11.RELEASE
    • 5.0.12.RELEASE
    • 5.0.13.RELEASE
    • 5.0.14.RELEASE
    • 5.0.15.RELEASE
    • 5.0.16.RELEASE
    • 5.0.17.RELEASE
    • 5.0.18.RELEASE
    • 5.0.19.RELEASE
    • 5.0.20.RELEASE
    • 5.1.0.RELEASE
    • 5.1.1.RELEASE
    • 5.1.2.RELEASE
    • 5.1.3.RELEASE
    • 5.1.4.RELEASE
    • 5.1.5.RELEASE
    • 5.1.6.RELEASE
    • 5.1.7.RELEASE
    • 5.1.8.RELEASE
    • 5.1.9.RELEASE
    • 5.1.10.RELEASE
    • 5.1.11.RELEASE
    • 5.1.12.RELEASE
    • 5.1.13.RELEASE
    • 5.1.14.RELEASE
    • 5.1.15.RELEASE
    • 5.1.16.RELEASE
    • 5.1.17.RELEASE
    • 5.1.18.RELEASE
    • 5.1.19.RELEASE
    • 5.1.20.RELEASE
    • 5.2.0.RELEASE
    • 5.2.1.RELEASE
    • 5.2.2.RELEASE
    • 5.2.3.RELEASE
    • 5.2.4.RELEASE
    • 5.2.5.RELEASE
    • 5.2.6.RELEASE
    • 5.2.7.RELEASE
    • 5.2.8.RELEASE
    • 5.2.9.RELEASE
    • 5.2.10.RELEASE
    • 5.2.11.RELEASE
    • 5.2.12.RELEASE
    • 5.2.13.RELEASE
    • 5.2.14.RELEASE
    • 5.2.15.RELEASE
    • 5.2.16.RELEASE
    • 5.2.17.RELEASE
    • 5.2.18.RELEASE
    • 5.2.19.RELEASE
    • 5.2.20.RELEASE
    • 5.2.21.RELEASE
    • 5.2.22.RELEASE
    • 5.2.23.RELEASE
    • 5.2.24.RELEASE
    • 5.2.25.RELEASE
    • 5.3.0
    • 5.3.1
    • 5.3.2
    • 5.3.3
    • 5.3.4
    • javax.servlet-api:4.x
    • 5.3.5
    • 5.3.6
    • 5.3.7
    • 5.3.8
    • 5.3.9
    • 5.3.10
    • 5.3.11
    • 5.3.12
    • 5.3.13
    • 5.3.14
    • 5.3.15
    • 5.3.16
    • 5.3.17
    • 5.3.18
    • 5.3.19
    • 5.3.20
    • 5.3.21
    • 5.3.22
    • 5.3.23
    • 5.3.24
    • 5.3.25
    • 5.3.26
    • 5.3.27
    • 5.3.28
    • 5.3.29
    • 5.3.30
    • 5.3.31
    • 5.3.32
    • 5.3.33
    • 5.3.34
    • 5.3.35
    • 5.3.36
    • 5.3.37
    • 5.3.38
    • 5.3.39
    • javax.* -> jakarta.*
      jakarta.servlet-api:6.x
      Java 17 min
    • 6.0.0
    • 6.0.1
    • 6.0.2
    • 6.0.3
    • 6.0.4
    • 6.0.5
    • 6.0.6
    • 6.0.7
    • 6.0.8
    • 6.0.9
    • 6.0.10
    • 6.0.11
    • 6.0.12
    • 6.0.13
    • 6.0.14
    • 6.0.15
    • 6.0.16
    • 6.0.17
    • 6.0.18
    • 6.0.19
    • 6.0.20
    • 6.0.21
    • 6.0.22
    • 6.0.23
    • 6.1.0
    • 6.1.1
    • 6.1.2
    • 6.1.3
    • 6.1.4
    • 6.1.5
    • 6.1.6
    • 6.1.7
    • 6.1.8
    • 6.1.9
    • 6.1.10
    • 6.1.11
    • 6.1.12
    • 6.1.13
    • 6.1.14
    • 6.1.15
    • 6.1.16
    • 6.1.17
    • 6.1.18
    • 6.1.19
    • 6.1.20
    • 6.1.21
    • 6.2.0
    • 6.2.1
    • 6.2.2
    • 6.2.3
    • 6.2.4
    • 6.2.5
    • 6.2.6
    • 6.2.7
    • 6.2.8
    • 6.2.9
    • 6.2.10
    • 6.2.11
    • 6.2.12
    • 6.2.13
    • 6.2.14
    • 6.2.15
    • 6.2.16
    • 6.2.17
    • 7.0.0
    • 7.0.1
    • 7.0.2
    • 7.0.3
    • 7.0.4
    • 7.0.5
    • 7.0.6

    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)
  • junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
  • JDK 25
  • Maven 3.9.11

Spring MVC - @SessionAttributes with @ModelAttribute Example Select All Download
  • spring-model-attribute-with-session
    • src
      • main
        • java
          • com
            • logicbig
              • example
                • TradeController.java
          • webapp
            • WEB-INF
              • views
        • test
          • java
            • com
              • logicbig
                • example

    output:

    If you are using the same machine then you will get ip as 0:0:0:0:0:0:0:1 which is considered equivalent to 127.0.0.1. More info here

    On another machine in the local network:


    Clearing history:



    Note:

    If we close our browser and open it again with the same url, we will see the previous visitor history has gone. That demonstrates how @SessionAttributes works. If we remove the @SessionAttributes annotation from the controller and we will see that with each request, a new Visitor object is created each time.

    See Also

    Join