From 2d975025805c2b06b3eb86e94d3d2e9109d992ba Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 30 Oct 2025 15:30:52 +0300 Subject: [PATCH 1/9] set up pom.xml: add jackson, add slfj, add assertJ dependensies --- .idea/amplicode-jpa.xml | 6 ++++++ .idea/misc.xml | 2 +- pom.xml | 38 +++++++++++++++++++++++++++++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 .idea/amplicode-jpa.xml diff --git a/.idea/amplicode-jpa.xml b/.idea/amplicode-jpa.xml new file mode 100644 index 0000000..98351bf --- /dev/null +++ b/.idea/amplicode-jpa.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 8dc554d..1be3843 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + diff --git a/pom.xml b/pom.xml index b4c0aac..dab0df0 100644 --- a/pom.xml +++ b/pom.xml @@ -12,20 +12,52 @@ 17 17 UTF-8 + 5.11.0 + 2.17.2 redis.clients jedis - 5.1.5 + 5.1.2 + + + com.fasterxml.jackson.core + jackson-databind + ${fasterxml.jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${fasterxml.jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${fasterxml.jackson.version} + + + org.slf4j + slf4j-api + 2.0.17 + + + ch.qos.logback + logback-classic + 1.5.18 org.junit.jupiter junit-jupiter - 5.8.1 + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + 3.26.3 test - \ No newline at end of file From e066995723d4311abfc7523eb6fedaa3fb416d8e Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 30 Oct 2025 15:48:16 +0300 Subject: [PATCH 2/9] add RedisWrapper.java, add serialization methods, add logback.xml, add logging --- src/main/java/org/redis/util/LogHelper.java | 16 ++ .../org/redis/wrapper/RedisWrapperMap.java | 176 ++++++++++++++++++ src/main/resources/logback.xml | 11 ++ 3 files changed, 203 insertions(+) create mode 100644 src/main/java/org/redis/util/LogHelper.java create mode 100644 src/main/java/org/redis/wrapper/RedisWrapperMap.java create mode 100644 src/main/resources/logback.xml diff --git a/src/main/java/org/redis/util/LogHelper.java b/src/main/java/org/redis/util/LogHelper.java new file mode 100644 index 0000000..27feb02 --- /dev/null +++ b/src/main/java/org/redis/util/LogHelper.java @@ -0,0 +1,16 @@ +package org.redis.util; + +public class LogHelper { + public static String safeToString(Object o) { + try { + return String.valueOf(o); + } catch (Exception ex) { + return ""; + } + } + + public static String truncate(String s, int max) { + if (s == null) return null; + return s.length() <= max ? s : s.substring(0, max) + "...(truncated)"; + } +} diff --git a/src/main/java/org/redis/wrapper/RedisWrapperMap.java b/src/main/java/org/redis/wrapper/RedisWrapperMap.java new file mode 100644 index 0000000..99168f8 --- /dev/null +++ b/src/main/java/org/redis/wrapper/RedisWrapperMap.java @@ -0,0 +1,176 @@ +package org.redis.wrapper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.redis.util.LogHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import redis.clients.jedis.JedisPool; + +import java.io.IOException; +import java.util.*; + +public class RedisWrapperMap implements Map, AutoCloseable { + private static final Logger LOG = LoggerFactory.getLogger(RedisWrapperMap.class); + private final JedisPool jedisPool; + private final ObjectMapper objectMapper; + private final String prefix; + private final String keysSetKey; + private Class valueType; + private Class keyType; + + public RedisWrapperMap(JedisPool pool, String prefix) { + this.jedisPool = Objects.requireNonNull(pool, "the 'pool' refers to null!"); + this.objectMapper = new ObjectMapper(); + this.prefix = Objects.requireNonNull(prefix, "prefix is null!"); + this.keysSetKey = prefix + ":keys"; + LOG.info("Initialized RedisWrapperMap: prefix='{}', keysSetKey='{}'", this.prefix, this.keysSetKey); + } + + private String keyForRedisString(String key) { + String redisKey = prefix + ":entry:" + key; + if (LOG.isTraceEnabled()) { + LOG.trace("Computed redisKeyForSerializedKey: serializedKey='{}' -> redisKey='{}'", key, redisKey); + } + return redisKey; + } + + private String serializeKey(K key) { + keyType = (Class) key.getClass(); + long t0 = System.nanoTime(); + try { + String json = objectMapper.writeValueAsString(key); + if (LOG.isDebugEnabled()) { + LOG.debug("Serialized key: type='{}', jsonLength={}, json='{}', timeMicros={}", + keyType.getName(), + json.length(), json, (System.nanoTime() - t0) / 1_000); + } + return json; + } catch (JsonProcessingException e) { + LOG.error("Failed to serialize key: type='{}', value='{}'", keyType, LogHelper.safeToString(key), e); + throw new RuntimeException("Unable to serialize key!", e); + } + } + + private String serializeValue(V value) { + valueType = (Class) value.getClass(); + long t0 = System.nanoTime(); + try { + String json = objectMapper.writeValueAsString(value); + if (LOG.isDebugEnabled()) { + LOG.debug("Serialized value: type='{}', jsonLength={}, timeMicros={}", + valueType.getName(), + json.length(), (System.nanoTime() - t0) / 1_000); + } + return json; + } catch (JsonProcessingException e) { + LOG.error("Failed to serialize value: type='{}', value='{}'", + valueType, + LogHelper.safeToString(value), e); + throw new RuntimeException("Unable to serialize value!", e); + } + } + + private V deserializeValue(String json) { + if (json == null) return null; + long t0 = System.nanoTime(); + try { + V value = objectMapper.readValue(json, valueType); + if (LOG.isTraceEnabled()) { + LOG.trace("Deserialized value (generic): jsonLength={}, timeMicros={}", + json.length(), + (System.nanoTime() - t0) / 1_000); + } + return value; + } catch (IOException e) { + LOG.error("Failed to deserialize value: jsonLength={}, json='{}'", + json.length(), + LogHelper.truncate(json, 512), e); + throw new RuntimeException("Unable to deserialize value", e); + } + } + + private K deserializeKey(String json) { + if (json == null) return null; + long t0 = System.nanoTime(); + try { + K key = objectMapper.readValue(json, keyType); + if (LOG.isTraceEnabled()) { + LOG.trace("Deserialized: targetType='{}', jsonLength={}, timeMicros={}", + keyType != null ? keyType.getName() : "null", json.length(), + (System.nanoTime() - t0) / 1_000); + } + return key; + } catch (IOException e) { + LOG.error("Failed to deserialize: targetType='{}', jsonLength={}, json='{}'", + keyType != null ? keyType.getName() : "null", json.length(), + LogHelper.truncate(json, 512), e); + throw new RuntimeException("Unable to deserialize", e); + } + } + + @Override + public void close() throws Exception { + + } + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return false; + } + + @Override + public boolean containsKey(Object o) { + return false; + } + + @Override + public boolean containsValue(Object o) { + return false; + } + + @Override + public V get(Object o) { + return null; + } + + @Override + public V put(K k, V v) { + return null; + } + + @Override + public V remove(Object o) { + return null; + } + + @Override + public void putAll(Map map) { + + } + + @Override + public void clear() { + + } + + @Override + public Set keySet() { + return Set.of(); + } + + @Override + public Collection values() { + return List.of(); + } + + @Override + public Set> entrySet() { + return Set.of(); + } +} diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..ee88f44 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file From 5c080b75b6e371f049a53817c6d825008df05aaf Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 30 Oct 2025 19:18:18 +0300 Subject: [PATCH 3/9] add logged CRUD (put(), get(), remove() and clear() implementation) --- .../org/redis/wrapper/RedisWrapperMap.java | 104 +++++++++++++++++- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/redis/wrapper/RedisWrapperMap.java b/src/main/java/org/redis/wrapper/RedisWrapperMap.java index 99168f8..064c37a 100644 --- a/src/main/java/org/redis/wrapper/RedisWrapperMap.java +++ b/src/main/java/org/redis/wrapper/RedisWrapperMap.java @@ -5,7 +5,9 @@ import org.redis.util.LogHelper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; +import redis.clients.jedis.Pipeline; import java.io.IOException; import java.util.*; @@ -136,17 +138,85 @@ public boolean containsValue(Object o) { @Override public V get(Object o) { - return null; + @SuppressWarnings("unchecked") + String serializedKey = serializeKey((K) o); + String redisKey = keyForRedisString(serializedKey); + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + String value = jedis.get(redisKey); + if (value == null) { + LOG.info("Get: MISS key='{}', redisKey='{}'", LogHelper.safeToString(o), redisKey); + return null; + } + V val = deserializeValue(value); + LOG.info("Get: HIT key='{}' -> valueType='{}', redisKey='{}'", + LogHelper.safeToString(o), + val != null ? val.getClass().getName() : "null", redisKey); + LOG.debug("Get latencyMicros={}, valueJsonLength={}", (System.nanoTime() - t0) / 1_000, value.length()); + return val; + } catch (Exception e) { + LOG.error("Get failed: key='{}', redisKey='{}'", LogHelper.safeToString(o), redisKey, e); + throw e; + } } @Override - public V put(K k, V v) { - return null; + public V put(K key, V value) { + String serializedKey = serializeKey(key); + String serializedValue = serializeValue(value); + String redisKey = keyForRedisString(serializedKey); + long t0 = System.nanoTime(); + + try (Jedis jedis = jedisPool.getResource()) { + String prev = jedis.get(redisKey); + Pipeline pipeline = jedis.pipelined(); + pipeline.set(redisKey, serializedValue); + pipeline.sadd(keysSetKey, serializedKey); + pipeline.sync(); + LOG.info("Put: keyType='{}', valueType='{}', redisKey='{}', keysSetKey='{}'", + key.getClass().getName(), + value.getClass().getName(), + redisKey, keysSetKey); + LOG.debug("Put latencyMicros={}, prevExists={}", (System.nanoTime() - t0) / 1_000, prev != null); + + if (prev == null) return null; + + return deserializeValue(prev); + + } catch (Exception e) { + LOG.error("Put failed: key='{}', redisKey='{}'", + LogHelper.safeToString(key), + redisKey, e); + throw e; + } } @Override - public V remove(Object o) { - return null; + public V remove(Object key) { + @SuppressWarnings("unchecked") + String sk = serializeKey((K) key); + String redisKeyForSerializedKey = keyForRedisString(sk); + long t0 = System.nanoTime(); + + try (Jedis jedis = jedisPool.getResource()) { + String prev = jedis.get(redisKeyForSerializedKey); + Pipeline pipeline = jedis.pipelined(); + pipeline.del(redisKeyForSerializedKey); + pipeline.srem(keysSetKey, sk); + pipeline.sync(); + LOG.info("Remove: key='{}', redisKey='{}', removedFromSet='{}'", + LogHelper.safeToString(key), + redisKeyForSerializedKey, keysSetKey); + LOG.debug("Remove latencyMicros={}, hadPrev={}", (System.nanoTime() - t0) / 1_000, prev != null); + + if (prev == null) return null; + return deserializeValue(prev); + } catch (Exception e) { + LOG.error("Remove failed: key='{}', redisKey='{}'", + LogHelper.safeToString(key), + redisKeyForSerializedKey, e); + throw e; + } } @Override @@ -156,7 +226,29 @@ public void putAll(Map map) { @Override public void clear() { - + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + Set allSerializedKeys = jedis.smembers(keysSetKey); + LOG.info("Clear: keysSetKey='{}', keysCount={}", keysSetKey, allSerializedKeys.size()); + if (!allSerializedKeys.isEmpty()) { + Pipeline pipeline = jedis.pipelined(); + int ops = 0; + for (String sk : allSerializedKeys) { + pipeline.del(keyForRedisString(sk)); + ops++; + } + pipeline.del(keysSetKey); + ops++; + pipeline.sync(); + LOG.debug("Clear: pipelineOps={}, latencyMicros={}", ops, (System.nanoTime() - t0) / 1_000); + } else { + jedis.del(keysSetKey); + LOG.debug("Clear: keysSetKey deleted (was already empty)"); + } + } catch (Exception e) { + LOG.error("Clear failed: keysSetKey='{}'", keysSetKey, e); + throw e; + } } @Override From fd077c042a7cb8e2a8a13de6bc4f10c7ca288e31 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 30 Oct 2025 19:40:45 +0300 Subject: [PATCH 4/9] add containsKey(), containsValue(), size(), isEmpty() and close() logged implementation --- .../org/redis/wrapper/RedisWrapperMap.java | 81 +++++++++++++++++-- 1 file changed, 73 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/redis/wrapper/RedisWrapperMap.java b/src/main/java/org/redis/wrapper/RedisWrapperMap.java index 064c37a..d9d58f2 100644 --- a/src/main/java/org/redis/wrapper/RedisWrapperMap.java +++ b/src/main/java/org/redis/wrapper/RedisWrapperMap.java @@ -8,9 +8,15 @@ import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.Pipeline; +import redis.clients.jedis.Response; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; public class RedisWrapperMap implements Map, AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(RedisWrapperMap.class); @@ -113,27 +119,86 @@ private K deserializeKey(String json) { @Override public void close() throws Exception { - + LOG.info("Closing JedisPool for prefix='{}'", prefix); + jedisPool.close(); + LOG.info("JedisPool closed for prefix='{}'", prefix); } @Override public int size() { - return 0; + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + Long result = jedis.scard(keysSetKey); + int size = result.intValue(); + LOG.info("Size: keysSetKey='{}' -> {}", keysSetKey, size); + LOG.debug("Size latencyMicros={}", (System.nanoTime() - t0) / 1_000); + return size; + } catch (Exception e) { + LOG.error("Size check failed: keysSetKey='{}'", keysSetKey, e); + throw e; + } } @Override public boolean isEmpty() { - return false; + boolean empty = size() == 0; + LOG.debug("IsEmpty: {}", empty); + return empty; } @Override - public boolean containsKey(Object o) { - return false; + public boolean containsKey(Object key) { + @SuppressWarnings("unchecked") + String serializedKey = serializeKey((K) key); + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + boolean result = jedis.sismember(keysSetKey, serializedKey); + LOG.info("ContainsKey: key='{}' (serialized='{}') -> {}", LogHelper.safeToString(key), serializedKey, result); + LOG.debug("ContainsKey latencyMicros={}", (System.nanoTime() - t0) / 1_000); + return result; + } catch (Exception e) { + LOG.error("ContainsKey failed: key='{}'", LogHelper.safeToString(key), e); + throw e; + } } @Override - public boolean containsValue(Object o) { - return false; + public boolean containsValue(Object value) { + @SuppressWarnings("unchecked") + String serializedValue = serializeValue((V) value); + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + Set allKeys = jedis.smembers(keysSetKey); + LOG.debug("ContainsValue: keysSet size={}", allKeys.size()); + if (allKeys.isEmpty()) { + LOG.info("ContainsValue: empty keysSet -> false"); + return false; + } + Pipeline pipeline = jedis.pipelined(); + List> responses = new ArrayList<>(allKeys.size()); + for (String key : allKeys) { + responses.add(pipeline.get(keyForRedisString(key))); + } + pipeline.sync(); + int matchedIndex = -1; + for (int i = 0; i < responses.size(); i++) { + Response response = responses.get(i); + String stored = response.get(); + if (Objects.equals(stored, serializedValue)) { + matchedIndex = i; + break; + } + } + boolean found = matchedIndex >= 0; + LOG.info("ContainsValue: found={} (keysQueried={}, latencyMicros={})", + found, + responses.size(), + (System.nanoTime() - t0) / 1_000); + return found; + } catch (Exception e) { + LOG.error("ContainsValue failed: value='{}'", LogHelper.safeToString(value), e); + throw e; + } } @Override From 2a0febd499f9576c9220666312d8f715b556ef49 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 30 Oct 2025 19:45:02 +0300 Subject: [PATCH 5/9] add keySet(), values(), entrySet() logged impl --- .../org/redis/wrapper/RedisWrapperMap.java | 93 +++++++++++++++++-- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/redis/wrapper/RedisWrapperMap.java b/src/main/java/org/redis/wrapper/RedisWrapperMap.java index d9d58f2..4d1d497 100644 --- a/src/main/java/org/redis/wrapper/RedisWrapperMap.java +++ b/src/main/java/org/redis/wrapper/RedisWrapperMap.java @@ -11,12 +11,7 @@ import redis.clients.jedis.Response; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; +import java.util.*; public class RedisWrapperMap implements Map, AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(RedisWrapperMap.class); @@ -318,16 +313,96 @@ public void clear() { @Override public Set keySet() { - return Set.of(); + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + Set ssk = jedis.smembers(keysSetKey); + Set result = new HashSet<>(ssk.size()); + for (String sk : ssk) { + K k = deserializeKey(sk); + result.add(k); + } + LOG.info("KeySet: size={}, keysSetKey='{}'", result.size(), keysSetKey); + LOG.debug("KeySet latencyMicros={}", (System.nanoTime() - t0) / 1_000); + + return Collections.unmodifiableSet(result); + + } catch (Exception e) { + LOG.error("KeySet failed: keysSetKey='{}'", keysSetKey, e); + throw e; + } } @Override public Collection values() { - return List.of(); + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + Set ssk = jedis.smembers(keysSetKey); + if (ssk.isEmpty()) { + LOG.info("Values: empty -> []"); + return Collections.emptyList(); + } + Pipeline pipeline = jedis.pipelined(); + List> responses = new ArrayList<>(ssk.size()); + for (String sk : ssk) { + responses.add(pipeline.get(keyForRedisString(sk))); + } + pipeline.sync(); + List values = new ArrayList<>(responses.size()); + int nullCount = 0; + for (Response r : responses) { + String json = r.get(); + if (json == null) { + nullCount++; + values.add(null); + } else { + V value = deserializeValue(json); + values.add(value); + } + } + LOG.info("Values: count={}, nulls={}, latencyMicros={}", + values.size(), + nullCount, + (System.nanoTime() - t0) / 1_000); + return Collections.unmodifiableList(values); + } catch (Exception e) { + LOG.error("Values failed", e); + throw e; + } } @Override public Set> entrySet() { - return Set.of(); + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + Set allKeys = jedis.smembers(keysSetKey); + Set> entries = new HashSet<>(allKeys.size()); + if (!allKeys.isEmpty()) { + Pipeline pipeline = jedis.pipelined(); + Map> respMap = new LinkedHashMap<>(); + for (String key : allKeys) { + respMap.put(key, pipeline.get(keyForRedisString(key))); + } + pipeline.sync(); + int nullVals = 0; + for (Map.Entry> e : respMap.entrySet()) { + String sk = e.getKey(); + String jsonVal = e.getValue().get(); + K k = deserializeKey(sk); + V v = jsonVal == null ? null : deserializeValue(jsonVal); + if (v == null) nullVals++; + entries.add(new AbstractMap.SimpleImmutableEntry<>(k, v)); + } + LOG.info("EntrySet: size={}, nullValues={}, latencyMicros={}", + entries.size(), + nullVals, + (System.nanoTime() - t0) / 1_000); + } else { + LOG.info("EntrySet: empty"); + } + return Collections.unmodifiableSet(entries); + } catch (Exception e) { + LOG.error("EntrySet failed: keysSetKey='{}'", keysSetKey, e); + throw e; + } } } From 811d06701baed8ef17b2276679cbf6bd6320ba9e Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 30 Oct 2025 19:50:32 +0300 Subject: [PATCH 6/9] add putAll() logged impl, hashCode() and equals() --- .../org/redis/wrapper/RedisWrapperMap.java | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/redis/wrapper/RedisWrapperMap.java b/src/main/java/org/redis/wrapper/RedisWrapperMap.java index 4d1d497..ca1b89e 100644 --- a/src/main/java/org/redis/wrapper/RedisWrapperMap.java +++ b/src/main/java/org/redis/wrapper/RedisWrapperMap.java @@ -280,8 +280,29 @@ public V remove(Object key) { } @Override - public void putAll(Map map) { - + public void putAll(Map another) { + if (another.isEmpty()) { + LOG.info("PutAll: empty map -> no-op"); + return; + } + long t0 = System.nanoTime(); + try (Jedis jedis = jedisPool.getResource()) { + Pipeline pipeline = jedis.pipelined(); + int count = 0; + for (Entry e : another.entrySet()) { + String sk = serializeKey(e.getKey()); + String sv = serializeValue(e.getValue()); + pipeline.set(keyForRedisString(sk), sv); + pipeline.sadd(keysSetKey, sk); + count++; + } + pipeline.sync(); + LOG.info("PutAll: entries={}, keysSetKey='{}'", count, keysSetKey); + LOG.debug("PutAll latencyMicros={}, pipelineOps={}", (System.nanoTime() - t0) / 1_000, count * 2); + } catch (Exception e) { + LOG.error("PutAll failed: entries={}", another.size(), e); + throw e; + } } @Override @@ -405,4 +426,17 @@ public Set> entrySet() { throw e; } } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Map)) return false; + Map other = (Map) o; + return this.entrySet().equals(other.entrySet()); + } + + @Override + public int hashCode() { + return entrySet().hashCode(); + } } From e69b4371c07036b39e9da736abbd9a483e4f8523 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 30 Oct 2025 20:01:44 +0300 Subject: [PATCH 7/9] add tests --- .../org/redis/wrapper/RedisWrapperMap.java | 13 ++- .../redis/wrapper/RedisWrapperMapTest.java | 97 +++++++++++++++++++ 2 files changed, 108 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/redis/wrapper/RedisWrapperMapTest.java diff --git a/src/main/java/org/redis/wrapper/RedisWrapperMap.java b/src/main/java/org/redis/wrapper/RedisWrapperMap.java index ca1b89e..a38497f 100644 --- a/src/main/java/org/redis/wrapper/RedisWrapperMap.java +++ b/src/main/java/org/redis/wrapper/RedisWrapperMap.java @@ -11,7 +11,16 @@ import redis.clients.jedis.Response; import java.io.IOException; -import java.util.*; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; public class RedisWrapperMap implements Map, AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(RedisWrapperMap.class); @@ -113,7 +122,7 @@ private K deserializeKey(String json) { } @Override - public void close() throws Exception { + public void close() { LOG.info("Closing JedisPool for prefix='{}'", prefix); jedisPool.close(); LOG.info("JedisPool closed for prefix='{}'", prefix); diff --git a/src/test/java/org/redis/wrapper/RedisWrapperMapTest.java b/src/test/java/org/redis/wrapper/RedisWrapperMapTest.java new file mode 100644 index 0000000..0a12579 --- /dev/null +++ b/src/test/java/org/redis/wrapper/RedisWrapperMapTest.java @@ -0,0 +1,97 @@ +package org.redis.wrapper; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class RedisWrapperMapTest { + private JedisPool pool; + private RedisWrapperMap map; + + @BeforeAll + public void beforeAll() { + pool = new JedisPool(new JedisPoolConfig(), "localhost", 6379); + map = new RedisWrapperMap<>(pool, "test_map"); + map.clear(); + } + + @AfterAll + public void afterAll() { + map.clear(); + map.close(); + } + + @BeforeEach + public void beforeEach() { + map.clear(); + } + + @Test + public void testPutGetRemove() { + assertTrue(map.isEmpty()); + assertNull(map.put("k1", "v1")); + assertEquals("v1", map.get("k1")); + assertEquals(1, map.size()); + assertEquals("v1", map.remove("k1")); + assertNull(map.get("k1")); + assertEquals(0, map.size()); + } + + @Test + public void testContainsKeyAndValue() { + map.put("a", "1"); + map.put("b", "2"); + assertTrue(map.containsKey("a")); + assertTrue(map.containsValue("2")); + assertFalse(map.containsValue("3")); + } + + @Test + public void testPutAllAndClear() { + Map other = new HashMap<>(); + other.put("x", "10"); + other.put("y", "20"); + map.putAll(other); + assertEquals(2, map.size()); + assertEquals(Set.of("x", "y"), map.keySet()); + map.clear(); + assertTrue(map.isEmpty()); + } + + @Test + public void testEntrySetAndValues() { + map.put("k1", "v1"); + map.put("k2", "v2"); + Set> entries = map.entrySet(); + assertEquals(2, entries.size()); + Collection vals = map.values(); + assertTrue(vals.contains("v1")); + assertTrue(vals.contains("v2")); + } + + @Test + public void testRemoveViaKey() { + map.put("a", "1"); + map.put("b", "2"); + + map.remove("a"); + + assertFalse(map.containsKey("a")); + assertEquals(1, map.size()); + } +} \ No newline at end of file From e24e07fdde75dfb6103ccbf3a03fa4c822abe133 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 30 Oct 2025 20:48:05 +0300 Subject: [PATCH 8/9] add JavaDoc --- src/main/java/org/redis/util/LogHelper.java | 27 +++ .../org/redis/wrapper/RedisWrapperMap.java | 196 ++++++++++++++++++ 2 files changed, 223 insertions(+) diff --git a/src/main/java/org/redis/util/LogHelper.java b/src/main/java/org/redis/util/LogHelper.java index 27feb02..bc0399d 100644 --- a/src/main/java/org/redis/util/LogHelper.java +++ b/src/main/java/org/redis/util/LogHelper.java @@ -1,6 +1,22 @@ package org.redis.util; +/** + * Утилитный класс для безопасной работы со строковыми представлениями объектов и обрезкой длинных строк. + * + *

Предназначен для логирования: позволяет избежать исключений при вызове {@code toString()}, + * а также ограничить длину выводимых строк для логов и отладочной информации.

+ */ public class LogHelper { + + /** + * Безопасно преобразует объект в строку. + * + *

Если {@code toString()} выбрасывает исключение, возвращает строку вида + * {@code ""} вместо падения приложения.

+ * + * @param o объект для преобразования + * @return строковое представление объекта или сообщение об ошибке + */ public static String safeToString(Object o) { try { return String.valueOf(o); @@ -9,6 +25,17 @@ public static String safeToString(Object o) { } } + /** + * Обрезает строку до заданной длины, добавляя пометку об усечении. + * + *

Если длина строки превышает {@code max}, возвращает первые {@code max} символов + * с добавлением суффикса {@code "...(truncated)"}. Если строка короче или равна — + * возвращается как есть.

+ * + * @param s исходная строка + * @param max максимальная допустимая длина + * @return обрезанная строка или {@code null}, если вход был {@code null} + */ public static String truncate(String s, int max) { if (s == null) return null; return s.length() <= max ? s : s.substring(0, max) + "...(truncated)"; diff --git a/src/main/java/org/redis/wrapper/RedisWrapperMap.java b/src/main/java/org/redis/wrapper/RedisWrapperMap.java index a38497f..19aa647 100644 --- a/src/main/java/org/redis/wrapper/RedisWrapperMap.java +++ b/src/main/java/org/redis/wrapper/RedisWrapperMap.java @@ -22,6 +22,27 @@ import java.util.Objects; import java.util.Set; +/** + * Обёртка над Redis, реализующая интерфейс {@link Map}, с сериализацией ключей и значений в JSON. + * + *

Ключи и значения сериализуются с помощью {@link ObjectMapper} и хранятся в Redis как строки. + * Для отслеживания всех ключей используется отдельное множество Redis ({@code keysSetKey}), + * что позволяет реализовать методы {@code keySet()}, {@code values()}, + * {@code entrySet()} и {@code size()}.

+ * + *

Ключи и значения сериализуются при записи и десериализуются при чтении. Типы {@code K} и {@code V} + * определяются во время выполнения на основе переданных объектов. Все операции логируются с указанием + * задержек и подробностей сериализации.

+ * + *

Класс потокобезопасен при условии, что {@link JedisPool} не передаётся между потоками напрямую.

+ * + * @param тип ключей + * @param тип значений + * + * @see Map + * @see JedisPool + * @see ObjectMapper + */ public class RedisWrapperMap implements Map, AutoCloseable { private static final Logger LOG = LoggerFactory.getLogger(RedisWrapperMap.class); private final JedisPool jedisPool; @@ -39,6 +60,15 @@ public RedisWrapperMap(JedisPool pool, String prefix) { LOG.info("Initialized RedisWrapperMap: prefix='{}', keysSetKey='{}'", this.prefix, this.keysSetKey); } + /** + * Формирует Redis-ключ для уже сериализованного ключа. + * + *

Комбинирует поле {@code prefix} с литералом {@code "entry"} и + * переданной сериализованной строкой ключа, возвращая итоговый ключ для хранения в Redis(String).

+ * + * @param key сериализованная строка ключа (не должна быть null) + * @return итоговый Redis-ключ в формате {@code prefix + ":entry:" + key} + */ private String keyForRedisString(String key) { String redisKey = prefix + ":entry:" + key; if (LOG.isTraceEnabled()) { @@ -47,6 +77,17 @@ private String keyForRedisString(String key) { return redisKey; } + /** + * Сериализует объект-ключ в JSON и сохраняет его runtime-класс в {@code keyType}. + * + *

Использует {@code objectMapper} для преобразования объекта в JSON. + * Логирует информацию о сериализации на уровне DEBUG и пробрасывает ошибки как + * непроверяемые исключения.

+ * + * @param key объект-ключ для сериализации; + * @return JSON-представление ключа + * @throws RuntimeException если сериализация в JSON завершилась неудачей + */ private String serializeKey(K key) { keyType = (Class) key.getClass(); long t0 = System.nanoTime(); @@ -64,6 +105,17 @@ private String serializeKey(K key) { } } + /** + * Сериализует объект-значение в JSON и сохраняет его runtime-класс в {@code valueType}. + * + *

Использует {@code objectMapper} для преобразования объекта в JSON. + * Логирует информацию о сериализации на уровне DEBUG и пробрасывает ошибки как + * непроверяемые исключения.

+ * + * @param value объект-значение для сериализации; не должен быть null + * @return JSON-представление значения + * @throws RuntimeException если сериализация в JSON завершилась неудачей + */ private String serializeValue(V value) { valueType = (Class) value.getClass(); long t0 = System.nanoTime(); @@ -83,6 +135,17 @@ private String serializeValue(V value) { } } + /** + * Десериализует JSON-строку в объект значения типа, сохранённого в {@code valueType}. + * + *

Если {@code json} равен {@code null}, возвращает {@code null}. Использует + * {@code objectMapper} для чтения JSON в тип {@code valueType}. Логирует время + * выполнения на уровне TRACE и пробрасывает ошибки как непроверяемые исключения.

+ * + * @param json JSON-строка, представляющая значение, или {@code null} + * @return десериализованное значение типа {@code V}, либо {@code null} если вход равен {@code null} + * @throws RuntimeException если десериализация JSON завершилась неудачей + */ private V deserializeValue(String json) { if (json == null) return null; long t0 = System.nanoTime(); @@ -102,6 +165,17 @@ private V deserializeValue(String json) { } } + /** + * Десериализует JSON-строку в объект ключа типа, сохранённого в {@code keyType}. + * + *

Если {@code json} равен {@code null}, возвращает {@code null}. Использует + * {@code objectMapper} для чтения JSON в тип {@code keyType}. Логирует время + * выполнения на уровне TRACE и пробрасывает ошибки как непроверяемые исключения.

+ * + * @param json JSON-строка, представляющая ключ, или {@code null} + * @return десериализованный ключ типа {@code K}, либо {@code null} если вход равен {@code null} + * @throws RuntimeException если десериализация JSON завершилась неудачей + */ private K deserializeKey(String json) { if (json == null) return null; long t0 = System.nanoTime(); @@ -121,6 +195,12 @@ private K deserializeKey(String json) { } } + /** + * Закрывает пул соединений Jedis. + * + *

Вызывается при завершении работы компонента, освобождая ресурсы Redis.

+ *

Логирует начало и завершение закрытия пула.

+ */ @Override public void close() { LOG.info("Closing JedisPool for prefix='{}'", prefix); @@ -128,6 +208,15 @@ public void close() { LOG.info("JedisPool closed for prefix='{}'", prefix); } + /** + * Возвращает количество элементов в хранилище. + * + *

Измеряет размер множества ключей {@code keysSetKey} в Redis с помощью команды {@code SCARD}.

+ *

Логирует результат и задержку выполнения.

+ * + * @return количество элементов в хранилище + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public int size() { long t0 = System.nanoTime(); @@ -143,6 +232,13 @@ public int size() { } } + /** + * Проверяет, пусто ли хранилище. + * + *

Вызывает {@link #size()} и сравнивает результат с нулём.

+ * + * @return {@code true}, если хранилище пусто; иначе {@code false} + */ @Override public boolean isEmpty() { boolean empty = size() == 0; @@ -150,6 +246,16 @@ public boolean isEmpty() { return empty; } + /** + * Проверяет наличие указанного ключа в хранилище. + * + *

Сериализует ключ и проверяет его наличие в множестве {@code keysSetKey} с помощью {@code SISMEMBER}.

+ *

Логирует входные данные, результат и задержку выполнения.

+ * + * @param key ключ для проверки + * @return {@code true}, если ключ присутствует; иначе {@code false} + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public boolean containsKey(Object key) { @SuppressWarnings("unchecked") @@ -166,6 +272,18 @@ public boolean containsKey(Object key) { } } + /** + * Проверяет наличие указанного значения в хранилище. + * + *

Сериализует значение и сравнивает его с каждым сохранённым значением + * по всем ключам из {@code keysSetKey}.

+ *

Использует Redis Pipeline для параллельного получения значений. + * Логирует размер множества, результат и задержку.

+ * + * @param value значение для проверки + * @return {@code true}, если значение найдено; иначе {@code false} + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public boolean containsValue(Object value) { @SuppressWarnings("unchecked") @@ -205,6 +323,17 @@ public boolean containsValue(Object value) { } } + /** + * Получает значение, связанное с указанным ключом. + * + *

Сериализует ключ, формирует Redis-ключ и извлекает значение из Redis. + * В случае отсутствия значения возвращает {@code null}. При наличии — десериализует + * и возвращает объект типа {@code V}. Логирует попадание/промах и задержку выполнения.

+ * + * @param o ключ, по которому производится поиск + * @return значение, связанное с ключом, либо {@code null}, если оно отсутствует + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public V get(Object o) { @SuppressWarnings("unchecked") @@ -229,6 +358,18 @@ public V get(Object o) { } } + /** + * Сохраняет значение по указанному ключу. + * + *

Сериализует ключ и значение, сохраняет их в Redis с помощью Pipeline. + * Добавляет ключ в множество {@code keysSetKey}. Возвращает предыдущее значение, + * если оно существовало, иначе {@code null}. Логирует типы, ключи и задержку.

+ * + * @param key ключ, по которому сохраняется значение + * @param value значение для сохранения + * @return предыдущее значение, если оно было, иначе {@code null} + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public V put(K key, V value) { String serializedKey = serializeKey(key); @@ -260,6 +401,16 @@ public V put(K key, V value) { } } + /** + * Удаляет значение, связанное с указанным ключом. + * + *

Сериализует ключ, удаляет соответствующий Redis-ключ и исключает его из множества {@code keysSetKey}. + * Возвращает предыдущее значение, если оно существовало, иначе {@code null}. Логирует удаление и задержку.

+ * + * @param key ключ, по которому производится удаление + * @return предыдущее значение, если оно было, иначе {@code null} + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public V remove(Object key) { @SuppressWarnings("unchecked") @@ -288,6 +439,15 @@ public V remove(Object key) { } } + /** + * Добавляет все пары ключ-значение из переданной {@code Map} в хранилище. + * + *

Сериализует каждый ключ и значение, сохраняет их в Redis и добавляет ключи в {@code keysSetKey}. + * Использует Pipeline для оптимизации операций. Логирует количество записей и задержку.

+ * + * @param another карта с данными для добавления + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public void putAll(Map another) { if (another.isEmpty()) { @@ -314,6 +474,16 @@ public void putAll(Map another) { } } + /** + * Очищает хранилище, удаляя все ключи и связанные значения. + * + *

Удаляет все Redis-ключи, соответствующие сериализованным ключам, + * и само множество {@code keysSetKey}. + * Использует Pipeline для пакетного удаления. + * Логирует количество операций и задержку.

+ * + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public void clear() { long t0 = System.nanoTime(); @@ -341,6 +511,15 @@ public void clear() { } } + /** + * Возвращает множество всех ключей, присутствующих в хранилище. + * + *

Десериализует все элементы множества {@code keysSetKey} + * и возвращает их как {@code Set}.

+ * + * @return неизменяемое множество ключей + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public Set keySet() { long t0 = System.nanoTime(); @@ -362,6 +541,14 @@ public Set keySet() { } } + /** + * Возвращает коллекцию всех значений, хранящихся в Redis. + * + *

Получает все значения по ключам из {@code keysSetKey}, десериализует их и возвращает как {@code List}.

+ * + * @return неизменяемая коллекция значений + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public Collection values() { long t0 = System.nanoTime(); @@ -400,6 +587,15 @@ public Collection values() { } } + /** + * Возвращает множество всех пар ключ-значение, хранящихся в Redis. + * + *

Получает все значения по ключам из {@code keysSetKey}, + * десериализует их и возвращает как {@code Set>}.

+ * + * @return неизменяемое множество пар ключ-значение + * @throws RuntimeException если операция Redis завершилась ошибкой + */ @Override public Set> entrySet() { long t0 = System.nanoTime(); From 36228e812212715dc8056bc3cc593dc20f1f79b2 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Fri, 31 Oct 2025 09:51:27 +0300 Subject: [PATCH 9/9] add entity (User.java) and example into Main.java --- src/main/java/org/redis/Main.java | 27 +++++++++++++- src/main/java/org/redis/model/User.java | 47 +++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/redis/model/User.java diff --git a/src/main/java/org/redis/Main.java b/src/main/java/org/redis/Main.java index 68b3db6..fa8b782 100644 --- a/src/main/java/org/redis/Main.java +++ b/src/main/java/org/redis/Main.java @@ -1,7 +1,32 @@ package org.redis; +import org.redis.model.User; +import org.redis.wrapper.RedisWrapperMap; +import redis.clients.jedis.JedisPool; + +import java.util.List; +import java.util.UUID; + public class Main { public static void main(String[] args) { - System.out.println("Hello world!"); + JedisPool jedis = new JedisPool("localhost", 6379); + + User u1 = new User("u1", "Mike", List.of(1, 2, 3, 3)); + User u2 = new User("u2", "Joe", List.of(1, 2, 3, 3)); + User u3 = new User("u3", "Julia", List.of(1, 2, 3, 3)); + UUID u1Id = UUID.randomUUID(); + UUID u2Id = UUID.randomUUID(); + UUID u3Id = UUID.randomUUID(); + + RedisWrapperMap map = new RedisWrapperMap<>(jedis, "users"); + + map.put(u1Id, u1); + map.put(u2Id, u2); + map.put(u3Id, u3); + + map.clear(); + + map.close(); + } } \ No newline at end of file diff --git a/src/main/java/org/redis/model/User.java b/src/main/java/org/redis/model/User.java new file mode 100644 index 0000000..98fb248 --- /dev/null +++ b/src/main/java/org/redis/model/User.java @@ -0,0 +1,47 @@ +package org.redis.model; + +import java.util.List; + +public class User { + private String id; + private String name; + private List score; + + public User() { + } + + public User(String id, String name, List score) { + this.id = id; + this.name = name; + this.score = score; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getScore() { + return score; + } + + public void setScore(List score) { + this.score = score; + } + + @Override + public String toString() { + return "User{id='" + id + "', name='" + name + "'}"; + } +} \ No newline at end of file