mirror of https://github.com/halo-dev/halo
Refactor CacheStore
parent
8d82a03e90
commit
68b78b9267
|
@ -0,0 +1,104 @@
|
|||
package cc.ryanc.halo.cache;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Abstract cache store.
|
||||
*
|
||||
* @author johnniang
|
||||
* @date 3/28/19
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractCacheStore<K, V> implements CacheStore<K, V> {
|
||||
|
||||
/**
|
||||
* Get cache wrapper by key.
|
||||
*
|
||||
* @param key key must not be null
|
||||
* @return an optional cache wrapper
|
||||
*/
|
||||
@NonNull
|
||||
abstract Optional<CacheWrapper<V>> getInternal(@NonNull K key);
|
||||
|
||||
/**
|
||||
* Puts the cache wrapper.
|
||||
*
|
||||
* @param key key must not be null
|
||||
* @param cacheWrapper cache wrapper must not be null
|
||||
*/
|
||||
abstract void putInternal(@NonNull K key, @NonNull CacheWrapper<V> cacheWrapper);
|
||||
|
||||
@Override
|
||||
public Optional<V> get(K key) {
|
||||
Assert.notNull(key, "Cache key must not be blank");
|
||||
|
||||
return getInternal(key).map(cacheWrapper -> {
|
||||
log.debug("Cache wrapper: [{}]", cacheWrapper);
|
||||
|
||||
// Check expiration
|
||||
if (cacheWrapper.getExpireAt() != null && cacheWrapper.getExpireAt().before(cc.ryanc.halo.utils.DateUtils.now())) {
|
||||
// Expired then delete it
|
||||
log.warn("Cache key: [{}] has been expired", key);
|
||||
|
||||
// Delete the key
|
||||
delete(key);
|
||||
|
||||
// Return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return cacheWrapper.getData();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(K key, V value, long timeout, TimeUnit timeUnit) {
|
||||
Assert.notNull(key, "Cache key must not be blank");
|
||||
Assert.notNull(value, "Cache value must not be null");
|
||||
Assert.isTrue(timeout > 0, "Cache expiration timeout must not be less than 1");
|
||||
Assert.notNull(timeUnit, "Time unit must not be null");
|
||||
|
||||
// Handle expiration
|
||||
Date now = cc.ryanc.halo.utils.DateUtils.now();
|
||||
|
||||
long millis = timeUnit.toMillis(timeout);
|
||||
if (millis <= 0) {
|
||||
millis = 1L;
|
||||
}
|
||||
|
||||
Date expireAt = DateUtils.addMilliseconds(now, Long.valueOf(millis).intValue());
|
||||
|
||||
// Build cache wrapper
|
||||
CacheWrapper<V> cacheWrapper = new CacheWrapper<>();
|
||||
cacheWrapper.setCreateAt(now);
|
||||
cacheWrapper.setExpireAt(expireAt);
|
||||
cacheWrapper.setData(value);
|
||||
|
||||
putInternal(key, cacheWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(K key, V value) {
|
||||
Assert.notNull(key, "Cache key must not be blank");
|
||||
Assert.notNull(value, "Cache value must not be null");
|
||||
|
||||
// Get current time
|
||||
Date now = cc.ryanc.halo.utils.DateUtils.now();
|
||||
|
||||
// Build cache wrapper
|
||||
CacheWrapper<V> cacheWrapper = new CacheWrapper<>();
|
||||
cacheWrapper.setCreateAt(now);
|
||||
cacheWrapper.setExpireAt(null);
|
||||
cacheWrapper.setData(value);
|
||||
|
||||
putInternal(key, cacheWrapper);
|
||||
|
||||
}
|
||||
}
|
|
@ -25,15 +25,23 @@ public interface CacheStore<K, V> {
|
|||
Optional<V> get(@NonNull K key);
|
||||
|
||||
/**
|
||||
* Puts a cache.
|
||||
* Puts a cache which will be expired.
|
||||
*
|
||||
* @param key cache key must not be null
|
||||
* @param value cache value must not be null
|
||||
* @param timeout the key expiration must not be less than 0
|
||||
* @param timeout the key expiration must not be less than 1
|
||||
* @param timeUnit timeout unit
|
||||
*/
|
||||
void put(@NonNull K key, @NonNull V value, long timeout, @NonNull TimeUnit timeUnit);
|
||||
|
||||
/**
|
||||
* Puts a non-expired cache.
|
||||
*
|
||||
* @param key cache key must not be null
|
||||
* @param value cache value must not be null
|
||||
*/
|
||||
void put(@NonNull K key, @NonNull V value);
|
||||
|
||||
/**
|
||||
* Delete a key.
|
||||
*
|
||||
|
|
|
@ -14,17 +14,12 @@ import java.util.Date;
|
|||
@ToString
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class CacheWrapper<T> {
|
||||
|
||||
/**
|
||||
* Cache key.
|
||||
*/
|
||||
private String key;
|
||||
public class CacheWrapper<V> {
|
||||
|
||||
/**
|
||||
* Cache data
|
||||
*/
|
||||
private T data;
|
||||
private V data;
|
||||
|
||||
/**
|
||||
* Expired time.
|
||||
|
|
|
@ -1,31 +1,46 @@
|
|||
package cc.ryanc.halo.cache;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* In-memory cache store.
|
||||
*
|
||||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public class InMemoryCacheStore extends StringCacheStore {
|
||||
|
||||
private final static ConcurrentHashMap<String, String> cacheContainer = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* Cache container.
|
||||
*/
|
||||
private final static ConcurrentHashMap<String, CacheWrapper<String>> cacheContainer = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public Optional<String> get(String key) {
|
||||
Optional<CacheWrapper<String>> getInternal(String key) {
|
||||
Assert.hasText(key, "Cache key must not be blank");
|
||||
|
||||
return Optional.ofNullable(cacheContainer.get(key));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(String key, String value, long timeout, TimeUnit timeUnit) {
|
||||
cacheContainer.put(key, value);
|
||||
void putInternal(String key, CacheWrapper<String> cacheWrapper) {
|
||||
Assert.hasText(key, "Cache key must not be blank");
|
||||
Assert.notNull(cacheWrapper, "Cache wrapper must not be null");
|
||||
|
||||
// Put the cache wrapper
|
||||
CacheWrapper<String> putCacheWrapper = cacheContainer.put(key, cacheWrapper);
|
||||
|
||||
log.debug("Put cache wrapper: [{}]", putCacheWrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String key) {
|
||||
// TODO Consider to delete the cache periodic
|
||||
Assert.hasText(key, "Cache key must not be blank");
|
||||
|
||||
cacheContainer.remove(key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +1,6 @@
|
|||
package cc.ryanc.halo.cache;
|
||||
|
||||
import cc.ryanc.halo.exception.ServiceException;
|
||||
import cc.ryanc.halo.utils.JsonUtils;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.time.DateUtils;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* String cache store.
|
||||
|
@ -19,83 +8,6 @@ import java.util.concurrent.TimeUnit;
|
|||
* @author johnniang
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class StringCacheStore implements CacheStore<String, String> {
|
||||
|
||||
public <T> void putForString(String key, T value, long timeout, TimeUnit timeUnit) {
|
||||
Assert.hasText(key, "Cache key must not be blank");
|
||||
Assert.notNull(value, "Cache value must not be null");
|
||||
Assert.isTrue(timeout > 0, "Timeout must not be less than 0");
|
||||
Assert.notNull(timeUnit, "Time unit must not be null");
|
||||
|
||||
// Convert to second
|
||||
Long seconds = timeUnit.toSeconds(timeout);
|
||||
|
||||
// Round the seconds
|
||||
if (seconds == 0) {
|
||||
seconds = 1L;
|
||||
}
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
// Calculate expire at
|
||||
Date expireAt = DateUtils.addSeconds(now, seconds.intValue());
|
||||
|
||||
// Build cache wrapper
|
||||
CacheWrapper<T> wrapper = new CacheWrapper<>();
|
||||
wrapper.setCreateAt(now);
|
||||
wrapper.setExpireAt(expireAt);
|
||||
wrapper.setKey(key);
|
||||
wrapper.setData(value);
|
||||
|
||||
try {
|
||||
// Convert wrapper to json
|
||||
String valueJson = JsonUtils.objectToJson(wrapper);
|
||||
// Put the the value json to cache store
|
||||
put(key, valueJson, timeout, timeUnit);
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new ServiceException("Failed to convert object to json", e).setErrorData(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@NonNull
|
||||
public <T> Optional<T> getForString(@NonNull String key, @NonNull Class<T> type) {
|
||||
Assert.hasText(key, "Cache key must not be blank");
|
||||
Assert.notNull(type, "Cache type must not be null");
|
||||
|
||||
return get(key).map(value -> {
|
||||
try {
|
||||
CacheWrapper<?> cacheWrapper = JsonUtils.jsonToObject(value, CacheWrapper.class);
|
||||
|
||||
if (cacheWrapper == null) {
|
||||
log.error("Cache wrapper is null, key: [{}]", key);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.debug("Cache wrapper: [{}]", cacheWrapper);
|
||||
|
||||
Date now = new Date();
|
||||
|
||||
if (cacheWrapper.getExpireAt().before(now)) {
|
||||
// Expired then delete it
|
||||
log.debug("Cache key: [{}] has been expired", key);
|
||||
|
||||
delete(key);
|
||||
return null;
|
||||
}
|
||||
|
||||
Object data = cacheWrapper.getData();
|
||||
|
||||
if (data != null && data.getClass().isAssignableFrom(type)) {
|
||||
return (T) data;
|
||||
}
|
||||
|
||||
log.error("Data type: [{}], but specified type: [{}]", data == null ? null : data.getClass(), type);
|
||||
throw new ServiceException("Cache value type is mismatched with the specified type");
|
||||
} catch (IOException e) {
|
||||
throw new ServiceException("Failed to convert from json to object", e).setErrorData(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
public abstract class StringCacheStore extends AbstractCacheStore<String, String> {
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
package cc.ryanc.halo.cache;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* InMemoryCacheStoreTest.
|
||||
*
|
||||
* @author johnniang
|
||||
* @date 3/28/19
|
||||
*/
|
||||
public class InMemoryCacheStoreTest {
|
||||
|
||||
private InMemoryCacheStore cacheStore;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
cacheStore = new InMemoryCacheStore();
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void putNullValueTest() {
|
||||
String key = "test_key";
|
||||
|
||||
cacheStore.put(key, null);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void putNullKeyTest() {
|
||||
String value = "test_value";
|
||||
|
||||
cacheStore.put(null, value);
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
public void getByNullKeyTest() {
|
||||
cacheStore.get(null);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void getNullTest() {
|
||||
String key = "test_key";
|
||||
|
||||
Optional<String> valueOptional = cacheStore.get(key);
|
||||
|
||||
assertFalse(valueOptional.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expirationTest() throws InterruptedException {
|
||||
String key = "test_key";
|
||||
String value = "test_value";
|
||||
cacheStore.put(key, value, 500, TimeUnit.MILLISECONDS);
|
||||
|
||||
Optional<String> valueOptional = cacheStore.get(key);
|
||||
|
||||
assertTrue(valueOptional.isPresent());
|
||||
assertThat(valueOptional.get(), equalTo(value));
|
||||
|
||||
TimeUnit.SECONDS.sleep(1L);
|
||||
|
||||
valueOptional = cacheStore.get(key);
|
||||
|
||||
assertFalse(valueOptional.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deleteTest() {
|
||||
String key = "test_key";
|
||||
String value = "test_value";
|
||||
|
||||
// Put the cache
|
||||
cacheStore.put(key, value);
|
||||
|
||||
// Get the caceh
|
||||
Optional<String> valueOptional = cacheStore.get(key);
|
||||
|
||||
// Assert
|
||||
assertTrue(valueOptional.isPresent());
|
||||
assertThat(valueOptional.get(), equalTo(value));
|
||||
|
||||
// Delete the cache
|
||||
cacheStore.delete(key);
|
||||
|
||||
// Get the cache again
|
||||
valueOptional = cacheStore.get(key);
|
||||
|
||||
// Assertion
|
||||
assertFalse(valueOptional.isPresent());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue