Skip to content

Commit 0fdf759

Browse files
kilinkbclozel
authored andcommitted
Optimize Map methods in ServletAttributesMap
ServletAttributesMap inherited default implementations of the size and isEmpty methods from AbstractMap which delegates to the Set returned by entrySet. ServletAttributesMap's entrySet method made this fairly expensive, since it would copy the attributes to a List, then use a Stream to build the Set. To avoid the cost, add implementations of isEmpty / size that don't need to call entrySet at all. Additionally, change entrySet to return a Set view that simply lazily delegates to the underlying servlet request for iteration. Closes gh-32189
1 parent c04d4da commit 0fdf759

File tree

2 files changed

+132
-10
lines changed

2 files changed

+132
-10
lines changed

Diff for: spring-webmvc/src/main/java/org/springframework/web/servlet/function/DefaultServerRequest.java

+89-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2023 the original author or authors.
2+
* Copyright 2002-2024 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -26,10 +26,13 @@
2626
import java.security.Principal;
2727
import java.time.Instant;
2828
import java.util.AbstractMap;
29+
import java.util.AbstractSet;
2930
import java.util.ArrayList;
3031
import java.util.Arrays;
3132
import java.util.Collection;
3233
import java.util.Collections;
34+
import java.util.Enumeration;
35+
import java.util.Iterator;
3336
import java.util.List;
3437
import java.util.Locale;
3538
import java.util.Map;
@@ -57,6 +60,7 @@
5760
import org.springframework.http.converter.HttpMessageConverter;
5861
import org.springframework.http.server.RequestPath;
5962
import org.springframework.http.server.ServletServerHttpRequest;
63+
import org.springframework.lang.NonNull;
6064
import org.springframework.lang.Nullable;
6165
import org.springframework.util.Assert;
6266
import org.springframework.util.CollectionUtils;
@@ -80,6 +84,7 @@
8084
*
8185
* @author Arjen Poutsma
8286
* @author Sam Brannen
87+
* @author Patrick Strawderman
8388
* @since 5.2
8489
*/
8590
class DefaultServerRequest implements ServerRequest {
@@ -469,18 +474,77 @@ public boolean containsKey(Object key) {
469474

470475
@Override
471476
public void clear() {
472-
List<String> attributeNames = Collections.list(this.servletRequest.getAttributeNames());
473-
attributeNames.forEach(this.servletRequest::removeAttribute);
477+
this.servletRequest.getAttributeNames().asIterator().forEachRemaining(this.servletRequest::removeAttribute);
474478
}
475479

476480
@Override
477481
public Set<Entry<String, Object>> entrySet() {
478-
return Collections.list(this.servletRequest.getAttributeNames()).stream()
479-
.map(name -> {
480-
Object value = this.servletRequest.getAttribute(name);
481-
return new SimpleImmutableEntry<>(name, value);
482-
})
483-
.collect(Collectors.toSet());
482+
return new AbstractSet<>() {
483+
@Override
484+
public Iterator<Entry<String, Object>> iterator() {
485+
return new Iterator<>() {
486+
487+
private final Iterator<String> attributes = ServletAttributesMap.this.servletRequest.getAttributeNames().asIterator();
488+
489+
@Override
490+
public boolean hasNext() {
491+
return this.attributes.hasNext();
492+
}
493+
494+
@Override
495+
public Entry<String, Object> next() {
496+
String attribute = this.attributes.next();
497+
Object value = ServletAttributesMap.this.servletRequest.getAttribute(attribute);
498+
return new SimpleImmutableEntry<>(attribute, value);
499+
}
500+
};
501+
}
502+
503+
@Override
504+
public boolean isEmpty() {
505+
return ServletAttributesMap.this.isEmpty();
506+
}
507+
508+
@Override
509+
public int size() {
510+
return ServletAttributesMap.this.size();
511+
}
512+
513+
@Override
514+
public boolean contains(Object o) {
515+
if (!(o instanceof Map.Entry<?,?> entry)) {
516+
return false;
517+
}
518+
String attribute = (String) entry.getKey();
519+
Object value = ServletAttributesMap.this.servletRequest.getAttribute(attribute);
520+
return value != null && value.equals(entry.getValue());
521+
}
522+
523+
@Override
524+
public boolean addAll(@NonNull Collection<? extends Entry<String, Object>> c) {
525+
throw new UnsupportedOperationException();
526+
}
527+
528+
@Override
529+
public boolean remove(Object o) {
530+
throw new UnsupportedOperationException();
531+
}
532+
533+
@Override
534+
public boolean removeAll(Collection<?> c) {
535+
throw new UnsupportedOperationException();
536+
}
537+
538+
@Override
539+
public boolean retainAll(@NonNull Collection<?> c) {
540+
throw new UnsupportedOperationException();
541+
}
542+
543+
@Override
544+
public void clear() {
545+
throw new UnsupportedOperationException();
546+
}
547+
};
484548
}
485549

486550
@Override
@@ -503,6 +567,22 @@ public Object remove(Object key) {
503567
this.servletRequest.removeAttribute(name);
504568
return value;
505569
}
570+
571+
@Override
572+
public int size() {
573+
Enumeration<String> attributes = this.servletRequest.getAttributeNames();
574+
int size = 0;
575+
while (attributes.hasMoreElements()) {
576+
size++;
577+
attributes.nextElement();
578+
}
579+
return size;
580+
}
581+
582+
@Override
583+
public boolean isEmpty() {
584+
return !this.servletRequest.getAttributeNames().hasMoreElements();
585+
}
506586
}
507587

508588

Diff for: spring-webmvc/src/test/java/org/springframework/web/servlet/function/DefaultServerRequestTests.java

+43-1
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@
2727
import java.time.Instant;
2828
import java.time.temporal.ChronoUnit;
2929
import java.util.Collections;
30+
import java.util.Iterator;
3031
import java.util.List;
3132
import java.util.Map;
3233
import java.util.Optional;
3334
import java.util.OptionalLong;
35+
import java.util.Set;
3436

3537
import jakarta.servlet.http.Cookie;
3638
import jakarta.servlet.http.Part;
@@ -62,8 +64,8 @@
6264
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
6365

6466
/**
67+
* Tests for {@link DefaultServerRequest}.
6568
* @author Arjen Poutsma
66-
* @since 5.1
6769
*/
6870
class DefaultServerRequestTests {
6971

@@ -115,6 +117,46 @@ void attribute() {
115117
assertThat(request.attribute("foo")).contains("bar");
116118
}
117119

120+
@Test
121+
void attributes() {
122+
MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true);
123+
servletRequest.setAttribute("foo", "bar");
124+
servletRequest.setAttribute("baz", "qux");
125+
126+
DefaultServerRequest request = new DefaultServerRequest(servletRequest, this.messageConverters);
127+
128+
Map<String, Object> attributesMap = request.attributes();
129+
assertThat(attributesMap).isNotEmpty();
130+
assertThat(attributesMap).containsEntry("foo", "bar");
131+
assertThat(attributesMap).containsEntry("baz", "qux");
132+
assertThat(attributesMap).doesNotContainEntry("foo", "blah");
133+
134+
Set<Map.Entry<String, Object>> entrySet = attributesMap.entrySet();
135+
assertThat(entrySet).isNotEmpty();
136+
assertThat(entrySet).hasSize(attributesMap.size());
137+
assertThat(entrySet).contains(Map.entry("foo", "bar"));
138+
assertThat(entrySet).contains(Map.entry("baz", "qux"));
139+
assertThat(entrySet).doesNotContain(Map.entry("foo", "blah"));
140+
assertThat(entrySet).isUnmodifiable();
141+
142+
assertThat(entrySet.iterator()).toIterable().contains(Map.entry("foo", "bar"), Map.entry("baz", "qux"));
143+
Iterator<String> attributes = servletRequest.getAttributeNames().asIterator();
144+
Iterator<Map.Entry<String, Object>> entrySetIterator = entrySet.iterator();
145+
while (attributes.hasNext()) {
146+
attributes.next();
147+
assertThat(entrySetIterator).hasNext();
148+
entrySetIterator.next();
149+
}
150+
assertThat(entrySetIterator).isExhausted();
151+
152+
attributesMap.clear();
153+
assertThat(attributesMap).isEmpty();
154+
assertThat(attributesMap).hasSize(0);
155+
assertThat(entrySet).isEmpty();
156+
assertThat(entrySet).hasSize(0);
157+
assertThat(entrySet.iterator()).isExhausted();
158+
}
159+
118160
@Test
119161
void params() {
120162
MockHttpServletRequest servletRequest = PathPatternsTestUtils.initRequest("GET", "/", true);

0 commit comments

Comments
 (0)