[Add] 添加基于 Hashtable 的本地缓存存储容器;

[Add] HashCacheStore 添加 CacheStore 的抽象实现;
[Add] HashSingleCacheStore 添加 SingleCacheStore 的具体实现;
[Add] HashSingleCacheStoreTest 增加完整测试项(涵盖了 HashCacheStore);
This commit is contained in:
LamGC 2020-09-03 19:27:08 +08:00
parent 9f08ca0eba
commit 7461376141
Signed by: LamGC
GPG Key ID: 6C5AE2A913941E1D
3 changed files with 333 additions and 0 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package net.lamgc.cgj.bot.cache.local;
import net.lamgc.cgj.bot.cache.CacheStore;
import java.util.*;
/**
* 基于 {@link Hashtable} 的缓存存储容器.
* @param <V> 值类型.
* @author LamGC
* @see net.lamgc.cgj.bot.cache.CacheStore
* @see Hashtable
*/
public abstract class HashCacheStore<V> implements CacheStore<V> {
private final Map<String, CacheItem<V>> cacheMap = new Hashtable<>();
/**
* 获取内部 Map 对象.
* 仅供其他子类使用.
* @return 返回存储缓存项的 Map.
*/
protected Map<String, CacheItem<V>> getCacheMap() {
return cacheMap;
}
@Override
public boolean setTimeToLive(String key, long ttl) {
if (!exists(key)) {
return false;
}
CacheItem<V> 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<V> 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<V> 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<String> keySet() {
return Collections.unmodifiableSet(cacheMap.keySet());
}
/**
* 缓存项.
* @author LamGC
*/
protected final static class CacheItem<V> {
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));
}
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
package net.lamgc.cgj.bot.cache.local;
import net.lamgc.cgj.bot.cache.SingleCacheStore;
import java.util.Objects;
/**
* 基于 {@link java.util.Hashtable} Map 缓存存储容器.
* @param <V> 值类型.
* @author LamGC
*/
public class HashSingleCacheStore<V> extends HashCacheStore<V> implements SingleCacheStore<V> {
@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();
}
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<String> 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<String> 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<String> 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<String> 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<String> 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<String> 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<String, String> 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<String> cacheStore = new HashSingleCacheStore<>();
expectedMap.forEach(cacheStore::set);
Assert.assertEquals(expectedMap.size(), cacheStore.size());
Assert.assertTrue(expectedMap.keySet().containsAll(cacheStore.keySet()));
}
}