From 746137614167a0f408a89ec82856ec2733de9f9f Mon Sep 17 00:00:00 2001 From: LamGC Date: Thu, 3 Sep 2020 19:27:08 +0800 Subject: [PATCH] =?UTF-8?q?[Add]=20=E6=B7=BB=E5=8A=A0=E5=9F=BA=E4=BA=8E=20?= =?UTF-8?q?Hashtable=20=E7=9A=84=E6=9C=AC=E5=9C=B0=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E5=AE=B9=E5=99=A8;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Add] HashCacheStore 添加 CacheStore 的抽象实现; [Add] HashSingleCacheStore 添加 SingleCacheStore 的具体实现; [Add] HashSingleCacheStoreTest 增加完整测试项(涵盖了 HashCacheStore); --- .../cgj/bot/cache/local/HashCacheStore.java | 145 ++++++++++++++++++ .../bot/cache/local/HashSingleCacheStore.java | 53 +++++++ .../cache/local/HashSingleCacheStoreTest.java | 135 ++++++++++++++++ 3 files changed, 333 insertions(+) create mode 100644 ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/HashCacheStore.java create mode 100644 ContentGrabbingJi-CacheStore-local/src/main/java/net/lamgc/cgj/bot/cache/local/HashSingleCacheStore.java create mode 100644 ContentGrabbingJi-CacheStore-local/src/test/java/net/lamgc/cgj/bot/cache/local/HashSingleCacheStoreTest.java 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())); + } + +}