SessionThemeResolver is similar to CookieThemeResolver (last example), with one difference: the user-selected theme name is stored in the Servlet's HttpSession object on the server side, instead of in a cookie on the browser side.
Note: As of Spring Framework 6.0, the entire theme-resolution API — ThemeResolver, SessionThemeResolver, CookieThemeResolver, FixedThemeResolver, and ThemeChangeInterceptor — is deprecated in favor of using CSS directly, with no direct replacement provided. These classes have now been removed outright in Spring Framework 7.0. This tutorial's code will not compile/run against Spring Framework 7.x or Spring Boot 4.
Example
We'll reuse the previous example and replace CookieThemeResolver with SessionThemeResolver. No other changes are needed.
Java Config class
package com.logicbig.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.ThemeResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.theme.SessionThemeResolver;
import org.springframework.web.servlet.theme.ThemeChangeInterceptor;
@EnableWebMvc
@Configuration
@ComponentScan
public class MyWebConfig implements WebMvcConfigurer {
@Bean(name = DispatcherServlet.THEME_RESOLVER_BEAN_NAME)
public ThemeResolver customThemeResolver() {
SessionThemeResolver ctr = new SessionThemeResolver();
ctr.setDefaultThemeName(ThemeInfo.DefaultThemeInfo.getThemeName());
return ctr;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
ThemeChangeInterceptor themeChangeInterceptor = new ThemeChangeInterceptor();
themeChangeInterceptor.setParamName("themeName");
registry.addInterceptor(themeChangeInterceptor);
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp().prefix("/WEB-INF/views/").suffix(".jsp");
}
}
Monitoring Session attribute changes
We'll also use the Servlet's HttpSessionAttributeListener implementation to observe when the theme name attribute is added to the session.
package com.logicbig.example;
import jakarta.servlet.annotation.WebListener;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpSessionAttributeListener;
import jakarta.servlet.http.HttpSessionBindingEvent;
@WebListener
public class MySessionAttributeListener implements HttpSessionAttributeListener {
@Override
public void attributeAdded(HttpSessionBindingEvent event) {
print("attributeAdded", event);
}
@Override
public void attributeRemoved(HttpSessionBindingEvent event) {
print("attributeRemoved", event);
}
@Override
public void attributeReplaced(HttpSessionBindingEvent event) {
print("attributeReplaced: ", event);
}
private void print(String msg, HttpSessionBindingEvent event) {
HttpSession session = event.getSession();
Object currentValue = null;
try {
currentValue = session.getAttribute(event.getName());
} catch (IllegalStateException e) {
}
System.out.printf(
"%s: name=%s, value=%s, currentValue=%s%n",
msg, event.getName(), event.getValue(), currentValue);
}
}
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
Output
The output is the same as in the last example, except that no cookie is sent to the client browser. Instead, we'll see the following printed by our HttpSessionAttributeListener on the server console.
When we first select and submit the theme:
attributeAdded: name=org.springframework.web.servlet.theme.SessionThemeResolver.THEME, value=ocean-theme, currentValue=ocean-theme
Subsequent theme selections will produce:
attributeReplaced: : name=org.springframework.web.servlet.theme.SessionThemeResolver.THEME, value=ocean-theme, currentValue=metal-theme
attributeReplaced: : name=org.springframework.web.servlet.theme.SessionThemeResolver.THEME, value=metal-theme, currentValue=ocean-theme
At the end of the session (e.g., on session timeout):
attributeRemoved: name=org.springframework.web.servlet.theme.SessionThemeResolver.THEME, value=ocean-theme, currentValue=null
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.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.theme.SessionThemeResolver;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = MyWebConfig.class)
public class ThemeControllerTest {
@Autowired private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@BeforeEach
public void setUp() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void defaultThemeIsUsedWhenSessionHasNoSelection() throws Exception {
mockMvc
.perform(get("/"))
.andExpect(status().isOk())
.andExpect(view().name("index"))
.andExpect(
model()
.attribute(
"themeInfo",
hasProperty("themeName", is(ThemeInfo.DefaultThemeInfo.getThemeName()))))
.andExpect(
model().attribute("themeChoices", allOf(hasKey("metal-theme"), hasKey("ocean-theme"))));
}
@Test
public void themeChangeInterceptorStoresSelectedThemeInSession() throws Exception {
MockHttpSession session = new MockHttpSession();
mockMvc
.perform(post("/").session(session).param("themeName", "ocean-theme"))
.andExpect(status().isOk())
.andExpect(view().name("index"));
assertThat(
session.getAttribute(SessionThemeResolver.THEME_SESSION_ATTRIBUTE_NAME),
is((Object) "ocean-theme"));
mockMvc
.perform(get("/").session(session))
.andExpect(status().isOk())
.andExpect(model().attribute("themeInfo", hasProperty("themeName", is("ocean-theme"))));
}
@Test
public void themeSelectionDoesNotLeakAcrossSeparateSessions() throws Exception {
MockHttpSession firstSession = new MockHttpSession();
MockHttpSession secondSession = new MockHttpSession();
mockMvc
.perform(post("/").session(firstSession).param("themeName", "ocean-theme"))
.andExpect(status().isOk());
mockMvc
.perform(get("/").session(secondSession))
.andExpect(status().isOk())
.andExpect(
model()
.attribute(
"themeInfo",
hasProperty("themeName", is(ThemeInfo.DefaultThemeInfo.getThemeName()))));
}
}
Example ProjectDependencies and Technologies Used: - spring-webmvc 6.0.0 (Spring Web MVC)
Version Compatibility: 4.1.0.RELEASE - 6.2.19 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 6.0.0 (Spring TestContext Framework)
- jakarta.servlet-api 6.0.0 (Jakarta Servlet API documentation)
- junit-jupiter-engine 5.8.2 (Module "junit-jupiter-engine" of JUnit 5)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- JDK 17
- Maven 3.9.11
|