From c1b9daa56174ca97ce871205967c34a24156fc87 Mon Sep 17 00:00:00 2001 From: baiyu-jujuc <299335170+baiyu-jujuc@users.noreply.github.com> Date: Wed, 23 Sep 2026 14:01:57 +0800 Subject: [PATCH] fix: avoid NPE when previewing images with redis cache CacheServiceRedisImpl#getImgCache(String) passed the key straight to Redisson, which throws NullPointerException (map key can't be null) for a null key. Every single-image preview hits this path because compressFileKey is only set for images inside an archive, so with cache.type=redis the preview endpoint always returned HTTP 500. Return an empty list for empty keys, matching CacheServiceJDKImpl. Fixes #793 --- .../cache/impl/CacheServiceRedisImpl.java | 5 ++ .../impl/CacheServiceRedisImplTests.java | 65 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 server/src/test/java/cn/keking/service/cache/impl/CacheServiceRedisImplTests.java diff --git a/server/src/main/java/cn/keking/service/cache/impl/CacheServiceRedisImpl.java b/server/src/main/java/cn/keking/service/cache/impl/CacheServiceRedisImpl.java index 6ead0ef81..dfd17b037 100644 --- a/server/src/main/java/cn/keking/service/cache/impl/CacheServiceRedisImpl.java +++ b/server/src/main/java/cn/keking/service/cache/impl/CacheServiceRedisImpl.java @@ -1,12 +1,14 @@ package cn.keking.service.cache.impl; import cn.keking.service.cache.CacheService; +import org.apache.commons.lang3.StringUtils; import org.redisson.api.RBlockingQueue; import org.redisson.api.RMapCache; import org.redisson.api.RedissonClient; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.stereotype.Service; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -68,6 +70,9 @@ public Map> getImgCache() { @Override public List getImgCache(String key) { + if (StringUtils.isEmpty(key)) { + return new ArrayList<>(); + } RMapCache> convertedList = redissonClient.getMapCache(FILE_PREVIEW_IMGS_KEY); return convertedList.get(key); } diff --git a/server/src/test/java/cn/keking/service/cache/impl/CacheServiceRedisImplTests.java b/server/src/test/java/cn/keking/service/cache/impl/CacheServiceRedisImplTests.java new file mode 100644 index 000000000..baced32d7 --- /dev/null +++ b/server/src/test/java/cn/keking/service/cache/impl/CacheServiceRedisImplTests.java @@ -0,0 +1,65 @@ +package cn.keking.service.cache.impl; + +import cn.keking.service.cache.CacheService; +import org.junit.jupiter.api.Test; +import org.redisson.api.RMapCache; +import org.redisson.api.RedissonClient; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * CacheServiceRedisImpl 回归测试。 + * + * 背景:cache.type=redis 时预览单张图片,compressFileKey 为 null, + * 修复前会把 null 直接传给 Redisson,抛出 NullPointerException: map key can't be null。 + */ +class CacheServiceRedisImplTests { + + @Test + void shouldReturnEmptyListWhenKeyIsNull() { + RedissonClient redissonClient = mock(RedissonClient.class); + CacheServiceRedisImpl cacheService = new CacheServiceRedisImpl(redissonClient); + + List result = cacheService.getImgCache(null); + + assertNotNull(result); + assertTrue(result.isEmpty()); + // 判空后不应再访问 Redis —— 修复前这里会调用 getMapCache 并抛出 NPE + verifyNoInteractions(redissonClient); + } + + @Test + void shouldReturnEmptyListWhenKeyIsEmpty() { + RedissonClient redissonClient = mock(RedissonClient.class); + CacheServiceRedisImpl cacheService = new CacheServiceRedisImpl(redissonClient); + + List result = cacheService.getImgCache(""); + + assertNotNull(result); + assertTrue(result.isEmpty()); + verifyNoInteractions(redissonClient); + } + + @Test + void shouldReadFromRedisWhenKeyIsPresent() { + RedissonClient redissonClient = mock(RedissonClient.class); + @SuppressWarnings("unchecked") + RMapCache> mapCache = mock(RMapCache.class); + when(redissonClient.>getMapCache(CacheService.FILE_PREVIEW_IMGS_KEY)) + .thenReturn(mapCache); + when(mapCache.get("zip-key")).thenReturn(List.of("a.png", "b.png")); + + CacheServiceRedisImpl cacheService = new CacheServiceRedisImpl(redissonClient); + + List result = cacheService.getImgCache("zip-key"); + + assertEquals(List.of("a.png", "b.png"), result); + } +}