diff --git a/ContentGrabbingJi-CacheStore-redis/pom.xml b/ContentGrabbingJi-CacheStore-redis/pom.xml index 1fe609c..db92c33 100644 --- a/ContentGrabbingJi-CacheStore-redis/pom.xml +++ b/ContentGrabbingJi-CacheStore-redis/pom.xml @@ -38,7 +38,7 @@ redis.clients jedis - 3.2.0 + 3.3.0 diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStore.java b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStore.java new file mode 100644 index 0000000..1ded35b --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStore.java @@ -0,0 +1,102 @@ +/* + * 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.redis; + +import net.lamgc.cgj.bot.cache.CacheKey; +import net.lamgc.cgj.bot.cache.CacheStore; +import redis.clients.jedis.Jedis; + +import java.util.HashSet; +import java.util.Set; + +/** + * + * @author LamGC + */ +public abstract class RedisCacheStore implements CacheStore { + + /** + * 获取 Key 前缀. + *

key = getKeyPrefix() + key + * @param cacheKey CacheKey 对象. + * @return 返回 Key 前缀. + */ + protected String getKeyString(CacheKey cacheKey) { + return getKeyPrefix() + cacheKey.join(RedisUtils.KEY_SEPARATOR); + } + + /** + * 获取 Key 的完整前缀. + * @return 返回完整前缀. + */ + protected abstract String getKeyPrefix(); + + @Override + public boolean setTimeToLive(CacheKey key, long ttl) { + String keyString = getKeyString(key); + return RedisConnectionPool.executeRedis(jedis -> { + Long result; + if (ttl >= 0) { + result = jedis.pexpire(keyString, ttl); + } else { + result = jedis.persist(keyString); + } + return result == RedisUtils.RETURN_CODE_OK; + }); + } + + @Override + public long getTimeToLive(CacheKey key) { + return RedisConnectionPool.executeRedis(jedis -> { + Long ttl = jedis.pttl(getKeyString(key)); + return ttl < 0 ? -1 : ttl; + }); + } + + @Override + public long size() { + return RedisConnectionPool.executeRedis(Jedis::dbSize); + } + + @Override + public boolean clear() { + return RedisConnectionPool.executeRedis(jedis -> RedisUtils.isOk(jedis.flushDB())); + } + + @Override + public boolean exists(CacheKey key) { + return RedisConnectionPool.executeRedis(jedis -> jedis.exists(getKeyString(key))); + } + + @Override + public boolean remove(CacheKey key) { + return RedisConnectionPool.executeRedis(jedis -> jedis.del(getKeyString(key)) == RedisUtils.RETURN_CODE_OK); + } + + @Override + public Set keySet() { + Set keys = RedisConnectionPool.executeRedis(jedis -> jedis.keys(RedisUtils.KEY_PATTERN_ALL)); + final int prefixLength = getKeyPrefix().length(); + Set newKeys = new HashSet<>(); + for (String key : keys) { + newKeys.add(key.substring(prefixLength)); + } + return newKeys; + } + +} diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStoreFactory.java b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStoreFactory.java new file mode 100644 index 0000000..79df6b2 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisCacheStoreFactory.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.redis; + +import net.lamgc.cgj.bot.cache.*; +import net.lamgc.cgj.bot.cache.convert.StringConverter; + +/** + * + * @author LamGC + */ +@Factory(name = "Redis") +public class RedisCacheStoreFactory implements CacheStoreFactory { + @Override + public SingleCacheStore newSingleCacheStore(String identify, StringConverter converter) { + return new RedisSingleCacheStore<>(identify, converter); + } + + @Override + public ListCacheStore newListCacheStore(String identify, StringConverter converter) { + return null; + } + + @Override + public SetCacheStore newSetCacheStore(String identify, StringConverter converter) { + return null; + } + + @Override + public MapCacheStore newMapCacheStore(String identify, StringConverter converter) { + return new RedisMapCacheStore<>(identify, converter); + } + + @Override + public boolean canGetCacheStore() { + return RedisConnectionPool.available(); + } +} diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisConnectionPool.java b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisConnectionPool.java new file mode 100644 index 0000000..63c5aa3 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisConnectionPool.java @@ -0,0 +1,103 @@ +/* + * 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.redis; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Function; + +/** + * 统一的 Redis 连接池. + * @author LamGC + */ +public class RedisConnectionPool { + + private final static Logger log = LoggerFactory.getLogger(RedisConnectionPool.class); + private final static AtomicReference POOL = new AtomicReference<>(); + + public static synchronized void reconnectRedis() { + JedisPool jedisPool = POOL.get(); + if (jedisPool != null && !jedisPool.isClosed()) { + return; + } + JedisPoolConfig config = new JedisPoolConfig(); + config.setTestOnBorrow(true); + config.setTestOnReturn(true); + jedisPool = new JedisPool(config); + POOL.set(jedisPool); + } + + /** + * 获取一个 Jedis 对象. + *

注意, 需回收 Jedis 对象, 否则可能会耗尽连接池导致后续操作受到影响. + * @return 返回可用的 Jedis 连接. + */ + public static Jedis getConnection() { + JedisPool pool = POOL.get(); + if (pool == null || pool.isClosed()) { + reconnectRedis(); + pool = POOL.get(); + if (pool == null) { + throw new IllegalStateException("Redis connection lost"); + } + } + return pool.getResource(); + } + + /** + * 执行 Redis 操作并返回相关值. + *

本方法会自动回收 Jedis. + * @param function 待运行的操作. + * @param 返回值类型. + * @return 返回 function 返回的内容. + */ + public static R executeRedis(Function function) { + try (Jedis jedis = getConnection()) { + return function.apply(jedis); + } + } + + /** + * 检查 Redis 连接池是否有可用的资源. + * @return 如果连接池依然活跃, 返回 true. + */ + public static boolean available() { + JedisPool jedisPool = POOL.get(); + if (jedisPool == null || jedisPool.isClosed()) { + reconnectRedis(); + if (jedisPool == null || jedisPool.isClosed()) { + return false; + } + } + if (jedisPool.getNumIdle() == 0) { + try (Jedis jedis = jedisPool.getResource()) { + return "pong".equalsIgnoreCase(jedis.ping()); + } catch (Exception e) { + log.error("Redis 连接测试时发生异常", e); + } + return false; + } + return true; + } + +} diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisMapCacheStore.java b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisMapCacheStore.java new file mode 100644 index 0000000..74c6dc5 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisMapCacheStore.java @@ -0,0 +1,175 @@ +/* + * 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.redis; + +import com.google.common.base.Strings; +import net.lamgc.cgj.bot.cache.CacheKey; +import net.lamgc.cgj.bot.cache.MapCacheStore; +import net.lamgc.cgj.bot.cache.convert.StringConverter; + +import java.util.*; + +/** + * Redis Map缓存存储容器. + * @param 值类型. + * @author LamGC + */ +public class RedisMapCacheStore extends RedisCacheStore> implements MapCacheStore { + + private final String keyPrefix; + private final StringConverter converter; + + public RedisMapCacheStore(String keyPrefix, StringConverter converter) { + keyPrefix = Strings.nullToEmpty(keyPrefix).trim(); + if (!keyPrefix.isEmpty() && keyPrefix.endsWith(RedisUtils.KEY_SEPARATOR)) { + this.keyPrefix = keyPrefix; + } else { + this.keyPrefix = keyPrefix + RedisUtils.KEY_SEPARATOR; + } + + this.converter = Objects.requireNonNull(converter); + } + + @Override + protected String getKeyPrefix() { + return this.keyPrefix; + } + + @Override + public int mapSize(CacheKey key) { + return RedisConnectionPool.executeRedis(jedis -> { + String keyString = getKeyString(key); + if (jedis.exists(keyString)) { + return jedis.hlen(keyString).intValue(); + } + return 0; + }); + } + + @Override + public Set mapFieldSet(CacheKey key) { + return RedisConnectionPool.executeRedis(jedis -> { + String keyString = getKeyString(key); + if (jedis.exists(keyString)) { + return jedis.hkeys(keyString); + } + return null; + }); + } + + @Override + public Set mapValueSet(CacheKey key) { + List rawValueSet = RedisConnectionPool.executeRedis(jedis -> { + String keyString = getKeyString(key); + if (jedis.exists(keyString)) { + return jedis.hvals(keyString); + } + return null; + }); + + if (rawValueSet == null) { + return null; + } + + Set result = new HashSet<>(); + for (String rawValue : rawValueSet) { + result.add(converter.from(rawValue)); + } + return result; + } + + @Override + public boolean put(CacheKey key, String field, V value) { + Objects.requireNonNull(field); + Objects.requireNonNull(value); + return RedisConnectionPool.executeRedis(jedis -> { + String keyString = getKeyString(key); + return jedis.hset(keyString, field, converter.to(value)) == RedisUtils.RETURN_CODE_OK; + }); + } + + @Override + public boolean putAll(CacheKey key, Map map) { + Objects.requireNonNull(key); + Objects.requireNonNull(map); + if (map.size() == 0 && exists(key)) { + return true; + } + + final Map targetMap = new HashMap<>(map.size()); + map.forEach((k, v) -> targetMap.put(k, converter.to(v))); + return RedisConnectionPool.executeRedis(jedis -> { + String keyString = getKeyString(key); + return RedisUtils.isOk(jedis.hmset(keyString, targetMap)); + }); + } + + @Override + public boolean putIfNotExist(CacheKey key, String field, V value) { + Objects.requireNonNull(field); + Objects.requireNonNull(value); + return RedisConnectionPool.executeRedis(jedis -> { + String keyString = getKeyString(key); + return jedis.hsetnx(keyString, field, converter.to(value)) == RedisUtils.RETURN_CODE_OK; + }); + } + + @Override + public V get(CacheKey key, String field) { + Objects.requireNonNull(field); + String value = RedisConnectionPool.executeRedis(jedis -> jedis.hget(getKeyString(key), field)); + if (value == null) { + return null; + } + return converter.from(value); + } + + @Override + public boolean removeField(CacheKey key, String field) { + Objects.requireNonNull(field); + return RedisConnectionPool.executeRedis(jedis -> + jedis.hdel(getKeyString(key), field) == RedisUtils.RETURN_CODE_OK); + } + + @Override + public boolean containsField(CacheKey key, String field) { + Objects.requireNonNull(field); + return RedisConnectionPool.executeRedis(jedis -> jedis.hexists(getKeyString(key), field)); + } + + @Override + public boolean mapIsEmpty(CacheKey key) { + return mapSize(key) == 0; + } + + @Override + public boolean clearMap(CacheKey key) { + Set fields = mapFieldSet(key); + if (fields == null) { + return false; + } else if (fields.size() == 0) { + return true; + } + + String[] fieldsArray = new String[fields.size()]; + fields.toArray(fieldsArray); + return RedisConnectionPool.executeRedis(jedis -> + jedis.hdel(getKeyString(key), fieldsArray) != RedisUtils.RETURN_CODE_FAILED); + } + +} diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisSingleCacheStore.java b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisSingleCacheStore.java new file mode 100644 index 0000000..253eb75 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisSingleCacheStore.java @@ -0,0 +1,77 @@ +/* + * 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.redis; + +import com.google.common.base.Strings; +import net.lamgc.cgj.bot.cache.CacheKey; +import net.lamgc.cgj.bot.cache.SingleCacheStore; +import net.lamgc.cgj.bot.cache.convert.StringConverter; + +import java.util.Objects; + +/** + * Redis 单项缓存存储容器. + * @param 值类型. + * @see net.lamgc.cgj.bot.cache.CacheStore + * @see net.lamgc.cgj.bot.cache.SingleCacheStore + * @see net.lamgc.cgj.bot.cache.redis.RedisCacheStore + * @author LamGC + */ +public class RedisSingleCacheStore extends RedisCacheStore implements SingleCacheStore { + + private final String keyPrefix; + private final StringConverter converter; + + public RedisSingleCacheStore(String keyPrefix, StringConverter converter) { + keyPrefix = Strings.nullToEmpty(keyPrefix).trim(); + if (!keyPrefix.isEmpty() && keyPrefix.endsWith(RedisUtils.KEY_SEPARATOR)) { + this.keyPrefix = keyPrefix; + } else { + this.keyPrefix = keyPrefix + RedisUtils.KEY_SEPARATOR; + } + + this.converter = Objects.requireNonNull(converter); + } + + @Override + public boolean set(CacheKey key, V value) { + return RedisConnectionPool.executeRedis(jedis -> + RedisUtils.isOk(jedis.set(getKeyString(key), converter.to(Objects.requireNonNull(value))))); + } + + @Override + public boolean setIfNotExist(CacheKey key, V value) { + return RedisConnectionPool.executeRedis(jedis -> + jedis.setnx(getKeyString(key), converter.to(Objects.requireNonNull(value))) + == RedisUtils.RETURN_CODE_OK); + } + + @Override + public V get(CacheKey key) { + String value = RedisConnectionPool.executeRedis(jedis -> jedis.get(getKeyString(key))); + if (value == null) { + return null; + } + return converter.from(value); + } + + @Override + protected String getKeyPrefix() { + return this.keyPrefix; + } +} diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisUtils.java b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisUtils.java new file mode 100644 index 0000000..66f9974 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/main/java/net/lamgc/cgj/bot/cache/redis/RedisUtils.java @@ -0,0 +1,54 @@ +/* + * 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.redis; + +/** + * @author LamGC + */ +public class RedisUtils { + + /** + * 返回码 - 成功 + */ + public final static int RETURN_CODE_OK = 1; + + /** + * 返回码 - 失败 + */ + public final static int RETURN_CODE_FAILED = 0; + + /** + * Key匹配规则 - 所有Key + */ + public final static String KEY_PATTERN_ALL = "*"; + + /** + * Key 分隔符 + */ + public final static String KEY_SEPARATOR = ":"; + + /** + * 检查字符串返回结果是否为操作成功. + * @param result 字符串返回结果. + * @return 如果为操作成功, 返回 true. + */ + public static boolean isOk(String result) { + return "OK".equalsIgnoreCase(result); + } + +} diff --git a/ContentGrabbingJi-CacheStore-redis/src/main/resources/META-INF/services/net.lamgc.cgj.bot.cache.CacheStoreFactory b/ContentGrabbingJi-CacheStore-redis/src/main/resources/META-INF/services/net.lamgc.cgj.bot.cache.CacheStoreFactory new file mode 100644 index 0000000..1b0b7e6 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/main/resources/META-INF/services/net.lamgc.cgj.bot.cache.CacheStoreFactory @@ -0,0 +1,18 @@ +# +# 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 . +# + +net.lamgc.cgj.bot.cache.redis.RedisCacheStoreFactory diff --git a/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisMapCacheStoreTest.java b/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisMapCacheStoreTest.java new file mode 100644 index 0000000..f577829 --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisMapCacheStoreTest.java @@ -0,0 +1,151 @@ +/* + * 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.redis; + + +import net.lamgc.cgj.bot.cache.CacheKey; +import net.lamgc.cgj.bot.cache.MapCacheStore; +import net.lamgc.cgj.bot.cache.convert.StringToStringConverter; +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * @see RedisMapCacheStore + */ +public class RedisMapCacheStoreTest { + + private final static MapCacheStore cacheStore = + new RedisMapCacheStore<>("test", new StringToStringConverter()); + + @Before + public void before() { + Assert.assertTrue(cacheStore.clear()); + } + + @After + public void after() { + Assert.assertTrue(cacheStore.clear()); + } + + @Test + public void nullThrowTest() { + final CacheKey key = new CacheKey("testKey"); + + Assert.assertThrows(NullPointerException.class, () -> cacheStore.mapSize(null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.mapFieldSet(null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.mapValueSet(null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.put(null, "field", "value")); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.put(key, null, "value")); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.put(key, "field", null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.putAll(null, new HashMap<>())); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.putAll(key, null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.putIfNotExist(null, "field", "value")); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.putIfNotExist(key, null, "value")); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.putIfNotExist(key, "field", null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.get(key, null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.get(null, "field")); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.removeField(key, null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.removeField(null, "field")); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.mapIsEmpty(null)); + Assert.assertThrows(NullPointerException.class, () -> cacheStore.clearMap(null)); + } + + @Test + public void keyNotExistTest() { + final CacheKey key = new CacheKey("testKey"); + HashMap testMap = new HashMap<>(); + testMap.put("testField", "value"); + + // 例外情况: 因为 Redis 对空 Map 的处理机制, 导致返回只能为空. + Assert.assertEquals(0, cacheStore.mapSize(key)); + // 例外情况: mapIsEmpty 在 Redis 没有相应指令, 所以是依靠 mapSize 实现的, + // 同样因 mapSize 的原因, 不存在等于空. + Assert.assertTrue(cacheStore.mapIsEmpty(key)); + + Assert.assertFalse(cacheStore.clearMap(key)); + Assert.assertFalse(cacheStore.containsField(key, "Field")); + Assert.assertFalse(cacheStore.removeField(key, "Field")); + Assert.assertNull(cacheStore.get(key, "Field")); + Assert.assertTrue(cacheStore.put(key, "Field", "value")); + Assert.assertTrue("clearMap operation failed!", cacheStore.remove(key)); + Assert.assertTrue(cacheStore.putAll(key, testMap)); + Assert.assertTrue("clearMap operation failed!", cacheStore.remove(key)); + Assert.assertTrue(cacheStore.putIfNotExist(key, "Field", "value")); + } + + @Test + public void putAndGetTest() { + final CacheKey key = new CacheKey("testKey"); + final 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"); + + // put/get, mapIsEmpty, containsField + Assert.assertTrue("put operation failed!", cacheStore.put(key, "test00", "testValue00")); + Assert.assertTrue(cacheStore.containsField(key, "test00")); + Assert.assertEquals("testValue00", cacheStore.get(key, "test00")); + Assert.assertTrue("removeField operation failed!", cacheStore.removeField(key, "test00")); + + // putIfNotExist + Assert.assertTrue(cacheStore.putIfNotExist(key, "test00", "testValue00")); + Assert.assertFalse(cacheStore.putIfNotExist(key, "test00", "testValue00")); + Assert.assertTrue("clearMap operation failed!", cacheStore.clearMap(key)); + + // putAll + Assert.assertTrue(cacheStore.putAll(key, expectedMap)); + Assert.assertTrue(expectedMap.keySet().containsAll(cacheStore.mapFieldSet(key))); + Assert.assertTrue(expectedMap.values().containsAll(cacheStore.mapValueSet(key))); + } + + @Test + public void fieldChangeTest() { + final CacheKey key = new CacheKey("testKey"); + final 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"); + + // mapSize, clearMap, mapIsEmpty 测试 + Assert.assertTrue("putAll operation failed!", cacheStore.putAll(key, expectedMap)); + Assert.assertEquals(expectedMap.size(), cacheStore.mapSize(key)); + Assert.assertTrue(cacheStore.clearMap(key)); + Assert.assertEquals(0, cacheStore.mapSize(key)); + Assert.assertTrue(cacheStore.mapIsEmpty(key)); + + // removeField 多分支测试 + Assert.assertTrue("put operation failed!", cacheStore.put(key, "test00", "testValue00")); + Assert.assertTrue(cacheStore.containsField(key, "test00")); + Assert.assertEquals("testValue00", cacheStore.get(key, "test00")); + Assert.assertTrue("removeField operation failed!", cacheStore.removeField(key, "test00")); + Assert.assertFalse(cacheStore.removeField(key, "test00")); + } + + +} \ No newline at end of file diff --git a/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisSingleCacheStoreTest.java b/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisSingleCacheStoreTest.java new file mode 100644 index 0000000..031487c --- /dev/null +++ b/ContentGrabbingJi-CacheStore-redis/src/test/java/net/lamgc/cgj/bot/cache/redis/RedisSingleCacheStoreTest.java @@ -0,0 +1,149 @@ +/* + * 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.redis; + +import net.lamgc.cgj.bot.cache.CacheKey; +import net.lamgc.cgj.bot.cache.SingleCacheStore; +import net.lamgc.cgj.bot.cache.convert.StringToStringConverter; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +/** + * @see RedisCacheStore + * @see RedisSingleCacheStore + */ +public class RedisSingleCacheStoreTest { + + private final static SingleCacheStore cacheStore = new RedisSingleCacheStore<>("test", new StringToStringConverter()); + + @Before + public void before() { + Assert.assertTrue(cacheStore.clear()); + } + + @Test + public void nullThrowTest() { + final SingleCacheStore tempCacheStore = + new RedisSingleCacheStore<>("test" + RedisUtils.KEY_SEPARATOR, new StringToStringConverter()); + final CacheKey key = new CacheKey("testKey"); + + // RedisSingleCacheStore + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.set(null, "testValue")); + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.set(key, null)); + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.get(null)); + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.setIfNotExist(null, "testValue")); + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.setIfNotExist(key, null)); + + // RedisCacheStore + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.exists(null)); + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.getTimeToLive(null)); + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.setTimeToLive(null, 0)); + Assert.assertThrows(NullPointerException.class, () -> tempCacheStore.remove(null)); + } + + @Test + public void setAndGetTest() { + final CacheKey key = new CacheKey("testKey"); + final String value = "testValue"; + + Assert.assertTrue("Set operation failed!", cacheStore.set(key, value)); + Assert.assertEquals(value, cacheStore.get(key)); + Assert.assertTrue("Remove operation failed!", cacheStore.remove(key)); + Assert.assertNull("Set operation failed!", cacheStore.get(key)); + } + + @Test + public void setIfNotExistTest() { + final CacheKey key = new CacheKey("testKey"); + 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 CacheKey key = new CacheKey("testKey"); + 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 CacheKey key = new CacheKey("testKey"); + 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 CacheKey key = new CacheKey("testKey"); + final String value = "testValue"; + + Assert.assertTrue("Set operation failed!", cacheStore.set(key, value)); + + Assert.assertTrue(cacheStore.exists(key)); + Assert.assertTrue("Clear operation failed!", cacheStore.clear()); + Assert.assertFalse(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"); + + expectedMap.forEach((key, value) -> cacheStore.set(new CacheKey(key), value)); + Assert.assertEquals(expectedMap.size(), cacheStore.size()); + Assert.assertTrue(expectedMap.keySet().containsAll(cacheStore.keySet())); + } + +}