Skip to content

Commit d198c44

Browse files
committed
Extract ConcurrentLruCache for reuse in NamedParameterJdbcTemplate
Closes gh-24197
1 parent 6884a3a commit d198c44

File tree

3 files changed

+150
-105
lines changed

3 files changed

+150
-105
lines changed
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2002-2020 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+
* https://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.util;
18+
19+
import java.util.concurrent.ConcurrentHashMap;
20+
import java.util.concurrent.ConcurrentLinkedDeque;
21+
import java.util.concurrent.locks.ReadWriteLock;
22+
import java.util.concurrent.locks.ReentrantReadWriteLock;
23+
import java.util.function.Function;
24+
25+
/**
26+
* Simple LRU (Least Recently Used) cache, bounded by a specified cache limit.
27+
*
28+
* <p>This implementation is backed by a {@code ConcurrentHashMap} for storing
29+
* the cached values and a {@code ConcurrentLinkedDeque} for ordering the keys
30+
* and choosing the least recently used key when the cache is at full capacity.
31+
*
32+
* @author Brian Clozel
33+
* @author Juergen Hoeller
34+
* @since 5.3
35+
* @param <K> the type of the key used for cache retrieval
36+
* @param <V> the type of the cached values
37+
* @see #get
38+
*/
39+
public class ConcurrentLruCache<K, V> {
40+
41+
private final int sizeLimit;
42+
43+
private final Function<K, V> generator;
44+
45+
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
46+
47+
private final ConcurrentLinkedDeque<K> queue = new ConcurrentLinkedDeque<>();
48+
49+
private final ReadWriteLock lock = new ReentrantReadWriteLock();
50+
51+
private volatile int size;
52+
53+
54+
/**
55+
* Create a new cache instance with the given limit and generator function.
56+
* @param sizeLimit the maximum number of entries in the cache
57+
* (0 indicates no caching, always generating a new value)
58+
* @param generator a function to generate a new value for a given key
59+
*/
60+
public ConcurrentLruCache(int sizeLimit, Function<K, V> generator) {
61+
Assert.isTrue(sizeLimit >= 0, "Cache size limit must not be negative");
62+
Assert.notNull(generator, "Generator function must not be null");
63+
this.sizeLimit = sizeLimit;
64+
this.generator = generator;
65+
}
66+
67+
68+
/**
69+
* Retrieve an entry from the cache, potentially triggering generation
70+
* of the value.
71+
* @param key the key to retrieve the entry for
72+
* @return the cached or newly generated value
73+
*/
74+
public V get(K key) {
75+
if (this.sizeLimit == 0) {
76+
return this.generator.apply(key);
77+
}
78+
79+
V cached = this.cache.get(key);
80+
if (cached != null) {
81+
if (this.size < this.sizeLimit) {
82+
return cached;
83+
}
84+
this.lock.readLock().lock();
85+
try {
86+
if (this.queue.removeLastOccurrence(key)) {
87+
this.queue.offer(key);
88+
}
89+
return cached;
90+
}
91+
finally {
92+
this.lock.readLock().unlock();
93+
}
94+
}
95+
96+
this.lock.writeLock().lock();
97+
try {
98+
// Retrying in case of concurrent reads on the same key
99+
cached = this.cache.get(key);
100+
if (cached != null) {
101+
if (this.queue.removeLastOccurrence(key)) {
102+
this.queue.offer(key);
103+
}
104+
return cached;
105+
}
106+
// Generate value first, to prevent size inconsistency
107+
V value = this.generator.apply(key);
108+
int cacheSize = this.size;
109+
if (cacheSize == this.sizeLimit) {
110+
K leastUsed = this.queue.poll();
111+
if (leastUsed != null) {
112+
this.cache.remove(leastUsed);
113+
cacheSize--;
114+
}
115+
}
116+
this.queue.offer(key);
117+
this.cache.put(key, value);
118+
this.size = cacheSize + 1;
119+
return value;
120+
}
121+
finally {
122+
this.lock.writeLock().unlock();
123+
}
124+
}
125+
126+
/**
127+
* Return the current size of the cache.
128+
* @see #sizeLimit()
129+
*/
130+
public int size() {
131+
return this.size;
132+
}
133+
134+
/**
135+
* Return the the maximum number of entries in the cache
136+
* (0 indicates no caching, always generating a new value).
137+
* @see #size()
138+
*/
139+
public int sizeLimit() {
140+
return this.sizeLimit;
141+
}
142+
143+
}

spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java

Lines changed: 0 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,6 @@
2828
import java.util.List;
2929
import java.util.Map;
3030
import java.util.Random;
31-
import java.util.concurrent.ConcurrentHashMap;
32-
import java.util.concurrent.ConcurrentLinkedDeque;
33-
import java.util.concurrent.locks.ReadWriteLock;
34-
import java.util.concurrent.locks.ReentrantReadWriteLock;
35-
import java.util.function.Function;
3631
import java.util.stream.Collectors;
3732

3833
import org.springframework.lang.Nullable;
@@ -406,84 +401,4 @@ public static String generateMultipartBoundaryString() {
406401
return new String(generateMultipartBoundary(), StandardCharsets.US_ASCII);
407402
}
408403

409-
410-
/**
411-
* Simple Least Recently Used cache, bounded by the maximum size given
412-
* to the class constructor.
413-
* <p>This implementation is backed by a {@code ConcurrentHashMap} for storing
414-
* the cached values and a {@code ConcurrentLinkedQueue} for ordering the keys
415-
* and choosing the least recently used key when the cache is at full capacity.
416-
* @param <K> the type of the key used for caching
417-
* @param <V> the type of the cached values
418-
*/
419-
private static class ConcurrentLruCache<K, V> {
420-
421-
private final int maxSize;
422-
423-
private final ConcurrentLinkedDeque<K> queue = new ConcurrentLinkedDeque<>();
424-
425-
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
426-
427-
private final ReadWriteLock lock;
428-
429-
private final Function<K, V> generator;
430-
431-
private volatile int size;
432-
433-
public ConcurrentLruCache(int maxSize, Function<K, V> generator) {
434-
Assert.isTrue(maxSize > 0, "LRU max size should be positive");
435-
Assert.notNull(generator, "Generator function should not be null");
436-
this.maxSize = maxSize;
437-
this.generator = generator;
438-
this.lock = new ReentrantReadWriteLock();
439-
}
440-
441-
public V get(K key) {
442-
V cached = this.cache.get(key);
443-
if (cached != null) {
444-
if (this.size < this.maxSize) {
445-
return cached;
446-
}
447-
this.lock.readLock().lock();
448-
try {
449-
if (this.queue.removeLastOccurrence(key)) {
450-
this.queue.offer(key);
451-
}
452-
return cached;
453-
}
454-
finally {
455-
this.lock.readLock().unlock();
456-
}
457-
}
458-
this.lock.writeLock().lock();
459-
try {
460-
// Retrying in case of concurrent reads on the same key
461-
cached = this.cache.get(key);
462-
if (cached != null) {
463-
if (this.queue.removeLastOccurrence(key)) {
464-
this.queue.offer(key);
465-
}
466-
return cached;
467-
}
468-
// Generate value first, to prevent size inconsistency
469-
V value = this.generator.apply(key);
470-
int cacheSize = this.size;
471-
if (cacheSize == this.maxSize) {
472-
K leastUsed = this.queue.poll();
473-
if (leastUsed != null) {
474-
this.cache.remove(leastUsed);
475-
cacheSize--;
476-
}
477-
}
478-
this.queue.offer(key);
479-
this.cache.put(key, value);
480-
this.size = cacheSize + 1;
481-
return value;
482-
}
483-
finally {
484-
this.lock.writeLock().unlock();
485-
}
486-
}
487-
}
488-
489404
}

spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.sql.PreparedStatement;
2020
import java.sql.SQLException;
21-
import java.util.LinkedHashMap;
2221
import java.util.List;
2322
import java.util.Map;
2423
import java.util.function.Consumer;
@@ -45,6 +44,7 @@
4544
import org.springframework.jdbc.support.rowset.SqlRowSet;
4645
import org.springframework.lang.Nullable;
4746
import org.springframework.util.Assert;
47+
import org.springframework.util.ConcurrentLruCache;
4848

4949
/**
5050
* Template class with a basic set of JDBC operations, allowing the use
@@ -76,17 +76,9 @@ public class NamedParameterJdbcTemplate implements NamedParameterJdbcOperations
7676
/** The JdbcTemplate we are wrapping. */
7777
private final JdbcOperations classicJdbcTemplate;
7878

79-
private volatile int cacheLimit = DEFAULT_CACHE_LIMIT;
80-
8179
/** Cache of original SQL String to ParsedSql representation. */
82-
@SuppressWarnings("serial")
83-
private final Map<String, ParsedSql> parsedSqlCache =
84-
new LinkedHashMap<String, ParsedSql>(DEFAULT_CACHE_LIMIT, 0.75f, true) {
85-
@Override
86-
protected boolean removeEldestEntry(Map.Entry<String, ParsedSql> eldest) {
87-
return size() > getCacheLimit();
88-
}
89-
};
80+
private volatile ConcurrentLruCache<String, ParsedSql> parsedSqlCache =
81+
new ConcurrentLruCache<>(DEFAULT_CACHE_LIMIT, NamedParameterUtils::parseSqlStatement);
9082

9183

9284
/**
@@ -133,17 +125,17 @@ public JdbcTemplate getJdbcTemplate() {
133125

134126
/**
135127
* Specify the maximum number of entries for this template's SQL cache.
136-
* Default is 256.
128+
* Default is 256. 0 indicates no caching, always parsing each statement.
137129
*/
138130
public void setCacheLimit(int cacheLimit) {
139-
this.cacheLimit = cacheLimit;
131+
this.parsedSqlCache = new ConcurrentLruCache<>(cacheLimit, NamedParameterUtils::parseSqlStatement);
140132
}
141133

142134
/**
143135
* Return the maximum number of entries for this template's SQL cache.
144136
*/
145137
public int getCacheLimit() {
146-
return this.cacheLimit;
138+
return this.parsedSqlCache.sizeLimit();
147139
}
148140

149141

@@ -441,12 +433,7 @@ protected PreparedStatementCreator getPreparedStatementCreator(String sql, SqlPa
441433
* @return a representation of the parsed SQL statement
442434
*/
443435
protected ParsedSql getParsedSql(String sql) {
444-
if (getCacheLimit() <= 0) {
445-
return NamedParameterUtils.parseSqlStatement(sql);
446-
}
447-
synchronized (this.parsedSqlCache) {
448-
return this.parsedSqlCache.computeIfAbsent(sql, NamedParameterUtils::parseSqlStatement);
449-
}
436+
return this.parsedSqlCache.get(sql);
450437
}
451438

452439
/**

0 commit comments

Comments
 (0)