Store known attribute names in session (for distributed sessions)

Closes gh-30463
This commit is contained in:
Juergen Hoeller 2024-02-06 16:46:11 +01:00
parent 4ed337247c
commit 80949eb30f
2 changed files with 64 additions and 7 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2024 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.web.method.annotation; package org.springframework.web.method.annotation;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -26,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.SessionAttributes; import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionAttributeStore; import org.springframework.web.bind.support.SessionAttributeStore;
import org.springframework.web.bind.support.SessionStatus; import org.springframework.web.bind.support.SessionStatus;
@ -48,6 +50,16 @@ import org.springframework.web.context.request.WebRequest;
*/ */
public class SessionAttributesHandler { public class SessionAttributesHandler {
/**
* Key for known-attribute-names storage (a String array) as a session attribute.
* <p>This is necessary for consistent handling of type-based session attributes
* in distributed session scenarios where handler methods from the same class
* may get invoked on different servers.
* @since 6.1.4
*/
public static final String SESSION_KNOWN_ATTRIBUTE = SessionAttributesHandler.class.getName() + ".KNOWN";
private final Set<String> attributeNames = new HashSet<>(); private final Set<String> attributeNames = new HashSet<>();
private final Set<Class<?>> attributeTypes = new HashSet<>(); private final Set<Class<?>> attributeTypes = new HashSet<>();
@ -96,12 +108,12 @@ public class SessionAttributesHandler {
*/ */
public boolean isHandlerSessionAttribute(String attributeName, Class<?> attributeType) { public boolean isHandlerSessionAttribute(String attributeName, Class<?> attributeType) {
Assert.notNull(attributeName, "Attribute name must not be null"); Assert.notNull(attributeName, "Attribute name must not be null");
if (this.attributeNames.contains(attributeName) || this.attributeTypes.contains(attributeType)) { if (this.attributeTypes.contains(attributeType)) {
this.knownAttributeNames.add(attributeName); this.knownAttributeNames.add(attributeName);
return true; return true;
} }
else { else {
return false; return this.attributeNames.contains(attributeName);
} }
} }
@ -117,6 +129,13 @@ public class SessionAttributesHandler {
this.sessionAttributeStore.storeAttribute(request, name, value); this.sessionAttributeStore.storeAttribute(request, name, value);
} }
}); });
// Store known attribute names in session (for distributed sessions)
// Only necessary for type-based attributes which get added to knownAttributeNames when touched.
if (!this.attributeTypes.isEmpty()) {
this.sessionAttributeStore.storeAttribute(request,
SESSION_KNOWN_ATTRIBUTE, StringUtils.toStringArray(this.knownAttributeNames));
}
} }
/** /**
@ -127,6 +146,15 @@ public class SessionAttributesHandler {
* @return a map with handler session attributes, possibly empty * @return a map with handler session attributes, possibly empty
*/ */
public Map<String, Object> retrieveAttributes(WebRequest request) { public Map<String, Object> retrieveAttributes(WebRequest request) {
// Restore known attribute names from session (for distributed sessions)
// Only necessary for type-based attributes which get added to knownAttributeNames when touched.
if (!this.attributeTypes.isEmpty()) {
Object known = this.sessionAttributeStore.retrieveAttribute(request, SESSION_KNOWN_ATTRIBUTE);
if (known instanceof String[] retrievedAttributeNames) {
this.knownAttributeNames.addAll(Arrays.asList(retrievedAttributeNames));
}
}
Map<String, Object> attributes = new HashMap<>(); Map<String, Object> attributes = new HashMap<>();
for (String name : this.knownAttributeNames) { for (String name : this.knownAttributeNames) {
Object value = this.sessionAttributeStore.retrieveAttribute(request, name); Object value = this.sessionAttributeStore.retrieveAttribute(request, name);

View File

@ -22,6 +22,7 @@ import java.util.Collections;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.ui.ModelMap; import org.springframework.ui.ModelMap;
@ -65,7 +66,7 @@ class ModelFactoryTests {
@BeforeEach @BeforeEach
void setUp() { void setup() {
this.webRequest = new ServletWebRequest(new MockHttpServletRequest()); this.webRequest = new ServletWebRequest(new MockHttpServletRequest());
this.attributeStore = new DefaultSessionAttributeStore(); this.attributeStore = new DefaultSessionAttributeStore();
this.attributeHandler = new SessionAttributesHandler(TestController.class, this.attributeStore); this.attributeHandler = new SessionAttributesHandler(TestController.class, this.attributeStore);
@ -155,11 +156,33 @@ class ModelFactoryTests {
// Now add attribute and try again // Now add attribute and try again
this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue"); this.attributeStore.storeAttribute(this.webRequest, "sessionAttr", "sessionAttrValue");
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod); modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
assertThat(this.mavContainer.getModel().get("sessionAttr")).isEqualTo("sessionAttrValue"); assertThat(this.mavContainer.getModel().get("sessionAttr")).isEqualTo("sessionAttrValue");
} }
@Test
void sessionAttributeByType() throws Exception {
ModelFactory modelFactory = new ModelFactory(null, null, this.attributeHandler);
HandlerMethod handlerMethod = createHandlerMethod("handleTestBean", TestBean.class);
assertThatExceptionOfType(HttpSessionRequiredException.class).isThrownBy(() ->
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod));
// Now add attribute and try again
this.attributeStore.storeAttribute(this.webRequest, "testBean", new TestBean("tb"));
modelFactory.initModel(this.webRequest, this.mavContainer, handlerMethod);
assertThat(this.mavContainer.getModel().get("testBean")).isEqualTo(new TestBean("tb"));
this.mavContainer.setRequestHandled(true);
modelFactory.updateModel(this.webRequest, this.mavContainer);
// Simulate switch to distributed session on different server
SessionAttributesHandler newHandler = new SessionAttributesHandler(TestController.class, this.attributeStore);
ModelFactory newFactory = new ModelFactory(null, null, newHandler);
ModelAndViewContainer newContainer = new ModelAndViewContainer();
HandlerMethod modelMethod = createHandlerMethod("handleModel", Model.class);
newFactory.initModel(this.webRequest, newContainer, modelMethod);
assertThat(newContainer.getModel().get("testBean")).isEqualTo(new TestBean("tb"));
}
@Test @Test
void updateModelBindingResult() throws Exception { void updateModelBindingResult() throws Exception {
String commandName = "attr1"; String commandName = "attr1";
@ -263,7 +286,7 @@ class ModelFactoryTests {
} }
@SessionAttributes({"sessionAttr", "foo"}) @SessionAttributes(names = {"sessionAttr", "foo"}, types = TestBean.class)
static class TestController { static class TestController {
@ModelAttribute @ModelAttribute
@ -286,7 +309,7 @@ class ModelFactoryTests {
return null; return null;
} }
@ModelAttribute(name="foo", binding=false) @ModelAttribute(name = "foo", binding = false)
public Foo modelAttrWithBindingDisabled() { public Foo modelAttrWithBindingDisabled() {
return new Foo(); return new Foo();
} }
@ -296,6 +319,12 @@ class ModelFactoryTests {
public void handleSessionAttr(@ModelAttribute("sessionAttr") String sessionAttr) { public void handleSessionAttr(@ModelAttribute("sessionAttr") String sessionAttr) {
} }
public void handleTestBean(@ModelAttribute TestBean testBean) {
}
public void handleModel(Model model) {
}
} }