diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java b/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java index 239860bdc..0a00e3a45 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java +++ b/src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java @@ -1,6 +1,6 @@ package cn.iocoder.dashboard.framework.redis.config; -import cn.iocoder.dashboard.framework.redis.core.listener.AbstractMessageListener; +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; @@ -35,7 +35,7 @@ public class RedisConfig { @Bean public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory factory, - List> listeners) { + List> listeners) { // 创建 RedisMessageListenerContainer 对象 RedisMessageListenerContainer container = new RedisMessageListenerContainer(); // 设置 RedisConnection 工厂。 diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/core/listener/AbstractMessageListener.java b/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java similarity index 78% rename from src/main/java/cn/iocoder/dashboard/framework/redis/core/listener/AbstractMessageListener.java rename to src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java index 8c95aecb0..98443e491 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/redis/core/listener/AbstractMessageListener.java +++ b/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java @@ -1,7 +1,8 @@ -package cn.iocoder.dashboard.framework.redis.core.listener; +package cn.iocoder.dashboard.framework.redis.core.pubsub; import cn.hutool.core.util.ArrayUtil; import cn.iocoder.dashboard.util.json.JSONUtils; +import lombok.SneakyThrows; import org.springframework.data.redis.connection.Message; import org.springframework.data.redis.connection.MessageListener; import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl; @@ -15,15 +16,21 @@ import java.lang.reflect.Type; * * @author 芋道源码 */ -public abstract class AbstractMessageListener implements MessageListener { +public abstract class AbstractChannelMessageListener implements MessageListener { /** * 消息类型 */ private final Class messageType; + /** + * Redis Channel + */ + private final String channel; - protected AbstractMessageListener() { + @SneakyThrows + protected AbstractChannelMessageListener() { this.messageType = getMessageClass(); + this.channel = messageType.newInstance().getChannel(); } /** @@ -31,7 +38,9 @@ public abstract class AbstractMessageListener implements MessageListener { * * @return channel */ - public abstract String getChannel(); + public final String getChannel() { + return channel; + } @Override public final void onMessage(Message message, byte[] bytes) { @@ -56,7 +65,7 @@ public abstract class AbstractMessageListener implements MessageListener { Class targetClass = getClass(); while (targetClass.getSuperclass() != null) { // 如果不是 AbstractMessageListener 父类,继续向上查找 - if (targetClass.getSuperclass() != AbstractMessageListener.class) { + if (targetClass.getSuperclass() != AbstractChannelMessageListener.class) { targetClass = targetClass.getSuperclass(); continue; } diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java b/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java new file mode 100644 index 000000000..7df444750 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java @@ -0,0 +1,18 @@ +package cn.iocoder.dashboard.framework.redis.core.pubsub; + +import com.alibaba.fastjson.annotation.JSONField; + +/** + * Redis Channel Message 接口 + */ +public interface ChannelMessage { + + /** + * 获得 Redis Channel + * + * @return Channel + */ + @JSONField(serialize = false) // 必须序列化 + String getChannel(); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java b/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java new file mode 100644 index 000000000..2db401629 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java @@ -0,0 +1,24 @@ +package cn.iocoder.dashboard.framework.redis.core.util; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; +import cn.iocoder.dashboard.util.json.JSONUtils; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * Redis 消息工具类 + * + * @author 芋道源码 + */ +public class RedisMessageUtils { + + /** + * 发送 Redis 消息,基于 Redis pub/sub 实现 + * + * @param redisTemplate Redis 操作模板 + * @param message 消息 + */ + public static void sendChannelMessage(RedisTemplate redisTemplate, T message) { + redisTemplate.convertAndSend(message.getChannel(), JSONUtils.toJSONString(message)); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/SysAuthController.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/SysAuthController.java index 3669652e8..bc986410b 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/SysAuthController.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/SysAuthController.java @@ -63,7 +63,8 @@ public class SysAuthController { // 获得角色列表 List roleList = roleService.listRolesFromCache(getLoginUserRoleIds()); // 获得菜单列表 - List menuList = permissionService.listRoleMenusFromCache(getLoginUserRoleIds(), + List menuList = permissionService.listRoleMenusFromCache( + getLoginUserRoleIds(), // 注意,基于登陆的角色,因为后续的权限判断也是基于它 SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType(), MenuTypeEnum.BUTTON.getType()), SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); // 拼接结果返回 @@ -74,7 +75,8 @@ public class SysAuthController { @GetMapping("list-menus") public CommonResult> listMenus() { // 获得用户拥有的菜单列表 - List menuList = permissionService.listRoleMenusFromCache(getLoginUserRoleIds(), + List menuList = permissionService.listRoleMenusFromCache( + getLoginUserRoleIds(), // 注意,基于登陆的角色,因为后续的权限判断也是基于它 SetUtils.asSet(MenuTypeEnum.DIR.getType(), MenuTypeEnum.MENU.getType()), // 只要目录和菜单类型 SetUtils.asSet(CommonStatusEnum.ENABLE.getStatus())); // 只要开启的 // 转换成 Tree 结构返回 diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/dict/SysDictDataMapper.java b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/dict/SysDictDataMapper.java index cc6fee182..61426b224 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/dict/SysDictDataMapper.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/dict/SysDictDataMapper.java @@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import org.apache.ibatis.annotations.Mapper; +import java.util.Date; import java.util.List; import static com.baomidou.mybatisplus.core.metadata.OrderItem.asc; @@ -42,4 +43,10 @@ public interface SysDictDataMapper extends BaseMapper { .likeIfPresent("dict_type", reqVO.getDictType()) .eqIfPresent("status", reqVO.getStatus())); } + + default boolean selectExistsByUpdateTimeAfter(Date maxUpdateTime) { + return selectOne(new QueryWrapper().select("id") + .gt("update_time", maxUpdateTime).last("LIMIT 1")) != null; + } + } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/dict/SysDictDataRefreshConsumer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/dict/SysDictDataRefreshConsumer.java new file mode 100644 index 000000000..12bf134a3 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/dict/SysDictDataRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.modules.system.mq.consumer.dict; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; +import cn.iocoder.dashboard.modules.system.mq.message.dict.SysDictDataRefreshMessage; +import cn.iocoder.dashboard.modules.system.service.dict.SysDictDataService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SysDictDataRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SysDictDataRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private SysDictDataService dictDataService; + + @Override + public void onMessage(SysDictDataRefreshMessage message) { + log.info("[onMessage][收到 DictData 刷新消息]"); + dictDataService.initLocalCache(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/SysMenuRefreshConsumer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysMenuRefreshConsumer.java similarity index 56% rename from src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/SysMenuRefreshConsumer.java rename to src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysMenuRefreshConsumer.java index fd88df11c..36152424c 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/SysMenuRefreshConsumer.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysMenuRefreshConsumer.java @@ -1,6 +1,6 @@ -package cn.iocoder.dashboard.modules.system.mq.consumer; +package cn.iocoder.dashboard.modules.system.mq.consumer.permission; -import cn.iocoder.dashboard.framework.redis.core.listener.AbstractMessageListener; +import cn.iocoder.dashboard.framework.redis.core.pubsub.AbstractChannelMessageListener; import cn.iocoder.dashboard.modules.system.mq.message.permission.SysMenuRefreshMessage; import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService; import lombok.extern.slf4j.Slf4j; @@ -8,9 +8,14 @@ import org.springframework.stereotype.Component; import javax.annotation.Resource; +/** + * 针对 {@link SysMenuRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ @Component @Slf4j -public class SysMenuRefreshConsumer extends AbstractMessageListener { +public class SysMenuRefreshConsumer extends AbstractChannelMessageListener { @Resource private SysMenuService menuService; @@ -18,12 +23,7 @@ public class SysMenuRefreshConsumer extends AbstractMessageListener sort + */ private static final Comparator COMPARATOR_TYPE_AND_SORT = Comparator .comparing(SysDictDataDO::getDictType) .thenComparingInt(SysDictDataDO::getSort); + /** + * 定时执行 {@link #schedulePeriodicRefresh()} 的周期 + * 因为已经通过 Redis Pub/Sub 机制,所以频率不需要高 + */ + private static final long SCHEDULER_PERIOD = 5 * 60 * 1000L; + /** * 字典数据缓存,第二个 key 使用 label * @@ -50,6 +66,10 @@ public class SysDictDataServiceImpl implements SysDictDataService { * key2:字典值 value */ private ImmutableTable valueDictDataCache; + /** + * 缓存字典数据的最大更新时间,用于后续的增量轮询,判断是否有更新 + */ + private volatile Date maxUpdateTime; @Resource private SysDictTypeService dictTypeService; @@ -57,20 +77,56 @@ public class SysDictDataServiceImpl implements SysDictDataService { @Resource private SysDictDataMapper dictDataMapper; + @Resource + private SysDictDataProducer dictDataProducer; + @Override @PostConstruct - public void init() { - // 获得字典数据 - List list = this.listDictDatas(); + public void initLocalCache() { + // 获取字典数据列表,如果有更新 + List dataList = this.loadDictDataIfUpdate(maxUpdateTime); + if (CollUtil.isEmpty(dataList)) { + return; + } + // 构建缓存 ImmutableTable.Builder labelDictDataBuilder = ImmutableTable.builder(); ImmutableTable.Builder valueDictDataBuilder = ImmutableTable.builder(); - list.forEach(dictData -> { + dataList.forEach(dictData -> { labelDictDataBuilder.put(dictData.getDictType(), dictData.getLabel(), dictData); valueDictDataBuilder.put(dictData.getDictType(), dictData.getValue(), dictData); }); labelDictDataCache = labelDictDataBuilder.build(); valueDictDataCache = valueDictDataBuilder.build(); + assert dataList.size() > 0; // 断言,避免告警 + maxUpdateTime = dataList.stream().max(Comparator.comparing(BaseDO::getUpdateTime)).get().getUpdateTime(); + log.info("[init][缓存字典数据,数量为:{}]", dataList.size()); + } + + @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) + public void schedulePeriodicRefresh() { + initLocalCache(); + } + + /** + * 如果字典数据发生变化,从数据库中获取最新的全量字典数据。 + * 如果未发生变化,则返回空 + * + * @param maxUpdateTime 当前字典数据的最大更新时间 + * @return 字典数据列表 + */ + private List loadDictDataIfUpdate(Date maxUpdateTime) { + // 第一步,判断是否要更新。 + if (maxUpdateTime == null) { // 如果更新时间为空,说明 DB 一定有新数据 + log.info("[loadDictDataIfUpdate][首次加载全量字典数据]"); + } else { // 判断数据库中是否有更新的字典数据 + if (!dictDataMapper.selectExistsByUpdateTimeAfter(maxUpdateTime)) { + return null; + } + log.info("[loadDictDataIfUpdate][增量加载全量字典数据]"); + } + // 第二步,如果有更新,则从数据库加载所有字典数据 + return dictDataMapper.selectList(); } @Override @@ -104,6 +160,8 @@ public class SysDictDataServiceImpl implements SysDictDataService { // 插入字典类型 SysDictDataDO dictData = SysDictDataConvert.INSTANCE.convert(reqVO); dictDataMapper.insert(dictData); + // 发送消息 + dictDataProducer.sendMenuRefreshMessage(); return dictData.getId(); } @@ -114,6 +172,8 @@ public class SysDictDataServiceImpl implements SysDictDataService { // 更新字典类型 SysDictDataDO updateObj = SysDictDataConvert.INSTANCE.convert(reqVO); dictDataMapper.updateById(updateObj); + // 发送消息 + dictDataProducer.sendMenuRefreshMessage(); } @Override @@ -122,6 +182,8 @@ public class SysDictDataServiceImpl implements SysDictDataService { this.checkDictDataExists(id); // 删除字典数据 dictDataMapper.deleteById(id); + // 发送消息 + dictDataProducer.sendMenuRefreshMessage(); } @Override diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java index f0b999d84..42b91c8d5 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java @@ -16,9 +16,9 @@ import java.util.List; public interface SysMenuService { /** - * 初始化菜单 + * 初始化菜单的本地缓存 */ - void init(); + void initLocalCache(); /** * 获得所有菜单列表 diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java index 92d228faa..d28465057 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java @@ -77,8 +77,8 @@ public class SysMenuServiceImpl implements SysMenuService { */ @Override @PostConstruct - public synchronized void init() { - // 获取 + public synchronized void initLocalCache() { + // 获取菜单列表,如果有更新 List menuList = this.loadMenuIfUpdate(maxUpdateTime); if (CollUtil.isEmpty(menuList)) { return; @@ -100,7 +100,7 @@ public class SysMenuServiceImpl implements SysMenuService { @Scheduled(fixedDelay = SCHEDULER_PERIOD, initialDelay = SCHEDULER_PERIOD) public void schedulePeriodicRefresh() { - init(); + initLocalCache(); } /**