Refactor CacheStore

pull/137/head
johnniang 2019-03-28 14:21:42 +08:00
parent 8d82a03e90
commit 68b78b9267
6 changed files with 236 additions and 104 deletions

View File

@ -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);
}
}

View File

@ -25,15 +25,23 @@ public interface CacheStore<K, V> {
Optional<V> get(@NonNull K key); 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 key cache key must not be null
* @param value cache value 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 * @param timeUnit timeout unit
*/ */
void put(@NonNull K key, @NonNull V value, long timeout, @NonNull TimeUnit timeUnit); 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. * Delete a key.
* *

View File

@ -14,17 +14,12 @@ import java.util.Date;
@ToString @ToString
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @AllArgsConstructor
public class CacheWrapper<T> { public class CacheWrapper<V> {
/**
* Cache key.
*/
private String key;
/** /**
* Cache data * Cache data
*/ */
private T data; private V data;
/** /**
* Expired time. * Expired time.

View File

@ -1,31 +1,46 @@
package cc.ryanc.halo.cache; package cc.ryanc.halo.cache;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/** /**
* In-memory cache store. * In-memory cache store.
* *
* @author johnniang * @author johnniang
*/ */
@Slf4j
public class InMemoryCacheStore extends StringCacheStore { 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 @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)); return Optional.ofNullable(cacheContainer.get(key));
} }
@Override @Override
public void put(String key, String value, long timeout, TimeUnit timeUnit) { void putInternal(String key, CacheWrapper<String> cacheWrapper) {
cacheContainer.put(key, value); 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 @Override
public void delete(String key) { public void delete(String key) {
// TODO Consider to delete the cache periodic Assert.hasText(key, "Cache key must not be blank");
cacheContainer.remove(key); cacheContainer.remove(key);
} }
} }

View File

@ -1,17 +1,6 @@
package cc.ryanc.halo.cache; 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 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. * String cache store.
@ -19,83 +8,6 @@ import java.util.concurrent.TimeUnit;
* @author johnniang * @author johnniang
*/ */
@Slf4j @Slf4j
public abstract class StringCacheStore implements CacheStore<String, String> { public abstract class StringCacheStore extends AbstractCacheStore<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);
}
});
}
} }

View File

@ -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());
}
}