Add support for TTI expiration in Redis Cache implementation.

We now support time-to-idle (TTI) expiration policies for cache reads.

The TTI implementation is achieved with the use of the Redis GETEX command on Cache.get(key) operations as well as consistently using the same TTL configuration for all cache operations when TTI is enabled and TTL expiration has been configured,
with the use of a TtlFunction or fixed Duration.

Closes #2351
Original pull request: #2643
This commit is contained in:
John Blum
2023-07-20 15:37:38 -07:00
committed by Christoph Strobl
parent 862e3446bc
commit dddf3530b9
12 changed files with 600 additions and 90 deletions

View File

@@ -125,11 +125,18 @@ class DefaultRedisCacheWriter implements RedisCacheWriter {
@Override
public byte[] get(String name, byte[] key) {
return get(name, key, null);
}
@Override
public byte[] get(String name, byte[] key, @Nullable Duration ttl) {
Assert.notNull(name, "Name must not be null");
Assert.notNull(key, "Key must not be null");
byte[] result = execute(name, connection -> connection.get(key));
byte[] result = shouldExpireWithin(ttl)
? execute(name, connection -> connection.getEx(key, Expiration.from(ttl)))
: execute(name, connection -> connection.get(key));
statistics.incGets(name);

View File

@@ -17,6 +17,7 @@ package org.springframework.data.redis.cache;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Map;
@@ -188,11 +189,21 @@ public class RedisCache extends AbstractValueAdaptingCache {
@Override
protected Object lookup(Object key) {
byte[] value = getCacheWriter().get(getName(), createAndConvertCacheKey(key));
byte[] value = getCacheConfiguration().isTimeToIdleEnabled()
? getCacheWriter().get(getName(), createAndConvertCacheKey(key), getTimeToLive(key))
: getCacheWriter().get(getName(), createAndConvertCacheKey(key));
return value != null ? deserializeCacheValue(value) : null;
}
private Duration getTimeToLive(Object key) {
return getTimeToLive(key, null);
}
private Duration getTimeToLive(Object key, @Nullable Object value) {
return getCacheConfiguration().getTtlFunction().getTimeToLive(key, value);
}
@Override
public void put(Object key, @Nullable Object value) {
@@ -208,7 +219,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
}
getCacheWriter().put(getName(), createAndConvertCacheKey(key), serializeCacheValue(cacheValue),
getCacheConfiguration().getTtlFunction().getTimeToLive(key, value));
getTimeToLive(key, value));
}
@Override
@@ -221,7 +232,7 @@ public class RedisCache extends AbstractValueAdaptingCache {
}
byte[] result = getCacheWriter().putIfAbsent(getName(), createAndConvertCacheKey(key),
serializeCacheValue(cacheValue), getCacheConfiguration().getTtlFunction().getTimeToLive(key, value));
serializeCacheValue(cacheValue), getTimeToLive(key, value));
return result != null ? new SimpleValueWrapper(fromStoreValue(deserializeCacheValue(result))) : null;
}

View File

@@ -47,9 +47,11 @@ import org.springframework.util.Assert;
public class RedisCacheConfiguration {
protected static final boolean DEFAULT_CACHE_NULL_VALUES = true;
protected static final boolean DEFAULT_ENABLE_TIME_TO_IDLE_EXPIRATION = false;
protected static final boolean DEFAULT_USE_PREFIX = true;
protected static final boolean DO_NOT_CACHE_NULL_VALUES = false;
protected static final boolean DO_NOT_USE_PREFIX = false;
protected static final boolean USE_TIME_TO_IDLE_EXPIRATION = true;
/**
* Default {@link RedisCacheConfiguration} using the following:
@@ -108,7 +110,10 @@ public class RedisCacheConfiguration {
registerDefaultConverters(conversionService);
return new RedisCacheConfiguration(TtlFunction.persistent(), DEFAULT_CACHE_NULL_VALUES, DEFAULT_USE_PREFIX,
return new RedisCacheConfiguration(TtlFunction.persistent(),
DEFAULT_CACHE_NULL_VALUES,
DEFAULT_ENABLE_TIME_TO_IDLE_EXPIRATION,
DEFAULT_USE_PREFIX,
CacheKeyPrefix.simple(),
SerializationPair.fromSerializer(RedisSerializer.string()),
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)),
@@ -116,6 +121,7 @@ public class RedisCacheConfiguration {
}
private final boolean cacheNullValues;
private final boolean enableTimeToIdle;
private final boolean usePrefix;
private final CacheKeyPrefix keyPrefix;
@@ -128,12 +134,13 @@ public class RedisCacheConfiguration {
private final TtlFunction ttlFunction;
@SuppressWarnings("unchecked")
private RedisCacheConfiguration(TtlFunction ttlFunction, Boolean cacheNullValues, Boolean usePrefix,
CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair,
private RedisCacheConfiguration(TtlFunction ttlFunction, Boolean cacheNullValues, Boolean enableTimeToIdle,
Boolean usePrefix, CacheKeyPrefix keyPrefix, SerializationPair<String> keySerializationPair,
SerializationPair<?> valueSerializationPair, ConversionService conversionService) {
this.ttlFunction = ttlFunction;
this.cacheNullValues = cacheNullValues;
this.enableTimeToIdle = enableTimeToIdle;
this.usePrefix = usePrefix;
this.keyPrefix = keyPrefix;
this.keySerializationPair = keySerializationPair;
@@ -168,8 +175,9 @@ public class RedisCacheConfiguration {
Assert.notNull(cacheKeyPrefix, "Function used to compute prefix must not be null");
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), DEFAULT_USE_PREFIX,
cacheKeyPrefix, getKeySerializationPair(), getValueSerializationPair(), getConversionService());
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTimeToIdleEnabled(),
DEFAULT_USE_PREFIX, cacheKeyPrefix, getKeySerializationPair(), getValueSerializationPair(),
getConversionService());
}
/**
@@ -181,8 +189,9 @@ public class RedisCacheConfiguration {
* @return new {@link RedisCacheConfiguration}.
*/
public RedisCacheConfiguration disableCachingNullValues() {
return new RedisCacheConfiguration(getTtlFunction(), DO_NOT_CACHE_NULL_VALUES, usePrefix(), getKeyPrefix(),
getKeySerializationPair(), getValueSerializationPair(), getConversionService());
return new RedisCacheConfiguration(getTtlFunction(), DO_NOT_CACHE_NULL_VALUES, isTimeToIdleEnabled(),
usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(),
getConversionService());
}
/**
@@ -193,8 +202,31 @@ public class RedisCacheConfiguration {
* @return new {@link RedisCacheConfiguration}.
*/
public RedisCacheConfiguration disableKeyPrefix() {
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), DO_NOT_USE_PREFIX,
getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), getConversionService());
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTimeToIdleEnabled(),
DO_NOT_USE_PREFIX, getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), getConversionService());
}
/**
* Enables {@literal time-to-idle (TTI) expiration} on {@link Cache} read operations,
* such as {@link Cache#get(Object)}.
* <p>
* Enabling this option applies the same {@link #getTtlFunction() TTL expiration policy} to {@link Cache} read
* operations as it does for {@link Cache} write operations. In effect, this will invoke the Redis {@literal GETEX}
* command in place of {@literal GET}.
* <p>
* Redis does not support the concept of {@literal TTI}, only {@literal TTL}. However, if {@literal TTL} expiration
* is applied to all {@link Cache} operations, both read and write alike, and {@link Cache} operations passed with
* expiration are used consistently across the application, then in effect, an application can achieve
* {@literal TTI} expiration-like behavior.
*
* @return this {@link RedisCacheConfiguration}.
* @see <a href="https://redis.io/commands/getex/">GETEX</a>
* @since 3.2.0
*/
public RedisCacheConfiguration enableTimeToIdle() {
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), USE_TIME_TO_IDLE_EXPIRATION,
usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(),
getConversionService());
}
/**
@@ -222,8 +254,9 @@ public class RedisCacheConfiguration {
Assert.notNull(ttlFunction, "TtlFunction must not be null");
return new RedisCacheConfiguration(ttlFunction, getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
getKeySerializationPair(), getValueSerializationPair(), getConversionService());
return new RedisCacheConfiguration(ttlFunction, getAllowCacheNullValues(), isTimeToIdleEnabled(),
usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(),
getConversionService());
}
/**
@@ -236,8 +269,8 @@ public class RedisCacheConfiguration {
Assert.notNull(keySerializationPair, "KeySerializationPair must not be null");
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
keySerializationPair, getValueSerializationPair(), getConversionService());
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTimeToIdleEnabled(),
usePrefix(), getKeyPrefix(), keySerializationPair, getValueSerializationPair(), getConversionService());
}
/**
@@ -250,8 +283,8 @@ public class RedisCacheConfiguration {
Assert.notNull(valueSerializationPair, "ValueSerializationPair must not be null");
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
getKeySerializationPair(), valueSerializationPair, getConversionService());
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTimeToIdleEnabled(),
usePrefix(), getKeyPrefix(), getKeySerializationPair(), valueSerializationPair, getConversionService());
}
/**
@@ -264,8 +297,8 @@ public class RedisCacheConfiguration {
Assert.notNull(conversionService, "ConversionService must not be null");
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), usePrefix(), getKeyPrefix(),
getKeySerializationPair(), getValueSerializationPair(), conversionService);
return new RedisCacheConfiguration(getTtlFunction(), getAllowCacheNullValues(), isTimeToIdleEnabled(),
usePrefix(), getKeyPrefix(), getKeySerializationPair(), getValueSerializationPair(), conversionService);
}
/**
@@ -275,6 +308,20 @@ public class RedisCacheConfiguration {
return this.cacheNullValues;
}
/**
* Determines whether {@literal time-to-idle (TTI) expiration} has been enabled for caching.
* <p>
* Use {@link #enableTimeToIdle()} to opt-in and enable {@literal time-to-idle (TTI) expiration} for caching.
*
* @return {@literal true} if {@literal time-to-idle (TTI) expiration} was configured and enabled for caching.
* Defaults to {@literal false}.
* @see <a href="https://redis.io/commands/getex/">GETEX</a>
* @since 3.2.0
*/
public boolean isTimeToIdleEnabled() {
return this.enableTimeToIdle;
}
/**
* @return {@literal true} if cache keys need to be prefixed with the {@link #getKeyPrefixFor(String)} if present or
* the default which resolves to {@link Cache#getName()}.

View File

@@ -22,7 +22,6 @@ import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
@@ -67,10 +66,10 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
/**
* Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager}
* using the given {@link RedisCacheWriter}.
* initialized with the given {@link RedisCacheWriter}.
*
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations by executing
* appropriate Redis commands; must not be {@literal null}.
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations
* by executing appropriate Redis commands; must not be {@literal null}.
* @return new {@link RedisCacheManagerBuilder}.
* @throws IllegalArgumentException if the given {@link RedisCacheWriter} is {@literal null}.
* @see org.springframework.data.redis.cache.RedisCacheWriter
@@ -84,7 +83,7 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
/**
* Factory method returning a {@literal Builder} used to construct and configure a {@link RedisCacheManager}
* using the given {@link RedisConnectionFactory}.
* initialized with the given {@link RedisConnectionFactory}.
*
* @param connectionFactory {@link RedisConnectionFactory} used by the {@link RedisCacheManager}
* to acquire connections to Redis when performing {@link RedisCache} operations; must not be {@literal null}.
@@ -100,8 +99,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
}
/**
* Factory method used to construct a new {@link RedisCacheManager} using the given {@link RedisConnectionFactory}
* with caching defaults applied.
* Factory method used to construct a new {@link RedisCacheManager} initialized with
* the given {@link RedisConnectionFactory} and using the defaults for caching.
*
* <dl>
* <dt>locking</dt>
@@ -143,8 +142,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
/**
* Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter}
* and a default {@link RedisCacheConfiguration}.
*
* Allows cache creation at runtime.
* <p>
* Allows {@link RedisCache cache} creation at runtime.
*
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations
* by executing appropriate Redis commands; must not be {@literal null}.
@@ -161,13 +160,13 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
/**
* Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter}
* and a default {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime.
* and default {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime.
*
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations
* by executing appropriate Redis commands; must not be {@literal null}.
* @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches}
* by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}.
* @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime;
* @param allowRuntimeCacheCreation boolean specifying whether to allow creation of undeclared caches at runtime;
* {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}.
* @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration}
* are {@literal null}.
@@ -190,8 +189,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
* Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter}
* and a default {@link RedisCacheConfiguration}, along with an optional, initial set of {@link String cache names}
* used to create {@link RedisCache Redis caches} on startup.
*
* Allows cache creation at runtime.
* <p>
* Allows {@link RedisCache cache} creation at runtime.
*
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations
* by executing appropriate Redis commands; must not be {@literal null}.
@@ -213,15 +212,15 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
/**
* Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter}
* and default {@link RedisCacheConfiguration}, and whether to allow cache creation at runtime.
*
* Additionally, the optional, initial set of {@link String cache names} witll be used to create
* <p>
* Additionally, the optional, initial set of {@link String cache names} will be used to create
* {@link RedisCache Redis caches} on startup.
*
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations
* by executing appropriate Redis commands; must not be {@literal null}.
* @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches}
* by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}.
* @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime;
* @param allowRuntimeCacheCreation boolean specifying whether to allow creation of undeclared caches at runtime;
* {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}.
* @param initialCacheNames optional set of {@link String cache names} used to create {@link RedisCache Redis caches}
* on startup. The default {@link RedisCacheConfiguration} will be applied to each cache.
@@ -244,11 +243,11 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
/**
* Creates new {@link RedisCacheManager} using given {@link RedisCacheWriter} and default
* {@link RedisCacheConfiguration}.
*
* <p>
* Additionally, an initial {@link RedisCache} will be created and configured using the associated
* {@link RedisCacheConfiguration} for each {@link String named} {@link RedisCache} in the given {@link Map}.
*
* Allows cache creation at runtime.
* <p>
* Allows {@link RedisCache cache} creation at runtime.
*
* @param cacheWriter {@link RedisCacheWriter} used to perform {@link RedisCache} operations
* by executing appropriate Redis commands; must not be {@literal null}.
@@ -271,7 +270,7 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
/**
* Creates a new {@link RedisCacheManager} initialized with the given {@link RedisCacheWriter}
* and a default {@link RedisCacheConfiguration}, and whether to allow {@link RedisCache} creation at runtime.
*
* <p>
* Additionally, an initial {@link RedisCache} will be created and configured using the associated
* {@link RedisCacheConfiguration} for each {@link String named} {@link RedisCache} in the given {@link Map}.
*
@@ -279,11 +278,11 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
* by executing appropriate Redis commands; must not be {@literal null}.
* @param defaultCacheConfiguration {@link RedisCacheConfiguration} applied to new {@link RedisCache Redis caches}
* by default when no cache-specific {@link RedisCacheConfiguration} is provided; must not be {@literal null}.
* @param allowRuntimeCacheCreation boolean to allow creation of undeclared caches at runtime;
* @param allowRuntimeCacheCreation boolean specifying whether to allow creation of undeclared caches at runtime;
* {@literal true} by default. Maybe just use {@link RedisCacheConfiguration#defaultCacheConfig()}.
* @param initialCacheConfigurations {@link Map} of declared, known {@link String cache names} along with associated
* {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Redis caches} on startup;
* must not be {@literal null}.
* @param initialCacheConfigurations {@link Map} of declared, known {@link String cache names} along with
* the associated {@link RedisCacheConfiguration} used to create and configure {@link RedisCache Redis caches}
* on startup; must not be {@literal null}.
* @throws IllegalArgumentException if either the given {@link RedisCacheWriter} or {@link RedisCacheConfiguration}
* are {@literal null}.
* @see org.springframework.data.redis.cache.RedisCacheConfiguration
@@ -333,7 +332,8 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
getCacheNames().forEach(cacheName -> {
RedisCache cache = (RedisCache) lookupCache(cacheName);
cacheConfigurationMap.put(cacheName, cache != null ? cache.getCacheConfiguration() : null);
RedisCacheConfiguration cacheConfiguration = cache != null ? cache.getCacheConfiguration() : null;
cacheConfigurationMap.put(cacheName, cacheConfiguration);
});
return Collections.unmodifiableMap(cacheConfigurationMap);
@@ -381,7 +381,7 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
return getInitialCacheConfiguration().entrySet().stream()
.map(entry -> createRedisCache(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
.toList();
}
private RedisCacheConfiguration resolveCacheConfiguration(@Nullable RedisCacheConfiguration cacheConfiguration) {
@@ -462,10 +462,10 @@ public class RedisCacheManager extends AbstractTransactionSupportingCacheManager
}
/**
* Disable {@link RedisCache} creation at runtime for unconfigured, undeclared caches.
*
* {@link RedisCacheManager#getMissingCache(String)} returns {@literal null} for any unconfigured {@link Cache}
* instead of a new {@link RedisCache} instance. This allows the
* Disable {@link RedisCache} creation at runtime for non-configured, undeclared caches.
* <p>
* {@link RedisCacheManager#getMissingCache(String)} returns {@literal null} for any non-configured, undeclared
* {@link Cache} instead of a new {@link RedisCache} instance. This allows the
* {@link org.springframework.cache.support.CompositeCacheManager} to participate.
*
* @return this {@link RedisCacheManagerBuilder}.

View File

@@ -85,7 +85,9 @@ public interface RedisCacheWriter extends CacheStatisticsProvider {
*/
static RedisCacheWriter lockingRedisCacheWriter(RedisConnectionFactory connectionFactory,
BatchStrategy batchStrategy) {
return lockingRedisCacheWriter(connectionFactory, Duration.ofMillis(50), TtlFunction.persistent(), batchStrategy);
return lockingRedisCacheWriter(connectionFactory, Duration.ofMillis(50), TtlFunction.persistent(),
batchStrategy);
}
/**
@@ -104,8 +106,33 @@ public interface RedisCacheWriter extends CacheStatisticsProvider {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null");
return new DefaultRedisCacheWriter(connectionFactory, sleepTime, lockTtlFunction, CacheStatisticsCollector.none(),
batchStrategy);
return new DefaultRedisCacheWriter(connectionFactory, sleepTime, lockTtlFunction,
CacheStatisticsCollector.none(), batchStrategy);
}
/**
* Get the binary value representation from Redis stored for the given key.
*
* @param name must not be {@literal null}.
* @param key must not be {@literal null}.
* @return {@literal null} if key does not exist.
* @see #get(String, byte[], Duration)
*/
@Nullable
byte[] get(String name, byte[] key);
/**
* Get the binary value representation from Redis stored for the given key and set the given
* {@link Duration TTL expiration} for the cache entry.
*
* @param name must not be {@literal null}.
* @param key must not be {@literal null}.
* @param ttl {@link Duration} specifying the {@literal expiration timeout} for the cache entry.
* @return {@literal null} if key does not exist or has {@literal expired}.
*/
@Nullable
default byte[] get(String name, byte[] key, @Nullable Duration ttl) {
return get(name, key);
}
/**
@@ -118,16 +145,6 @@ public interface RedisCacheWriter extends CacheStatisticsProvider {
*/
void put(String name, byte[] key, byte[] value, @Nullable Duration ttl);
/**
* Get the binary value representation from Redis stored for the given key.
*
* @param name must not be {@literal null}.
* @param key must not be {@literal null}.
* @return {@literal null} if key does not exist.
*/
@Nullable
byte[] get(String name, byte[] key);
/**
* Write the given value to Redis if the key does not already exist.
*

View File

@@ -16,10 +16,12 @@
package org.springframework.data.redis.core.types;
import java.time.Duration;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* {@link Expiration} holds a {@link Long numeric value} with an associated {@link TimeUnit}.
@@ -219,6 +221,25 @@ public class Expiration {
return false;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Expiration that)) {
return false;
}
return this.getTimeUnit().toMillis(getExpirationTime()) == that.getTimeUnit().toMillis(that.getExpirationTime());
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHashCode(new Object[] { getExpirationTime(), getTimeUnit() });
}
/**
* @author Christoph Strobl
* @since 2.4