diff --git a/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/HashCacheStore.java b/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/HashCacheStore.java
new file mode 100644
index 0000000..f92b65a
--- /dev/null
+++ b/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/HashCacheStore.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2020 LamGC
+ *
+ * ContentGrabbingJi is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * ContentGrabbingJi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package net.lamgc.cgj.bot.cache.local;
+
+import net.lamgc.cgj.bot.cache.CacheStore;
+
+import java.util.*;
+
+/**
+ * 基于 {@link Hashtable} 的缓存存储容器.
+ * @param 值类型.
+ * @author LamGC
+ * @see net.lamgc.cgj.bot.cache.CacheStore
+ * @see Hashtable
+ */
+public abstract class HashCacheStore implements CacheStore {
+
+ private final Map> cacheMap = new Hashtable<>();
+
+ /**
+ * 获取内部 Map 对象.
+ * 仅供其他子类使用.
+ * @return 返回存储缓存项的 Map.
+ */
+ protected Map> getCacheMap() {
+ return cacheMap;
+ }
+
+ @Override
+ public boolean setTimeToLive(String key, long ttl) {
+ if (!exists(key)) {
+ return false;
+ }
+ CacheItem item = cacheMap.get(key);
+ item.setExpireDate(ttl < 0 ? null : new Date(System.currentTimeMillis() + ttl));
+ return true;
+ }
+
+ @Override
+ public long getTimeToLive(String key) {
+ if (!exists(key)) {
+ return -1;
+ }
+ CacheItem item = cacheMap.get(key);
+ Date expireDate = item.getExpireDate();
+ if (expireDate != null) {
+ return expireDate.getTime() - System.currentTimeMillis();
+ }
+ return -1;
+ }
+
+ @Override
+ public long size() {
+ return cacheMap.size();
+ }
+
+ @Override
+ public boolean clear() {
+ cacheMap.clear();
+ return true;
+ }
+
+ @Override
+ public boolean exists(String key) {
+ if (!cacheMap.containsKey(key)) {
+ return false;
+ }
+ CacheItem item = cacheMap.get(key);
+ // 在检查其过期情况后根据情况进行清理, 减轻主动清理机制的负担.
+ if (item.isExpire(new Date())) {
+ remove(key);
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean remove(String key) {
+ // 根据 Collection 说明, 删除时 key 存在映射就会返回, 只要返回 null 就代表没有.
+ return cacheMap.remove(key) != null;
+ }
+
+ @Override
+ public Set keySet() {
+ return Collections.unmodifiableSet(cacheMap.keySet());
+ }
+
+ /**
+ * 缓存项.
+ * @author LamGC
+ */
+ protected final static class CacheItem {
+ private final V value;
+ private Date expireDate;
+
+ CacheItem(V value) {
+ this(value, null);
+ }
+
+ CacheItem(V value, Date expireDate) {
+ this.value = value;
+ this.expireDate = expireDate;
+ }
+
+ public V getValue() {
+ return value;
+ }
+
+ public void setExpireDate(Date expireDate) {
+ this.expireDate = expireDate;
+ }
+
+ public Date getExpireDate() {
+ return expireDate;
+ }
+
+ /**
+ * 检查缓存项是否过期.
+ * @param date 当前时间.
+ * @return 如果已设置过期时间且早于提供的Date, 则该缓存项过期, 返回 true.
+ * @throws NullPointerException 当 date 传入 null 时抛出.
+ */
+ public boolean isExpire(Date date) {
+ Date expireDate = getExpireDate();
+ return expireDate != null && expireDate.before(Objects.requireNonNull(date));
+ }
+
+ }
+
+}
diff --git a/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/HashSingleCacheStore.java b/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/HashSingleCacheStore.java
new file mode 100644
index 0000000..cfc5875
--- /dev/null
+++ b/ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/HashSingleCacheStore.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2020 LamGC
+ *
+ * ContentGrabbingJi is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * ContentGrabbingJi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package net.lamgc.cgj.bot.cache.local;
+
+import net.lamgc.cgj.bot.cache.SingleCacheStore;
+
+import java.util.Objects;
+
+/**
+ * 基于 {@link java.util.Hashtable} 的 Map 缓存存储容器.
+ * @param 值类型.
+ * @author LamGC
+ */
+public class HashSingleCacheStore extends HashCacheStore implements SingleCacheStore {
+
+ @Override
+ public boolean set(String key, V value) {
+ getCacheMap().put(Objects.requireNonNull(key), new CacheItem<>(Objects.requireNonNull(value)));
+ return true;
+ }
+
+ @Override
+ public boolean setIfNotExist(String key, V value) {
+ if (exists(key)) {
+ return false;
+ }
+ return set(key, value);
+ }
+
+ @Override
+ public V get(String key) {
+ if (!exists(key)) {
+ return null;
+ }
+ return getCacheMap().get(key).getValue();
+ }
+
+}
diff --git a/ContentGrabbingJi-CacheStore-local/src/test/java/net/lamgc/cgj/bot/cache/local/HashSingleCacheStoreTest.java b/ContentGrabbingJi-CacheStore-local/src/test/java/net/lamgc/cgj/bot/cache/local/HashSingleCacheStoreTest.java
new file mode 100644
index 0000000..18ad180
--- /dev/null
+++ b/ContentGrabbingJi-CacheStore-local/src/test/java/net/lamgc/cgj/bot/cache/local/HashSingleCacheStoreTest.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2020 LamGC
+ *
+ * ContentGrabbingJi is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * ContentGrabbingJi is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package net.lamgc.cgj.bot.cache.local;
+
+import net.lamgc.cgj.bot.cache.SingleCacheStore;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @see HashSingleCacheStore
+ * @see HashCacheStore
+ */
+public class HashSingleCacheStoreTest {
+
+ @Test
+ public void nullThrowTest() {
+ final SingleCacheStore cacheStore = new HashSingleCacheStore<>();
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.set(null, "testValue"));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.set("testKey", null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.get(null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.setIfNotExist(null, "testValue"));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.setIfNotExist("testKey", null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.exists(null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.getTimeToLive(null));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.setTimeToLive(null, 0));
+ Assert.assertThrows(NullPointerException.class, () -> cacheStore.remove(null));
+ }
+
+ @Test
+ public void setAndGetTest() {
+ SingleCacheStore cacheStore = new HashSingleCacheStore<>();
+ final String key = "test01";
+ final String value = "testValue";
+ Assert.assertTrue("Set operation failed!", cacheStore.set(key, value));
+ Assert.assertEquals(value, cacheStore.get("test01"));
+ Assert.assertTrue("Remove operation failed!", cacheStore.remove(key));
+ Assert.assertNull("Set operation failed!", cacheStore.get(key));
+ }
+
+ @Test
+ public void setIfNotExistTest() {
+ SingleCacheStore cacheStore = new HashSingleCacheStore<>();
+ final String key = "test01";
+ final String value = "testValue";
+ final String value2 = "testValue02";
+ Assert.assertTrue("Set operation failed!", cacheStore.set(key, value));
+ Assert.assertFalse(cacheStore.setIfNotExist(key, value2));
+ Assert.assertEquals(value, cacheStore.get(key));
+ }
+
+ @Test
+ public void expireTest() throws InterruptedException {
+ final SingleCacheStore cacheStore = new HashSingleCacheStore<>();
+ final String key = "test01";
+ final String value = "testValue";
+
+ // Cache
+ Assert.assertFalse(cacheStore.setTimeToLive(key, 300));
+ Assert.assertEquals(-1, cacheStore.getTimeToLive(key));
+
+ // TTL 到期被动检查测试: 使用 exists 经 expire 检查失败后返回 false.
+ Assert.assertTrue("Set operation failed!", cacheStore.set(key, value));
+ Assert.assertTrue("SetTTL operation failed!", cacheStore.setTimeToLive(key, 200));
+ Assert.assertNotEquals(-1, cacheStore.getTimeToLive(key));
+ Thread.sleep(300);
+ Assert.assertFalse(cacheStore.exists(key));
+
+ // 取消 TTL 测试
+ Assert.assertTrue("Set operation failed!", cacheStore.set(key, value));
+ Assert.assertTrue("SetTTL operation failed!", cacheStore.setTimeToLive(key, 200));
+ Assert.assertTrue("SetTTL operation failed!", cacheStore.setTimeToLive(key, -1));
+ Thread.sleep(300);
+ Assert.assertTrue(cacheStore.exists(key));
+ Assert.assertEquals(-1, cacheStore.getTimeToLive(key));
+ }
+
+ @Test
+ public void removeTest() {
+ final SingleCacheStore cacheStore = new HashSingleCacheStore<>();
+ final String key = "test01";
+ final String value = "testValue";
+
+ // 删除不存在Cache测试
+ Assert.assertFalse(cacheStore.remove(key));
+ // 删除存在的Cache测试
+ Assert.assertTrue("Set operation failed!", cacheStore.set(key, value));
+ Assert.assertTrue(cacheStore.remove(key));
+ }
+
+ @Test
+ public void clearTest() {
+ final SingleCacheStore cacheStore = new HashSingleCacheStore<>();
+ final String key = "test01";
+ final String value = "testValue";
+ Assert.assertTrue("Set operation failed!", cacheStore.set(key, value));
+ Assert.assertTrue("before-exists operation failed!", cacheStore.exists(key));
+ Assert.assertTrue("Clear operation failed!", cacheStore.clear());
+ Assert.assertFalse("after-exists operation failed!", cacheStore.exists(key));
+ }
+
+ @Test
+ public void sizeAndKeySetTest() {
+ Map expectedMap = new HashMap<>();
+ expectedMap.put("test01", "testValue01");
+ expectedMap.put("test02", "testValue02");
+ expectedMap.put("test03", "testValue03");
+ expectedMap.put("test04", "testValue04");
+ expectedMap.put("test05", "testValue05");
+ expectedMap.put("test06", "testValue06");
+
+ final SingleCacheStore cacheStore = new HashSingleCacheStore<>();
+ expectedMap.forEach(cacheStore::set);
+ Assert.assertEquals(expectedMap.size(), cacheStore.size());
+ Assert.assertTrue(expectedMap.keySet().containsAll(cacheStore.keySet()));
+ }
+
+}