diff --git a/pom.xml b/pom.xml
index f5f5fdda2..42152d9e7 100755
--- a/pom.xml
+++ b/pom.xml
@@ -48,6 +48,7 @@
7.2.18
0.4.8
0.12.1
+ 3.8.1
@@ -213,6 +214,13 @@
2.9.2
+
+
+ org.apache.commons
+ commons-lang3
+ ${commons-lang3.version}
+
+
diff --git a/src/main/java/cc/ryanc/halo/cache/CacheStore.java b/src/main/java/cc/ryanc/halo/cache/CacheStore.java
new file mode 100644
index 000000000..264b56f02
--- /dev/null
+++ b/src/main/java/cc/ryanc/halo/cache/CacheStore.java
@@ -0,0 +1,44 @@
+package cc.ryanc.halo.cache;
+
+import org.springframework.lang.NonNull;
+
+import java.util.Optional;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Cache store interface.
+ *
+ * @param cache key type
+ * @param cache value type
+ * @author johnniang
+ * *
+ */
+public interface CacheStore {
+
+ /**
+ * Gets by cache key.
+ *
+ * @param key must not be null
+ * @return cache value
+ */
+ @NonNull
+ Optional get(@NonNull K key);
+
+ /**
+ * Puts a cache.
+ *
+ * @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 timeUnit timeout unit
+ */
+ void put(@NonNull K key, @NonNull V value, long timeout, @NonNull TimeUnit timeUnit);
+
+ /**
+ * Delete a key.
+ *
+ * @param key cache key must not be null
+ */
+ void delete(@NonNull K key);
+
+}
diff --git a/src/main/java/cc/ryanc/halo/cache/CacheWrapper.java b/src/main/java/cc/ryanc/halo/cache/CacheWrapper.java
new file mode 100644
index 000000000..4e6de7066
--- /dev/null
+++ b/src/main/java/cc/ryanc/halo/cache/CacheWrapper.java
@@ -0,0 +1,38 @@
+package cc.ryanc.halo.cache;
+
+import lombok.*;
+
+import java.util.Date;
+
+/**
+ * Cache wrapper.
+ *
+ * @author johnniang
+ */
+@Data
+@EqualsAndHashCode
+@ToString
+@NoArgsConstructor
+@AllArgsConstructor
+public class CacheWrapper {
+
+ /**
+ * Cache key.
+ */
+ private String key;
+
+ /**
+ * Cache data
+ */
+ private T data;
+
+ /**
+ * Expired time.
+ */
+ private Date expireAt;
+
+ /**
+ * Create time.
+ */
+ private Date createAt;
+}
diff --git a/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java b/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java
new file mode 100644
index 000000000..aae592fbd
--- /dev/null
+++ b/src/main/java/cc/ryanc/halo/cache/InMemoryCacheStore.java
@@ -0,0 +1,31 @@
+package cc.ryanc.halo.cache;
+
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * In-memory cache store.
+ *
+ * @author johnniang
+ */
+public class InMemoryCacheStore extends StringCacheStore {
+
+ private final static ConcurrentHashMap cacheContainer = new ConcurrentHashMap<>();
+
+ @Override
+ public Optional get(String key) {
+ return Optional.ofNullable(cacheContainer.get(key));
+ }
+
+ @Override
+ public void put(String key, String value, long timeout, TimeUnit timeUnit) {
+ cacheContainer.put(key, value);
+ }
+
+ @Override
+ public void delete(String key) {
+ // TODO Consider to delete the cache periodic
+ 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
new file mode 100644
index 000000000..196bffca5
--- /dev/null
+++ b/src/main/java/cc/ryanc/halo/cache/StringCacheStore.java
@@ -0,0 +1,101 @@
+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;
+
+/**
+ * Wechat cache store.
+ *
+ * @author johnniang
+ */
+@Slf4j
+public abstract class StringCacheStore implements CacheStore {
+
+ public void putForWechat(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 getForWechat(@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);
+ }
+ });
+ }
+
+}
diff --git a/src/main/java/cc/ryanc/halo/config/HaloConfiguration.java b/src/main/java/cc/ryanc/halo/config/HaloConfiguration.java
index 541c88331..6e8a52e44 100644
--- a/src/main/java/cc/ryanc/halo/config/HaloConfiguration.java
+++ b/src/main/java/cc/ryanc/halo/config/HaloConfiguration.java
@@ -1,5 +1,7 @@
package cc.ryanc.halo.config;
+import cc.ryanc.halo.cache.InMemoryCacheStore;
+import cc.ryanc.halo.cache.StringCacheStore;
import cc.ryanc.halo.config.properties.HaloProperties;
import cc.ryanc.halo.filter.CorsFilter;
import cc.ryanc.halo.filter.LogFilter;
@@ -8,6 +10,7 @@ import cc.ryanc.halo.security.filter.ApiAuthenticationFilter;
import cc.ryanc.halo.security.handler.AdminAuthenticationFailureHandler;
import cc.ryanc.halo.security.handler.DefaultAuthenticationFailureHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
@@ -23,6 +26,12 @@ import org.springframework.core.Ordered;
@EnableConfigurationProperties(HaloProperties.class)
public class HaloConfiguration {
+ @Bean
+ @ConditionalOnMissingBean
+ public StringCacheStore stringCacheStore() {
+ return new InMemoryCacheStore();
+ }
+
/**
* Creates a CorsFilter.
*