Skip to content

Commit

Permalink
Avoid MimeType garbage creation
Browse files Browse the repository at this point in the history
Prior to this commit, calls to `MimeType` and `MediaType` would create a
significant amount of garbage:

* during startup time, in the static sections of `MimeType` and
`MediaType` when creating well-known types
* at runtime, when parsing media types for content negotiation or
writing known media types as strings in HTTP response headers

This commit does the following:

* Avoid parsing the well-known types and use regular constructors
instead
* Cache types in a simple LRU cache once they've been parsed, since an
application is likely to deal with a limited set of types
* Avoid using `java.util.stream.Stream` in hot code paths

Benchmarks show that a complete revision of the `MimeTypeUtils` parser
is not required, since the LRU cache is enough there.

Closes gh-22340
  • Loading branch information
bclozel committed Feb 5, 2019
1 parent 862fa55 commit ba8849d
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 44 deletions.
11 changes: 8 additions & 3 deletions spring-core/src/main/java/org/springframework/util/MimeType.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ public class MimeType implements Comparable<MimeType>, Serializable {

private final Map<String, String> parameters;

private String mimetype;


/**
* Create a new {@code MimeType} for the given primary type.
Expand Down Expand Up @@ -469,9 +471,12 @@ public int hashCode() {

@Override
public String toString() {
StringBuilder builder = new StringBuilder();
appendTo(builder);
return builder.toString();
if (this.mimetype == null) {
StringBuilder builder = new StringBuilder();
appendTo(builder);
this.mimetype = builder.toString();
}
return this.mimetype;
}

protected void appendTo(StringBuilder builder) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -28,6 +28,11 @@
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.lang.Nullable;
Expand All @@ -39,6 +44,7 @@
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Dimitrios Liapis
* @author Brian Clozel
* @since 4.0
*/
public abstract class MimeTypeUtils {
Expand All @@ -49,6 +55,9 @@ public abstract class MimeTypeUtils {
'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U',
'V', 'W', 'X', 'Y', 'Z'};

private static final ConcurrentLRUCache<String, MimeType> CACHED_MIMETYPES =
new ConcurrentLRUCache<>(32, MimeTypeUtils::parseMimeTypeInternal);

/**
* Comparator used by {@link #sortBySpecificity(List)}.
*/
Expand Down Expand Up @@ -157,28 +166,31 @@ public abstract class MimeTypeUtils {
@Nullable
private static volatile Random random;


static {
ALL = MimeType.valueOf(ALL_VALUE);
APPLICATION_JSON = MimeType.valueOf(APPLICATION_JSON_VALUE);
APPLICATION_OCTET_STREAM = MimeType.valueOf(APPLICATION_OCTET_STREAM_VALUE);
APPLICATION_XML = MimeType.valueOf(APPLICATION_XML_VALUE);
IMAGE_GIF = MimeType.valueOf(IMAGE_GIF_VALUE);
IMAGE_JPEG = MimeType.valueOf(IMAGE_JPEG_VALUE);
IMAGE_PNG = MimeType.valueOf(IMAGE_PNG_VALUE);
TEXT_HTML = MimeType.valueOf(TEXT_HTML_VALUE);
TEXT_PLAIN = MimeType.valueOf(TEXT_PLAIN_VALUE);
TEXT_XML = MimeType.valueOf(TEXT_XML_VALUE);
ALL = new MimeType("*", "*");
APPLICATION_JSON = new MimeType("application", "json");
APPLICATION_OCTET_STREAM = new MimeType("application", "octet-stream");
APPLICATION_XML = new MimeType("application", "xml");
IMAGE_GIF = new MimeType("image", "gif");
IMAGE_JPEG = new MimeType("image", "jpeg");
IMAGE_PNG = new MimeType("image", "png");
TEXT_HTML = new MimeType("text", "html");
TEXT_PLAIN = new MimeType("text", "plain");
TEXT_XML = new MimeType("text", "xml");

This comment has been minimized.

Copy link
@rstoyanchev

rstoyanchev Feb 8, 2019

Contributor

Some sort of comment at the top of all this would be a good idea, to ensure it doesn't ever get lost while "polishing" at some point in the future.

}


/**
* Parse the given String into a single {@code MimeType}.
* Recently parsed {@code MimeType} are cached for further retrieval.
* @param mimeType the string to parse
* @return the mime type
* @throws InvalidMimeTypeException if the string cannot be parsed
*/
public static MimeType parseMimeType(String mimeType) {
return CACHED_MIMETYPES.get(mimeType);
}

private static MimeType parseMimeTypeInternal(String mimeType) {
if (!StringUtils.hasLength(mimeType)) {
throw new InvalidMimeTypeException(mimeType, "'mimeType' must not be empty");
}
Expand Down Expand Up @@ -387,4 +399,52 @@ public static String generateMultipartBoundaryString() {
return new String(generateMultipartBoundary(), StandardCharsets.US_ASCII);
}

static class ConcurrentLRUCache<K, V> {

private final int maxSize;

private final ConcurrentLinkedQueue<K> queue = new ConcurrentLinkedQueue<>();

private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();

private final ReadWriteLock lock = new ReentrantReadWriteLock();

private final Function<K, V> generator;

ConcurrentLRUCache(int maxSize, Function<K, V> generator) {
Assert.isTrue(maxSize > 0, "LRU max size should be positive");
Assert.notNull(generator, "Generator function should not be null");
this.maxSize = maxSize;
this.generator = generator;
}

public V get(K key) {
this.lock.readLock().lock();
try {
if (this.queue.remove(key)) {
this.queue.add(key);
return this.cache.get(key);
}
}
finally {
this.lock.readLock().unlock();
}
this.lock.writeLock().lock();
try {
if (this.queue.size() == this.maxSize) {
K leastUsed = this.queue.poll();
this.cache.remove(leastUsed);
}
V value = this.generator.apply(key);
this.queue.add(key);
this.cache.put(key, value);
return value;
}
finally {
this.lock.writeLock().unlock();
}
}

}

}
64 changes: 36 additions & 28 deletions spring-web/src/main/java/org/springframework/http/MediaType.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -18,14 +18,14 @@

import java.io.Serializable;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
Expand Down Expand Up @@ -323,29 +323,29 @@ public class MediaType extends MimeType implements Serializable {


static {
ALL = valueOf(ALL_VALUE);
APPLICATION_ATOM_XML = valueOf(APPLICATION_ATOM_XML_VALUE);
APPLICATION_FORM_URLENCODED = valueOf(APPLICATION_FORM_URLENCODED_VALUE);
APPLICATION_JSON = valueOf(APPLICATION_JSON_VALUE);
APPLICATION_JSON_UTF8 = valueOf(APPLICATION_JSON_UTF8_VALUE);
APPLICATION_OCTET_STREAM = valueOf(APPLICATION_OCTET_STREAM_VALUE);
APPLICATION_PDF = valueOf(APPLICATION_PDF_VALUE);
APPLICATION_PROBLEM_JSON = valueOf(APPLICATION_PROBLEM_JSON_VALUE);
APPLICATION_PROBLEM_JSON_UTF8 = valueOf(APPLICATION_PROBLEM_JSON_UTF8_VALUE);
APPLICATION_PROBLEM_XML = valueOf(APPLICATION_PROBLEM_XML_VALUE);
APPLICATION_RSS_XML = valueOf(APPLICATION_RSS_XML_VALUE);
APPLICATION_STREAM_JSON = valueOf(APPLICATION_STREAM_JSON_VALUE);
APPLICATION_XHTML_XML = valueOf(APPLICATION_XHTML_XML_VALUE);
APPLICATION_XML = valueOf(APPLICATION_XML_VALUE);
IMAGE_GIF = valueOf(IMAGE_GIF_VALUE);
IMAGE_JPEG = valueOf(IMAGE_JPEG_VALUE);
IMAGE_PNG = valueOf(IMAGE_PNG_VALUE);
MULTIPART_FORM_DATA = valueOf(MULTIPART_FORM_DATA_VALUE);
TEXT_EVENT_STREAM = valueOf(TEXT_EVENT_STREAM_VALUE);
TEXT_HTML = valueOf(TEXT_HTML_VALUE);
TEXT_MARKDOWN = valueOf(TEXT_MARKDOWN_VALUE);
TEXT_PLAIN = valueOf(TEXT_PLAIN_VALUE);
TEXT_XML = valueOf(TEXT_XML_VALUE);
ALL = new MediaType("*", "*");
APPLICATION_ATOM_XML = new MediaType("application", "atom+xml");
APPLICATION_FORM_URLENCODED = new MediaType("application", "x-www-form-urlencoded");
APPLICATION_JSON = new MediaType("application", "json");
APPLICATION_JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream");;
APPLICATION_PDF = new MediaType("application", "pdf");
APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json");
APPLICATION_PROBLEM_JSON_UTF8 = new MediaType("application", "problem", StandardCharsets.UTF_8);
APPLICATION_PROBLEM_XML = new MediaType("application", "problem+xml");
APPLICATION_RSS_XML = new MediaType("application", "rss+xml");
APPLICATION_STREAM_JSON = new MediaType("application", "stream+json");
APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml");
APPLICATION_XML = new MediaType("application", "xml");
IMAGE_GIF = new MediaType("image", "gif");
IMAGE_JPEG = new MediaType("image", "jpeg");
IMAGE_PNG = new MediaType("image", "png");
MULTIPART_FORM_DATA = new MediaType("multipart", "form-data");
TEXT_EVENT_STREAM = new MediaType("text", "event-stream");
TEXT_HTML = new MediaType("text", "html");
TEXT_MARKDOWN = new MediaType("text", "markdown");
TEXT_PLAIN = new MediaType("text", "plain");
TEXT_XML = new MediaType("text", "xml");
}


Expand Down Expand Up @@ -552,8 +552,12 @@ public static List<MediaType> parseMediaTypes(@Nullable String mediaTypes) {
if (!StringUtils.hasLength(mediaTypes)) {
return Collections.emptyList();
}
return MimeTypeUtils.tokenize(mediaTypes).stream()
.map(MediaType::parseMediaType).collect(Collectors.toList());
List<String> tokenizedTypes = MimeTypeUtils.tokenize(mediaTypes);
List<MediaType> result = new ArrayList<>(tokenizedTypes.size());
for (String type : tokenizedTypes) {
result.add(parseMediaType(type));
}
return result;

This comment has been minimized.

Copy link
@rstoyanchev

rstoyanchev Feb 8, 2019

Contributor

Same here. It be too easy to for someone in the future to replace with a stream.

}

/**
Expand Down Expand Up @@ -586,7 +590,11 @@ else if (mediaTypes.size() == 1) {
* @since 5.0
*/
public static List<MediaType> asMediaTypes(List<MimeType> mimeTypes) {
return mimeTypes.stream().map(MediaType::asMediaType).collect(Collectors.toList());
List<MediaType> mediaTypes = new ArrayList<>(mimeTypes.size());
for(MimeType mimeType : mimeTypes) {
mediaTypes.add(MediaType.asMediaType(mimeType));
}
return mediaTypes;
}

/**
Expand Down

0 comments on commit ba8849d

Please sign in to comment.