fix(cache): 解决缓存中JSON null值导致的空指针问题
- 添加对历史缓存中JSON "null" 字符串的兼容处理 - 当缓存解析出null值时清理缓存并回源数据库 - 在CmsWebsiteServiceImpl中增加缓存清理逻辑 - 在ShopWebsiteServiceImpl中统一缓存异常处理机制 - 添加单元测试验证JSON null值场景的正确回退行为
This commit is contained in:
@@ -337,9 +337,16 @@ public class CmsWebsiteServiceImpl extends ServiceImpl<CmsWebsiteMapper, CmsWebs
|
|||||||
if (StrUtil.isNotBlank(siteInfo)) {
|
if (StrUtil.isNotBlank(siteInfo)) {
|
||||||
log.info("从缓存获取网站信息,租户ID: {}", tenantId);
|
log.info("从缓存获取网站信息,租户ID: {}", tenantId);
|
||||||
try {
|
try {
|
||||||
return JSONUtil.parseObject(siteInfo, ShopVo.class);
|
// 兼容历史缓存:JSON "null" 会被解析为 null;此时应视为未命中并回源数据库。
|
||||||
|
ShopVo cacheVo = JSONUtil.parseObject(siteInfo, ShopVo.class);
|
||||||
|
if (cacheVo != null) {
|
||||||
|
return cacheVo;
|
||||||
|
}
|
||||||
|
log.warn("网站信息缓存命中但内容为空(null),清理缓存后回源数据库,租户ID: {}", tenantId);
|
||||||
|
redisUtil.delete(cacheKey);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("缓存解析失败,从数据库重新获取: {}", e.getMessage());
|
log.warn("缓存解析失败,清理缓存后从数据库重新获取: {}", e.getMessage());
|
||||||
|
redisUtil.delete(cacheKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ public class ShopWebsiteServiceImpl implements ShopWebsiteService {
|
|||||||
log.info("从缓存获取商城信息,租户ID: {}", tenantId);
|
log.info("从缓存获取商城信息,租户ID: {}", tenantId);
|
||||||
return cacheVo;
|
return cacheVo;
|
||||||
}
|
}
|
||||||
|
// 兼容历史缓存:JSON "null" 会被解析为 null;此时清理缓存并回源。
|
||||||
|
log.warn("商城信息缓存命中但内容为空(null),清理缓存后回源数据库,租户ID: {}", tenantId);
|
||||||
|
redisUtil.delete(cacheKey);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.warn("商城缓存解析失败,清理缓存后重新获取: {}", e.getMessage());
|
log.warn("商城缓存解析失败,清理缓存后重新获取: {}", e.getMessage());
|
||||||
redisUtil.delete(cacheKey);
|
redisUtil.delete(cacheKey);
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.gxwebsoft.cms.service.impl;
|
||||||
|
|
||||||
|
import com.gxwebsoft.cms.entity.CmsWebsite;
|
||||||
|
import com.gxwebsoft.cms.mapper.CmsWebsiteMapper;
|
||||||
|
import com.gxwebsoft.cms.service.CmsNavigationService;
|
||||||
|
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||||
|
import com.gxwebsoft.shop.vo.ShopVo;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.Collections;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class CmsWebsiteServiceImplCacheTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void getSiteInfo_cacheValueIsJsonNull_shouldFallbackToDb() throws Exception {
|
||||||
|
Integer tenantId = 1;
|
||||||
|
|
||||||
|
RedisUtil redisUtil = mock(RedisUtil.class);
|
||||||
|
CmsNavigationService cmsNavigationService = mock(CmsNavigationService.class);
|
||||||
|
CmsWebsiteMapper cmsWebsiteMapper = mock(CmsWebsiteMapper.class);
|
||||||
|
|
||||||
|
// 历史缓存可能存在字符串 "null";Jackson 解析后会得到 null,需回源 DB。
|
||||||
|
when(redisUtil.get("SiteInfo:" + tenantId)).thenReturn("null");
|
||||||
|
when(cmsNavigationService.listRel(any())).thenReturn(Collections.emptyList());
|
||||||
|
|
||||||
|
CmsWebsite website = new CmsWebsite();
|
||||||
|
website.setTenantId(tenantId);
|
||||||
|
website.setTenantName("tenant");
|
||||||
|
website.setWebsiteName("site");
|
||||||
|
when(cmsWebsiteMapper.getByTenantId(tenantId)).thenReturn(website);
|
||||||
|
|
||||||
|
CmsWebsiteServiceImpl service = new CmsWebsiteServiceImpl();
|
||||||
|
setField(service, "redisUtil", redisUtil);
|
||||||
|
setField(service, "cmsNavigationService", cmsNavigationService);
|
||||||
|
setField(service, "baseMapper", cmsWebsiteMapper);
|
||||||
|
|
||||||
|
ShopVo vo = service.getSiteInfo(tenantId);
|
||||||
|
|
||||||
|
assertNotNull(vo);
|
||||||
|
verify(redisUtil).delete("SiteInfo:" + tenantId);
|
||||||
|
verify(cmsWebsiteMapper).getByTenantId(tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setField(Object target, String fieldName, Object value) throws Exception {
|
||||||
|
Class<?> c = target.getClass();
|
||||||
|
while (c != null) {
|
||||||
|
try {
|
||||||
|
Field f = c.getDeclaredField(fieldName);
|
||||||
|
f.setAccessible(true);
|
||||||
|
f.set(target, value);
|
||||||
|
return;
|
||||||
|
} catch (NoSuchFieldException ignored) {
|
||||||
|
c = c.getSuperclass();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new NoSuchFieldException(fieldName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user