Further optimize Servlet header adapters

- Optimize get method for request headers
- Update keySet methods to use custom extension of AbstractSet
- Drop use of native Tomcat headers, which could be an issue for
request and response wrappers that override header methods.
The performance of Servlet adapters should be similar for the
commonly used methods, and it should be possible to optimize
further for the future in HttpHeaders (e.g. by adding a
set alternative to put), and requesting Servlet API refinements.

Closes gh-36334
This commit is contained in:
rstoyanchev
2026-02-16 17:37:51 +00:00
parent 9273a11a2c
commit 5baa4fdd69
4 changed files with 112 additions and 116 deletions
@@ -16,9 +16,12 @@
package org.springframework.http.server;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
@@ -46,7 +49,7 @@ final class ServletRequestHeadersAdapter implements MultiValueMap<String, String
private final HttpServletRequest request;
ServletRequestHeadersAdapter(HttpServletRequest request) {
private ServletRequestHeadersAdapter(HttpServletRequest request) {
this.request = request;
}
@@ -125,7 +128,12 @@ final class ServletRequestHeadersAdapter implements MultiValueMap<String, String
if (key instanceof String headerName) {
Enumeration<String> values = this.request.getHeaders(headerName);
if (values.hasMoreElements()) {
List<String> result = new ArrayList<>();
String value = values.nextElement();
if (!values.hasMoreElements()) {
return Collections.singletonList(value);
}
List<String> result = new ArrayList<>(4);
result.add(value);
while (values.hasMoreElements()) {
result.add(values.nextElement());
}
@@ -157,12 +165,7 @@ final class ServletRequestHeadersAdapter implements MultiValueMap<String, String
@Override
public Set<String> keySet() {
Set<String> set = new LinkedHashSet<>();
Enumeration<String> names = this.request.getHeaderNames();
while (names.hasMoreElements()) {
set.add(names.nextElement());
}
return set;
return new HeaderNames();
}
@Override
@@ -183,30 +186,6 @@ final class ServletRequestHeadersAdapter implements MultiValueMap<String, String
return new UnsupportedOperationException("HttpHeaders does not support all Map operations");
}
@Override
public int hashCode() {
return toMultiValueMap().hashCode();
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof MultiValueMap<?,?> that && toMultiValueMap().equals(that)));
}
private MultiValueMap<String, String> toMultiValueMap() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
Enumeration<String> names = this.request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
Enumeration<String> values = this.request.getHeaders(name);
while (values.hasMoreElements()) {
map.add(name, values.nextElement());
}
}
return map;
}
@Override
public String toString() {
return HttpHeaders.formatHeaders(this);
@@ -214,13 +193,57 @@ final class ServletRequestHeadersAdapter implements MultiValueMap<String, String
/**
* Apply a wrapper that allows headers to be set or added, and treats those
* as overrides to the headers in the given MultiValueMap.
* @param headers the headers map to wrap
* @return the wrapper instance
* Factory method to create a Servlet request headers adapter.
* @param request the request to access headers from
* @return the created adapter instance
*/
static MultiValueMap<String, String> requestHeaderOverrideWrapper(MultiValueMap<String, String> headers) {
return new RequestHeaderOverrideWrapper(headers);
static MultiValueMap<String, String> create(HttpServletRequest request) {
return new RequestHeaderOverrideWrapper(new ServletRequestHeadersAdapter(request));
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(request.getHeaderNames());
}
@Override
public int size() {
Enumeration<String> names = request.getHeaderNames();
int size = 0;
while (names.hasMoreElements()) {
names.nextElement();
size++;
}
return size;
}
}
private static final class HeaderNamesIterator implements Iterator<String> {
private final Enumeration<String> enumeration;
private HeaderNamesIterator(Enumeration<String> enumeration) {
this.enumeration = enumeration;
}
@Override
public boolean hasNext() {
return this.enumeration.hasMoreElements();
}
@Override
public String next() {
return this.enumeration.nextElement();
}
@Override
public void remove() {
throw immutableRequestException();
}
}
@@ -351,11 +374,12 @@ final class ServletRequestHeadersAdapter implements MultiValueMap<String, String
@Override
public Set<String> keySet() {
Set<String> set = this.delegate.keySet();
if (this.overrideMap != null) {
Set<String> set = new LinkedHashSet<>(this.delegate.keySet());
set.addAll(this.overrideMap.keySet());
return set;
}
return set;
return this.delegate.keySet();
}
@Override
@@ -375,7 +399,6 @@ final class ServletRequestHeadersAdapter implements MultiValueMap<String, String
return this.overrideMap;
}
@Override
public int hashCode() {
return toMultiValueMap().hashCode();
@@ -16,11 +16,11 @@
package org.springframework.http.server;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -119,10 +119,9 @@ class ServletResponseHeadersAdapter implements MultiValueMap<String, String> {
public @Nullable List<String> get(Object key) {
if (key instanceof String headerName) {
Collection<String> values = this.response.getHeaders(headerName);
if (values.isEmpty()) {
return (this.response.containsHeader(headerName) ? Collections.emptyList() : null);
if (!values.isEmpty()) {
return new ArrayList<>(values);
}
return new ArrayList<>(values);
}
return null;
}
@@ -162,7 +161,7 @@ class ServletResponseHeadersAdapter implements MultiValueMap<String, String> {
@Override
public Set<String> keySet() {
return new LinkedHashSet<>(this.response.getHeaderNames());
return new HeaderNames();
}
@Override
@@ -205,4 +204,43 @@ class ServletResponseHeadersAdapter implements MultiValueMap<String, String> {
return HttpHeaders.formatHeaders(this);
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(response.getHeaderNames());
}
@Override
public int size() {
return ServletResponseHeadersAdapter.this.size();
}
}
private static final class HeaderNamesIterator implements Iterator<String> {
private final Iterator<String> values;
private HeaderNamesIterator(Collection<String> values) {
this.values = values.iterator();
}
@Override
public boolean hasNext() {
return this.values.hasNext();
}
@Override
public String next() {
return this.values.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}
@@ -22,7 +22,6 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.lang.reflect.Field;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.URISyntaxException;
@@ -42,9 +41,6 @@ import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import org.apache.catalina.connector.RequestFacade;
import org.apache.coyote.Request;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders;
@@ -52,10 +48,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -70,9 +63,6 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
protected static final Charset FORM_CHARSET = StandardCharsets.UTF_8;
private static final boolean TOMCAT_PRESENT = ClassUtils.isPresent(
"org.apache.tomcat.util.http.MimeHeaders", ServletServerHttpRequest.class.getClassLoader());
private final HttpServletRequest servletRequest;
@@ -166,8 +156,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
@Override
public HttpHeaders getHeaders() {
if (this.headers == null) {
MultiValueMap<String, String> headersAdapter = initHeadersMultiValueMap();
this.headers = new HttpHeaders(headersAdapter);
this.headers = new HttpHeaders(ServletRequestHeadersAdapter.create(this.servletRequest));
// HttpServletRequest exposes some headers as properties:
// we should include those if not already present
@@ -208,19 +197,6 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
return this.headers;
}
private MultiValueMap<String, String> initHeadersMultiValueMap() {
MultiValueMap<String, String> nativeHeaders = null;
if (TOMCAT_PRESENT) {
nativeHeaders = TomcatInitializer.createTomcatHttpHeaders(this.servletRequest);
}
if (nativeHeaders == null) {
nativeHeaders = new ServletRequestHeadersAdapter(this.servletRequest);
}
return ServletRequestHeadersAdapter.requestHeaderOverrideWrapper(nativeHeaders);
}
@Override
public @Nullable Principal getPrincipal() {
return this.servletRequest.getUserPrincipal();
}
@@ -330,41 +306,6 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
}
private static final class TomcatInitializer {
private static final Field COYOTE_REQUEST_FIELD;
static {
Field field = ReflectionUtils.findField(RequestFacade.class, "request");
Assert.state(field != null, "Incompatible Tomcat implementation");
ReflectionUtils.makeAccessible(field);
COYOTE_REQUEST_FIELD = field;
}
public static @Nullable MultiValueMap<String, String> createTomcatHttpHeaders(HttpServletRequest servletRequest) {
RequestFacade requestFacade = getRequestFacade(servletRequest);
if (requestFacade == null) {
return null;
}
Object field = ReflectionUtils.getField(COYOTE_REQUEST_FIELD, requestFacade);
Assert.state(field != null, "No Tomcat connector request");
Request coyoteRequest = ((org.apache.catalina.connector.Request) field).getCoyoteRequest();
return new TomcatHeadersAdapter(coyoteRequest.getMimeHeaders());
}
private static @Nullable RequestFacade getRequestFacade(HttpServletRequest request) {
if (request instanceof RequestFacade facade) {
return facade;
}
else if (request instanceof HttpServletRequestWrapper wrapper) {
HttpServletRequest wrappedRequest = (HttpServletRequest) wrapper.getRequest();
return getRequestFacade(wrappedRequest);
}
return null;
}
}
private final class AttributesMap extends AbstractMap<String, Object> {
private @Nullable transient Set<String> keySet;
@@ -87,13 +87,13 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
@Override
public OutputStream getBody() throws IOException {
this.bodyUsed = true;
writeHeaders();
this.headersWritten = true;
return this.servletResponse.getOutputStream();
}
@Override
public void flush() throws IOException {
writeHeaders();
this.headersWritten = true;
if (this.bodyUsed) {
this.servletResponse.flushBuffer();
}
@@ -101,13 +101,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
@Override
public void close() {
writeHeaders();
}
private void writeHeaders() {
if (!this.headersWritten) {
this.headersWritten = true;
}
this.headersWritten = true;
}
}