diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java new file mode 100644 index 00000000000..1af6d2e5aed --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2020 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.util; + +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; + +/** + * Simple LRU (Least Recently Used) cache, bounded by a specified cache limit. + * + *

This implementation is backed by a {@code ConcurrentHashMap} for storing + * the cached values and a {@code ConcurrentLinkedDeque} for ordering the keys + * and choosing the least recently used key when the cache is at full capacity. + * + * @author Brian Clozel + * @author Juergen Hoeller + * @since 5.3 + * @param the type of the key used for cache retrieval + * @param the type of the cached values + * @see #get + */ +public class ConcurrentLruCache { + + private final int sizeLimit; + + private final Function generator; + + private final ConcurrentHashMap cache = new ConcurrentHashMap<>(); + + private final ConcurrentLinkedDeque queue = new ConcurrentLinkedDeque<>(); + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + private volatile int size; + + + /** + * Create a new cache instance with the given limit and generator function. + * @param sizeLimit the maximum number of entries in the cache + * (0 indicates no caching, always generating a new value) + * @param generator a function to generate a new value for a given key + */ + public ConcurrentLruCache(int sizeLimit, Function generator) { + Assert.isTrue(sizeLimit >= 0, "Cache size limit must not be negative"); + Assert.notNull(generator, "Generator function must not be null"); + this.sizeLimit = sizeLimit; + this.generator = generator; + } + + + /** + * Retrieve an entry from the cache, potentially triggering generation + * of the value. + * @param key the key to retrieve the entry for + * @return the cached or newly generated value + */ + public V get(K key) { + if (this.sizeLimit == 0) { + return this.generator.apply(key); + } + + V cached = this.cache.get(key); + if (cached != null) { + if (this.size < this.sizeLimit) { + 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.sizeLimit) { + 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(); + } + } + + /** + * Return the current size of the cache. + * @see #sizeLimit() + */ + public int size() { + return this.size; + } + + /** + * Return the the maximum number of entries in the cache + * (0 indicates no caching, always generating a new value). + * @see #size() + */ + public int sizeLimit() { + return this.sizeLimit; + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 846ad1c0950..05809bc5ad7 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -28,11 +28,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Random; -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.stream.Collectors; import org.springframework.lang.Nullable; @@ -406,84 +401,4 @@ public abstract class MimeTypeUtils { return new String(generateMultipartBoundary(), StandardCharsets.US_ASCII); } - - /** - * 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; - - 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(); - } - } - } - } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java index d92ffa04a4f..ef7b6567dfc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java @@ -18,7 +18,6 @@ package org.springframework.jdbc.core.namedparam; import java.sql.PreparedStatement; import java.sql.SQLException; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -45,6 +44,7 @@ import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ConcurrentLruCache; /** * Template class with a basic set of JDBC operations, allowing the use @@ -76,17 +76,9 @@ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations /** The JdbcTemplate we are wrapping. */ private final JdbcOperations classicJdbcTemplate; - private volatile int cacheLimit = DEFAULT_CACHE_LIMIT; - /** Cache of original SQL String to ParsedSql representation. */ - @SuppressWarnings("serial") - private final Map parsedSqlCache = - new LinkedHashMap(DEFAULT_CACHE_LIMIT, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > getCacheLimit(); - } - }; + private volatile ConcurrentLruCache parsedSqlCache = + new ConcurrentLruCache<>(DEFAULT_CACHE_LIMIT, NamedParameterUtils::parseSqlStatement); /** @@ -133,17 +125,17 @@ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations /** * Specify the maximum number of entries for this template's SQL cache. - * Default is 256. + * Default is 256. 0 indicates no caching, always parsing each statement. */ public void setCacheLimit(int cacheLimit) { - this.cacheLimit = cacheLimit; + this.parsedSqlCache = new ConcurrentLruCache<>(cacheLimit, NamedParameterUtils::parseSqlStatement); } /** * Return the maximum number of entries for this template's SQL cache. */ public int getCacheLimit() { - return this.cacheLimit; + return this.parsedSqlCache.sizeLimit(); } @@ -441,12 +433,7 @@ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations * @return a representation of the parsed SQL statement */ protected ParsedSql getParsedSql(String sql) { - if (getCacheLimit() <= 0) { - return NamedParameterUtils.parseSqlStatement(sql); - } - synchronized (this.parsedSqlCache) { - return this.parsedSqlCache.computeIfAbsent(sql, NamedParameterUtils::parseSqlStatement); - } + return this.parsedSqlCache.get(sql); } /**