From 68b78b9267f6a0d8d03ec9560bf928d30779c9c4 Mon Sep 17 00:00:00 2001 From: johnniang Date: Thu, 28 Mar 2019 14:21:42 +0800 Subject: [PATCH] Refactor CacheStore --- .../ryanc/halo/cache/AbstractCacheStore.java | 104 ++++++++++++++++++ .../java/cc/ryanc/halo/cache/CacheStore.java | 12 +- .../cc/ryanc/halo/cache/CacheWrapper.java | 9 +- .../ryanc/halo/cache/InMemoryCacheStore.java | 27 ++++- .../cc/ryanc/halo/cache/StringCacheStore.java | 90 +-------------- .../halo/cache/InMemoryCacheStoreTest.java | 98 +++++++++++++++++ 6 files changed, 236 insertions(+), 104 deletions(-) create mode 100644 src/main/java/cc/ryanc/halo/cache/AbstractCacheStore.java create mode 100644 src/test/java/cc/ryanc/halo/cache/InMemoryCacheStoreTest.java diff --git a/src/main/java/cc/ryanc/halo/cache/AbstractCacheStore.java b/src/main/java/cc/ryanc/halo/cache/AbstractCacheStore.java new file mode 100644 index 000000000..fe0205028 --- /dev/null +++ b/src/main/java/cc/ryanc/halo/cache/AbstractCacheStore.java @@ -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 implements CacheStore { + + /** + * Get cache wrapper by key. + * + * @param key key must not be null + * @return an optional cache wrapper + */ + @NonNull + abstract Optional> 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 cacheWrapper); + + @Override + public Optional 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 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 cacheWrapper = new CacheWrapper<>(); + cacheWrapper.setCreateAt(now); + cacheWrapper.setExpireAt(null); + cacheWrapper.setData(value); + + putInternal(key, cacheWrapper); + + } +} diff --git a/src/main/java/cc/ryanc/halo/cache/CacheStore.java b/src/main/java/cc/ryanc/halo/cache/CacheStore.java index 264b56f02..ecdd1bd39 100644 --- a/src/main/java/cc/ryanc/halo/cache/CacheStore.java +++ b/src/main/java/cc/ryanc/halo/cache/CacheStore.java @@ -25,15 +25,23 @@ public interface CacheStore { Optional 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. * diff --git a/src/main/java/cc/ryanc/halo/cache/CacheWrapper.java b/src/main/java/cc/ryanc/halo/cache/CacheWrapper.java index 4e6de7066..a7069a8e8 100644 --- a/src/main/java/cc/ryanc/halo/cache/CacheWrapper.java +++ b/src/main/java/cc/ryanc/halo/cache/CacheWrapper.java @@ -14,17 +14,12 @@ import java.util.Date; @ToString @NoArgsConstructor @AllArgsConstructor -public class CacheWrapper { - - /** - * Cache key. - */ - private String key; +public class CacheWrapper { /** * Cache data */ - private T data; + private V data; /** * Expired time. diff --git a/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java b/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java index aae592fbd..11f7dbc8e 100644 --- a/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java +++ b/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java @@ -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 cacheContainer = new ConcurrentHashMap<>(); + /** + * Cache container. + */ + private final static ConcurrentHashMap> cacheContainer = new ConcurrentHashMap<>(); @Override - public Optional get(String key) { + Optional> 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 cacheWrapper) { + Assert.hasText(key, "Cache key must not be blank"); + Assert.notNull(cacheWrapper, "Cache wrapper must not be null"); + + // Put the cache wrapper + CacheWrapper 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); } } diff --git a/src/main/java/cc/ryanc/halo/cache/StringCacheStore.java b/src/main/java/cc/ryanc/halo/cache/StringCacheStore.java index 636fd3a88..fc2f5d178 100644 --- a/src/main/java/cc/ryanc/halo/cache/StringCacheStore.java +++ b/src/main/java/cc/ryanc/halo/cache/StringCacheStore.java @@ -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 { - - public 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 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 Optional getForString(@NonNull String key, @NonNull Class 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 { } diff --git a/src/test/java/cc/ryanc/halo/cache/InMemoryCacheStoreTest.java b/src/test/java/cc/ryanc/halo/cache/InMemoryCacheStoreTest.java new file mode 100644 index 000000000..e1f7900ac --- /dev/null +++ b/src/test/java/cc/ryanc/halo/cache/InMemoryCacheStoreTest.java @@ -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 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 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 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()); + } +}