org.thymeleaf.cache.StandardCache Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of org.everit.osgi.bundles.org.thymeleaf.thymeleaf Show documentation
Show all versions of org.everit.osgi.bundles.org.thymeleaf.thymeleaf Show documentation
XML/XHTML/HTML5 template engine for Java
The newest version!
/*
* =============================================================================
*
* Copyright (c) 2011-2013, The THYMELEAF team (http://www.thymeleaf.org)
*
* 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
*
* http://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.thymeleaf.cache;
import java.lang.ref.SoftReference;
import java.util.Arrays;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.slf4j.Logger;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.util.Validate;
/**
*
*
* @author Daniel Fernández
* @author Guven Demir
*
* @since 2.0.0
*
* @param The type of the cache keys
* @param The type of the cache values
*/
public final class StandardCache implements ICache {
private static final long REPORT_INTERVAL = 300000L; // 5 minutes
private static final String REPORT_FORMAT =
"[THYMELEAF][*][*][*][CACHE_REPORT] %8s elements | %12s puts | %12s gets | %12s hits | %12s misses - [%s]";
private volatile long lastExecution = System.currentTimeMillis();
private final String name;
private final boolean useSoftReferences;
private final int maxSize;
private final CacheDataContainer dataContainer;
private final ICacheEntryValidityChecker super K, ? super V> entryValidityChecker;
private final boolean traceExecution;
private final Logger logger;
private final AtomicLong getCount;
private final AtomicLong putCount;
private final AtomicLong hitCount;
private final AtomicLong missCount;
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final Logger logger) {
this(name, useSoftReferences, initialCapacity, -1, null, logger);
}
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final ICacheEntryValidityChecker super K, ? super V> entryValidityChecker,
final Logger logger) {
this(name, useSoftReferences, initialCapacity, -1, entryValidityChecker, logger);
}
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final int maxSize, final Logger logger) {
this(name, useSoftReferences, initialCapacity, maxSize, null, logger);
}
public StandardCache(final String name, final boolean useSoftReferences,
final int initialCapacity, final int maxSize, final ICacheEntryValidityChecker super K, ? super V> entryValidityChecker,
final Logger logger) {
super();
Validate.notEmpty(name, "Name cannot be null or empty");
Validate.isTrue(initialCapacity > 0, "Initial capacity must be > 0");
Validate.isTrue(maxSize != 0, "Cache max size must be either -1 (no limit) or > 0");
this.name = name;
this.useSoftReferences = useSoftReferences;
this.maxSize = maxSize;
this.entryValidityChecker = entryValidityChecker;
this.logger = logger;
this.traceExecution = (logger != null && logger.isTraceEnabled());
this.dataContainer =
new CacheDataContainer(this.name, initialCapacity, maxSize, this.traceExecution, this.logger);
this.getCount = new AtomicLong(0);
this.putCount = new AtomicLong(0);
this.hitCount = new AtomicLong(0);
this.missCount = new AtomicLong(0);
if (this.logger != null) {
if (this.maxSize < 0) {
this.logger.debug("[THYMELEAF][CACHE_INITIALIZE] Initializing cache {}. Soft references {}.",
this.name, (this.useSoftReferences? "are used" : "not used"));
} else {
this.logger.debug("[THYMELEAF][CACHE_INITIALIZE] Initializing cache {}. Max size: {}. Soft references {}.",
new Object[] {this.name, Integer.valueOf(this.maxSize), (this.useSoftReferences? "are used" : "not used")});
}
}
}
// -----
public void put(final K key, final V value) {
incrementReportEntity(this.putCount);
final CacheEntry entry = new CacheEntry(value, this.useSoftReferences);
// newSize will be -1 if traceExecution is false
final int newSize = this.dataContainer.put(key, entry);
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_ADD][{}] Adding cache entry in cache \"{}\" for key \"{}\". New size is {}.",
new Object[] {TemplateEngine.threadIndex(), this.name, Integer.valueOf(newSize), this.name, key, Integer.valueOf(newSize)});
}
outputReportIfNeeded();
}
public V get(final K key) {
return get(key, this.entryValidityChecker);
}
public V get(final K key, final ICacheEntryValidityChecker super K, ? super V> validityChecker) {
incrementReportEntity(this.getCount);
final CacheEntry resultEntry = this.dataContainer.get(key);
if (resultEntry == null) {
incrementReportEntity(this.missCount);
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_MISS] Cache miss in cache \"{}\" for key \"{}\".",
new Object[] {TemplateEngine.threadIndex(), this.name, this.name, key});
}
outputReportIfNeeded();
return null;
}
final V resultValue =
resultEntry.getValueIfStillValid(this.name, key, validityChecker, this.traceExecution, this.logger);
if (resultValue == null) {
final int newSize = this.dataContainer.remove(key);
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_REMOVE][{}] Removing cache entry in cache \"{}\" (Entry \"{}\" is not valid anymore). New size is {}.",
new Object[] {TemplateEngine.threadIndex(), this.name, Integer.valueOf(newSize), this.name, key, Integer.valueOf(newSize)});
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_MISS] Cache miss in cache \"{}\" for key \"{}\".",
new Object[] {TemplateEngine.threadIndex(), this.name, this.name, key});
}
incrementReportEntity(this.missCount);
outputReportIfNeeded();
return null;
}
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_HIT] Cache hit in cache \"{}\" for key \"{}\".",
new Object[] {TemplateEngine.threadIndex(), this.name, this.name, key});
}
incrementReportEntity(this.hitCount);
outputReportIfNeeded();
return resultValue;
}
public void clear() {
this.dataContainer.clear();
if (this.traceExecution) {
this.logger.trace(
"[THYMELEAF][{}][*][{}][CACHE_REMOVE][0] Removing ALL cache entries in cache \"{}\". New size is 0.",
new Object[] {TemplateEngine.threadIndex(), this.name, this.name});
}
}
public void clearKey(final K key) {
final int newSize = this.dataContainer.remove(key);
if (this.traceExecution && newSize != -1) {
this.logger.trace(
"[THYMELEAF][{}][*][{}][CACHE_REMOVE][{}] Removed cache entry in cache \"{}\" for key \"{}\". New size is {}.",
new Object[] {TemplateEngine.threadIndex(), this.name, Integer.valueOf(newSize), this.name, key, Integer.valueOf(newSize)});
}
}
// -----
public String getName() {
return this.name;
}
public boolean hasMaxSize() {
return (this.maxSize > 0);
}
public int getMaxSize() {
return this.maxSize;
}
public boolean getUseSoftReferences() {
return this.useSoftReferences;
}
public int size() {
return this.dataContainer.size();
}
// -----
private void incrementReportEntity(final AtomicLong entity) {
if (this.traceExecution) {
entity.incrementAndGet();
}
}
private void outputReportIfNeeded() {
if (this.traceExecution) { // fail fast
final long currentTime = System.currentTimeMillis();
if ((currentTime - this.lastExecution) >= REPORT_INTERVAL) { // first check without need to sync
synchronized (this) {
if ((currentTime - this.lastExecution) >= REPORT_INTERVAL) {
this.logger.trace(
String.format(REPORT_FORMAT,
Integer.valueOf(size()),
Long.valueOf(this.putCount.get()),
Long.valueOf(this.getCount.get()),
Long.valueOf(this.hitCount.get()),
Long.valueOf(this.missCount.get()),
this.name));
this.lastExecution = currentTime;
}
}
}
}
}
static final class CacheDataContainer {
private final String name;
private final boolean sizeLimit;
private final int maxSize;
private final boolean traceExecution;
private final Logger logger;
private final ConcurrentHashMap> container;
private final Object[] fifo;
private int fifoPointer;
CacheDataContainer(final String name, final int initialCapacity,
final int maxSize, final boolean traceExecution, final Logger logger) {
super();
this.name = name;
this.container = new ConcurrentHashMap>(initialCapacity);
this.maxSize = maxSize;
this.sizeLimit = (maxSize >= 0);
if (this.sizeLimit) {
this.fifo = new Object[this.maxSize];
Arrays.fill(this.fifo, null);
} else {
this.fifo = null;
}
this.fifoPointer = 0;
this.traceExecution = traceExecution;
this.logger = logger;
}
public CacheEntry get(final Object key) {
// FIFO is not used for this --> better performance, but no LRU (only insertion order will apply)
return this.container.get(key);
}
public int put(final K key, final CacheEntry value) {
if (this.traceExecution) {
return putWithTracing(key, value);
}
return putWithoutTracing(key, value);
}
private int putWithoutTracing(final K key, final CacheEntry value) {
// If we are not tracing, it's better to avoid the size() operation which has
// some performance implications in ConcurrentHashMap (iteration and counting these maps
// is slow if they are big)
final CacheEntry existing = this.container.putIfAbsent(key, value);
if (existing != null) {
// When not in 'trace' mode, will always return -1
return -1;
}
if (this.sizeLimit) {
synchronized (this.fifo) {
final Object removedKey = this.fifo[this.fifoPointer];
if (removedKey != null) {
this.container.remove(removedKey);
}
this.fifo[this.fifoPointer] = key;
this.fifoPointer = (this.fifoPointer + 1) % this.maxSize;
}
}
return -1;
}
private synchronized int putWithTracing(final K key, final CacheEntry value) {
final CacheEntry existing = this.container.putIfAbsent(key, value);
if (existing == null) {
if (this.sizeLimit) {
final Object removedKey = this.fifo[this.fifoPointer];
if (removedKey != null) {
final CacheEntry removed = this.container.remove(removedKey);
if (removed != null) {
final Integer newSize = Integer.valueOf(this.container.size());
this.logger.trace(
"[THYMELEAF][{}][{}][CACHE_REMOVE][{}] Max size exceeded for cache \"{}\". Removing entry for key \"{}\". New size is {}.",
new Object[] {TemplateEngine.threadIndex(), this.name, newSize, this.name, removedKey, newSize});
}
}
this.fifo[this.fifoPointer] = key;
this.fifoPointer = (this.fifoPointer + 1) % this.maxSize;
}
}
return this.container.size();
}
public int remove(final K key) {
if (this.traceExecution) {
return removeWithTracing(key);
}
return removeWithoutTracing(key);
}
private int removeWithoutTracing(final K key) {
// FIFO is not updated, not a real benefit in doing it.
this.container.remove(key);
return -1;
}
private synchronized int removeWithTracing(final K key) {
// FIFO is not updated, not a real benefit in doing it.
final CacheEntry removed = this.container.remove(key);
if (removed == null) {
// When tracing is active, this means nothing was removed
return -1;
}
return this.container.size();
}
public void clear() {
this.container.clear();
}
public int size() {
return this.container.size();
}
}
static final class CacheEntry {
private final SoftReference cachedValueReference;
private final long creationTimeInMillis;
// Although we will use the reference for normal operation for cleaner code, this
// variable will act as an "anchor" to avoid the value to be cleaned if we don't
// want the reference type to be "soft"
@SuppressWarnings("unused")
private final V cachedValueAnchor;
CacheEntry(final V cachedValue, final boolean useSoftReferences) {
super();
this.cachedValueReference = new SoftReference(cachedValue);
this.cachedValueAnchor = (!useSoftReferences? cachedValue : null);
this.creationTimeInMillis = System.currentTimeMillis();
}
public V getValueIfStillValid(final String cacheMapName,
final K key, final ICacheEntryValidityChecker super K, ? super V> checker,
final boolean traceExecution, final Logger logger) {
final V cachedValue = this.cachedValueReference.get();
if (cachedValue == null) {
// The soft reference has been cleared by GC -> Memory could be running low
if (traceExecution) {
logger.trace(
"[THYMELEAF][{}][*][{}][CACHE_DELETED_REFERENCES] Some entries at cache \"{}\" " +
"seem to have been sacrificed by the Garbage Collector (soft references).",
new Object[] {TemplateEngine.threadIndex(), cacheMapName, cacheMapName});
}
return null;
}
if (checker == null || checker.checkIsValueStillValid(key, cachedValue, this.creationTimeInMillis)) {
return cachedValue;
}
return null;
}
public long getCreationTimeInMillis() {
return this.creationTimeInMillis;
}
}
}