Skip to content

Commit ce7278a

Browse files
committed
Optimize HTTP headers management
Several benchmarks underlined a few hotspots for CPU and GC pressure in the Spring Framework codebase: 1. `org.springframework.util.MimeType.<init>(String, String, Map)` 2. `org.springframework.util.LinkedCaseInsensitiveMap.convertKey(String)` Both are linked with HTTP request headers parsing and response headers writin during the exchange processing phase. 1) is linked to repeated calls to `HttpHeaders.getContentType` within a single request handling. The media type parsing operation is expensive and the result doesn't change between calls, since the request headers are immutable at that point. This commit improves this by caching the parsed `MediaType` for the `"Content-Type"` request header in the `ReadOnlyHttpHeaders` class. This change is available for both Spring MVC and Spring WebFlux. 2) is linked to insertions/lookups in the `LinkedCaseInsensitiveMap`, which is the data structure behind `HttpHeaders`. Those operations are creating a lot of garbage (including a lot of `String` created by `toLowerCase`). We could choose a more efficient data structure for storing HTTP headers data. As a first step, this commit is focusing on Spring WebFlux and introduces `MultiValueMap` implementations mapped by native HTTP headers for the following servers: Tomcat, Jetty, Netty and Undertow. Such implementations avoid unnecessary copying of the headers and leverages as much as possible optimized operations provided by the native implementations. This change has a few consequences: * `HttpHeaders` can now wrap a `MultiValueMap` directly * The default constructor of `HttpHeaders` is still backed by a `LinkedCaseInsensitiveMap` * The HTTP request headers for the websocket HTTP handshake now need to be cloned, because native headers are likely to be pooled/recycled by the server implementation, hence gone when the initial HTTP exchange is done Issue: SPR-17250
1 parent 61403e3 commit ce7278a

19 files changed

+1224
-85
lines changed

spring-web/src/main/java/org/springframework/http/HttpHeaders.java

+26-35
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@
3535
import java.util.Collections;
3636
import java.util.EnumSet;
3737
import java.util.Iterator;
38-
import java.util.LinkedHashMap;
39-
import java.util.LinkedList;
4038
import java.util.List;
4139
import java.util.Locale;
4240
import java.util.Map;
@@ -47,7 +45,9 @@
4745

4846
import org.springframework.lang.Nullable;
4947
import org.springframework.util.Assert;
48+
import org.springframework.util.CollectionUtils;
5049
import org.springframework.util.LinkedCaseInsensitiveMap;
50+
import org.springframework.util.LinkedMultiValueMap;
5151
import org.springframework.util.MultiValueMap;
5252
import org.springframework.util.StringUtils;
5353

@@ -78,7 +78,8 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
7878
/**
7979
* The empty {@code HttpHeaders} instance (immutable).
8080
*/
81-
public static final HttpHeaders EMPTY = new HttpHeaders(new LinkedHashMap<>(), true);
81+
public static final HttpHeaders EMPTY =
82+
new ReadOnlyHttpHeaders(new HttpHeaders(new LinkedMultiValueMap<>(0)));
8283
/**
8384
* The HTTP {@code Accept} header field name.
8485
* @see <a href="http://tools.ietf.org/html/rfc7231#section-5.3.2">Section 5.3.2 of RFC 7231</a>
@@ -397,35 +398,27 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
397398
private static final DateTimeFormatter[] DATE_FORMATTERS = new DateTimeFormatter[] {
398399
DateTimeFormatter.RFC_1123_DATE_TIME,
399400
DateTimeFormatter.ofPattern("EEEE, dd-MMM-yy HH:mm:ss zz", Locale.US),
400-
DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy",Locale.US).withZone(GMT)
401+
DateTimeFormatter.ofPattern("EEE MMM dd HH:mm:ss yyyy", Locale.US).withZone(GMT)
401402
};
402403

403404

404-
private final Map<String, List<String>> headers;
405-
406-
private final boolean readOnly;
405+
final MultiValueMap<String, String> headers;
407406

408407

409408
/**
410-
* Constructs a new, empty instance of the {@code HttpHeaders} object.
409+
* Construct a new, empty instance of the {@code HttpHeaders} object.
411410
*/
412411
public HttpHeaders() {
413-
this(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH), false);
412+
this(CollectionUtils.toMultiValueMap(
413+
new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH)));
414414
}
415415

416416
/**
417-
* Private constructor that can create read-only {@code HttpHeader} instances.
417+
* Construct a new {@code HttpHeaders} instance backed by an existing map.
418418
*/
419-
private HttpHeaders(Map<String, List<String>> headers, boolean readOnly) {
420-
if (readOnly) {
421-
Map<String, List<String>> map = new LinkedCaseInsensitiveMap<>(headers.size(), Locale.ENGLISH);
422-
headers.forEach((key, valueList) -> map.put(key, Collections.unmodifiableList(valueList)));
423-
this.headers = Collections.unmodifiableMap(map);
424-
}
425-
else {
426-
this.headers = headers;
427-
}
428-
this.readOnly = readOnly;
419+
public HttpHeaders(MultiValueMap<String, String> headers) {
420+
Assert.notNull(headers, "headers must not be null");
421+
this.headers = headers;
429422
}
430423

431424

@@ -1474,8 +1467,7 @@ protected String toCommaDelimitedString(List<String> headerValues) {
14741467
@Override
14751468
@Nullable
14761469
public String getFirst(String headerName) {
1477-
List<String> headerValues = this.headers.get(headerName);
1478-
return (headerValues != null ? headerValues.get(0) : null);
1470+
return this.headers.getFirst(headerName);
14791471
}
14801472

14811473
/**
@@ -1488,19 +1480,17 @@ public String getFirst(String headerName) {
14881480
*/
14891481
@Override
14901482
public void add(String headerName, @Nullable String headerValue) {
1491-
List<String> headerValues = this.headers.computeIfAbsent(headerName, k -> new LinkedList<>());
1492-
headerValues.add(headerValue);
1483+
this.headers.add(headerName, headerValue);
14931484
}
14941485

14951486
@Override
14961487
public void addAll(String key, List<? extends String> values) {
1497-
List<String> currentValues = this.headers.computeIfAbsent(key, k -> new LinkedList<>());
1498-
currentValues.addAll(values);
1488+
this.headers.addAll(key, values);
14991489
}
15001490

15011491
@Override
15021492
public void addAll(MultiValueMap<String, String> values) {
1503-
values.forEach(this::addAll);
1493+
this.headers.addAll(values);
15041494
}
15051495

15061496
/**
@@ -1513,21 +1503,17 @@ public void addAll(MultiValueMap<String, String> values) {
15131503
*/
15141504
@Override
15151505
public void set(String headerName, @Nullable String headerValue) {
1516-
List<String> headerValues = new LinkedList<>();
1517-
headerValues.add(headerValue);
1518-
this.headers.put(headerName, headerValues);
1506+
this.headers.set(headerName, headerValue);
15191507
}
15201508

15211509
@Override
15221510
public void setAll(Map<String, String> values) {
1523-
values.forEach(this::set);
1511+
this.headers.setAll(values);
15241512
}
15251513

15261514
@Override
15271515
public Map<String, String> toSingleValueMap() {
1528-
LinkedHashMap<String, String> singleValueMap = new LinkedHashMap<>(this.headers.size());
1529-
this.headers.forEach((key, valueList) -> singleValueMap.put(key, valueList.get(0)));
1530-
return singleValueMap;
1516+
return this.headers.toSingleValueMap();
15311517
}
15321518

15331519

@@ -1623,7 +1609,12 @@ public String toString() {
16231609
*/
16241610
public static HttpHeaders readOnlyHttpHeaders(HttpHeaders headers) {
16251611
Assert.notNull(headers, "HttpHeaders must not be null");
1626-
return (headers.readOnly ? headers : new HttpHeaders(headers, true));
1612+
if (headers instanceof ReadOnlyHttpHeaders) {
1613+
return headers;
1614+
}
1615+
else {
1616+
return new ReadOnlyHttpHeaders(headers);
1617+
}
16271618
}
16281619

16291620
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2002-2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.http;
18+
19+
import java.util.AbstractMap;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.List;
23+
import java.util.Map;
24+
import java.util.Set;
25+
import java.util.stream.Collectors;
26+
27+
import org.springframework.lang.Nullable;
28+
import org.springframework.util.MultiValueMap;
29+
30+
/**
31+
* {@code HttpHeaders} object that can only be read, not written to.
32+
*
33+
* @author Brian Clozel
34+
* @since 5.1
35+
*/
36+
class ReadOnlyHttpHeaders extends HttpHeaders {
37+
38+
private static final long serialVersionUID = -8578554704772377436L;
39+
40+
@Nullable
41+
private MediaType cachedContentType;
42+
43+
ReadOnlyHttpHeaders(HttpHeaders headers) {
44+
super(headers.headers);
45+
}
46+
47+
@Override
48+
public MediaType getContentType() {
49+
if (this.cachedContentType != null) {
50+
return this.cachedContentType;
51+
}
52+
else {
53+
MediaType contentType = super.getContentType();
54+
this.cachedContentType = contentType;
55+
return contentType;
56+
}
57+
}
58+
59+
@Override
60+
public List<String> get(Object key) {
61+
List<String> values = this.headers.get(key);
62+
if (values != null) {
63+
return Collections.unmodifiableList(values);
64+
}
65+
return values;
66+
}
67+
68+
@Override
69+
public void add(String headerName, @Nullable String headerValue) {
70+
throw new UnsupportedOperationException();
71+
}
72+
73+
@Override
74+
public void addAll(String key, List<? extends String> values) {
75+
throw new UnsupportedOperationException();
76+
}
77+
78+
@Override
79+
public void addAll(MultiValueMap<String, String> values) {
80+
throw new UnsupportedOperationException();
81+
}
82+
83+
@Override
84+
public void set(String headerName, @Nullable String headerValue) {
85+
throw new UnsupportedOperationException();
86+
}
87+
88+
@Override
89+
public void setAll(Map<String, String> values) {
90+
throw new UnsupportedOperationException();
91+
}
92+
93+
@Override
94+
public Map<String, String> toSingleValueMap() {
95+
return Collections.unmodifiableMap(this.headers.toSingleValueMap());
96+
}
97+
98+
@Override
99+
public Set<String> keySet() {
100+
return Collections.unmodifiableSet(this.headers.keySet());
101+
}
102+
103+
@Override
104+
public List<String> put(String key, List<String> value) {
105+
throw new UnsupportedOperationException();
106+
}
107+
108+
@Override
109+
public List<String> remove(Object key) {
110+
throw new UnsupportedOperationException();
111+
}
112+
113+
@Override
114+
public void putAll(Map<? extends String, ? extends List<String>> map) {
115+
throw new UnsupportedOperationException();
116+
}
117+
118+
@Override
119+
public void clear() {
120+
throw new UnsupportedOperationException();
121+
}
122+
123+
@Override
124+
public Collection<List<String>> values() {
125+
return Collections.unmodifiableCollection(this.headers.values());
126+
}
127+
128+
@Override
129+
public Set<Entry<String, List<String>>> entrySet() {
130+
return Collections.unmodifiableSet(this.headers.entrySet().stream()
131+
.map(AbstractMap.SimpleImmutableEntry::new)
132+
.collect(Collectors.toSet()));
133+
}
134+
135+
}

spring-web/src/main/java/org/springframework/http/server/ServletServerHttpResponse.java

+3
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,9 @@ public List<String> get(Object key) {
153153
Assert.isInstanceOf(String.class, key, "Key must be a String-based header name");
154154

155155
Collection<String> values1 = servletResponse.getHeaders((String) key);
156+
if (headersWritten) {
157+
return new ArrayList<>(values1);
158+
}
156159
boolean isEmpty1 = CollectionUtils.isEmpty(values1);
157160

158161
List<String> values2 = super.get(key);

spring-web/src/main/java/org/springframework/http/server/reactive/AbstractListenerServerHttpResponse.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2016 the original author or authors.
2+
* Copyright 2002-2018 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.
@@ -24,6 +24,7 @@
2424

2525
import org.springframework.core.io.buffer.DataBuffer;
2626
import org.springframework.core.io.buffer.DataBufferFactory;
27+
import org.springframework.http.HttpHeaders;
2728

2829
/**
2930
* Abstract base class for listener-based server responses, e.g. Servlet 3.1
@@ -41,6 +42,10 @@ public AbstractListenerServerHttpResponse(DataBufferFactory dataBufferFactory) {
4142
super(dataBufferFactory);
4243
}
4344

45+
public AbstractListenerServerHttpResponse(DataBufferFactory dataBufferFactory, HttpHeaders headers) {
46+
super(dataBufferFactory, headers);
47+
}
48+
4449

4550
@Override
4651
protected final Mono<Void> writeWithInternal(Publisher<? extends DataBuffer> body) {

spring-web/src/main/java/org/springframework/http/server/reactive/AbstractServerHttpResponse.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,14 @@ private enum State {NEW, COMMITTING, COMMITTED}
7575

7676

7777
public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory) {
78+
this(dataBufferFactory, new HttpHeaders());
79+
}
80+
81+
public AbstractServerHttpResponse(DataBufferFactory dataBufferFactory, HttpHeaders headers) {
7882
Assert.notNull(dataBufferFactory, "DataBufferFactory must not be null");
83+
Assert.notNull(headers, "HttpHeaders must not be null");
7984
this.dataBufferFactory = dataBufferFactory;
80-
this.headers = new HttpHeaders();
85+
this.headers = headers;
8186
this.cookies = new LinkedMultiValueMap<>();
8287
}
8388

0 commit comments

Comments
 (0)