diff --git a/src/main/java/org/springframework/hateoas/UriTemplate.java b/src/main/java/org/springframework/hateoas/UriTemplate.java index 71a485e9..f4d8acf2 100644 --- a/src/main/java/org/springframework/hateoas/UriTemplate.java +++ b/src/main/java/org/springframework/hateoas/UriTemplate.java @@ -25,6 +25,11 @@ import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -32,7 +37,6 @@ import java.util.stream.Collectors; import org.springframework.hateoas.TemplateVariable.VariableType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.StringUtils; import org.springframework.web.util.DefaultUriBuilderFactory; import org.springframework.web.util.DefaultUriBuilderFactory.EncodingMode; @@ -55,7 +59,7 @@ public class UriTemplate implements Iterable, Serializable { private static final Pattern VARIABLE_REGEX = Pattern.compile("\\{([\\?\\&#/]?)([\\w\\,*]+)\\}"); private static final long serialVersionUID = -1007874653930162262L; - private static final Map CACHE = new ConcurrentReferenceHashMap<>(); + private static final ConcurrentLruCache CACHE = new ConcurrentLruCache<>(1024, UriTemplate::new); private final TemplateVariables variables; private String baseUri; @@ -134,7 +138,7 @@ public class UriTemplate implements Iterable, Serializable { Assert.hasText(template, "Template must not be null or empty!"); - return CACHE.computeIfAbsent(template, UriTemplate::new); + return CACHE.get(template); } /** @@ -147,7 +151,7 @@ public class UriTemplate implements Iterable, Serializable { Assert.hasText(template, "Template must not be null or empty!"); - return CACHE.computeIfAbsent(template, UriTemplate::new).with(variables); + return CACHE.get(template).with(variables); } /** @@ -426,4 +430,84 @@ public class UriTemplate implements Iterable, Serializable { this.factory = createFactory(baseUri); } + + // Copy of ConcurrentLruCache of Spring Framework's MimeTypeUtils + + /** + * Simple Least Recently Used cache, bounded by the maximum size given to the class constructor. + *

+ * This implementation is backed by a {@code ConcurrentHashMap} for storing the cached values and a + * {@code ConcurrentLinkedQueue} for ordering the keys and choosing the least recently used key when the cache is at + * full capacity. + * + * @param the type of the key used for caching + * @param the type of the cached values + */ + private static class ConcurrentLruCache { + + private final int maxSize; + + private final ConcurrentLinkedDeque queue = new ConcurrentLinkedDeque<>(); + + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + private final ReadWriteLock lock; + + private final Function generator; + + private volatile int size = 0; + + public ConcurrentLruCache(int maxSize, Function 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; + this.lock = new ReentrantReadWriteLock(); + } + + public V get(K key) { + V cached = this.cache.get(key); + if (cached != null) { + if (this.size < this.maxSize) { + return cached; + } + this.lock.readLock().lock(); + try { + if (this.queue.removeLastOccurrence(key)) { + this.queue.offer(key); + } + return cached; + } finally { + this.lock.readLock().unlock(); + } + } + this.lock.writeLock().lock(); + try { + // Retrying in case of concurrent reads on the same key + cached = this.cache.get(key); + if (cached != null) { + if (this.queue.removeLastOccurrence(key)) { + this.queue.offer(key); + } + return cached; + } + // Generate value first, to prevent size inconsistency + V value = this.generator.apply(key); + int cacheSize = this.size; + if (cacheSize == this.maxSize) { + K leastUsed = this.queue.poll(); + if (leastUsed != null) { + this.cache.remove(leastUsed); + cacheSize--; + } + } + this.queue.offer(key); + this.cache.put(key, value); + this.size = cacheSize + 1; + return value; + } finally { + this.lock.writeLock().unlock(); + } + } + } }