From dc42f0f1bb6378a48468bd74d7f4a89a060179fb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sat, 23 Jan 2021 10:56:34 +0800 Subject: [PATCH] 1. 进一步封装 Redis pub/sub 的能力,简化编程难度 2. 数据字典的本地缓存刷新 --- src/main/java/cn/iocoder/dashboard/framework/redis/config/RedisConfig.java | 4 ++-- src/main/java/cn/iocoder/dashboard/framework/redis/core/listener/AbstractMessageListener.java | 73 ------------------------------------------------------------------------- src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java | 82 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java | 18 ++++++++++++++++++ src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java | 24 ++++++++++++++++++++++++ src/main/java/cn/iocoder/dashboard/modules/system/controller/auth/SysAuthController.java | 6 ++++-- src/main/java/cn/iocoder/dashboard/modules/system/dal/mysql/dao/dict/SysDictDataMapper.java | 7 +++++++ src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/SysMenuRefreshConsumer.java | 29 ----------------------------- src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/dict/SysDictDataRefreshConsumer.java | 29 +++++++++++++++++++++++++++++ src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysMenuRefreshConsumer.java | 29 +++++++++++++++++++++++++++++ src/main/java/cn/iocoder/dashboard/modules/system/mq/message/dict/SysDictDataRefreshMessage.java | 17 +++++++++++++++++ src/main/java/cn/iocoder/dashboard/modules/system/mq/message/package-info.java | 1 - src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysMenuRefreshMessage.java | 8 ++++++-- src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/dict/SysDictDataProducer.java | 27 +++++++++++++++++++++++++++ src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/package-info.java | 1 - src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysMenuProducer.java | 4 ++-- src/main/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataService.java | 4 ++-- src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- src/main/java/cn/iocoder/dashboard/modules/system/service/permission/SysMenuService.java | 4 ++-- src/main/java/cn/iocoder/dashboard/modules/system/service/permission/impl/SysMenuServiceImpl.java | 6 +++--- 20 files changed, 320 insertions(+), 123 deletions(-) delete mode 100644 src/main/java/cn/iocoder/dashboard/framework/redis/core/listener/AbstractMessageListener.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/ChannelMessage.java create mode 100644 src/main/java/cn/iocoder/dashboard/framework/redis/core/util/RedisMessageUtils.java delete mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/SysMenuRefreshConsumer.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/dict/SysDictDataRefreshConsumer.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysMenuRefreshConsumer.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/mq/message/dict/SysDictDataRefreshMessage.java delete mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/mq/message/package-info.java create mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/dict/SysDictDataProducer.java delete mode 100644 src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/package-info.java 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 239860b..0a00e3a 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/listener/AbstractMessageListener.java deleted file mode 100644 index 8c95aec..0000000 --- a/src/main/java/cn/iocoder/dashboard/framework/redis/core/listener/AbstractMessageListener.java +++ /dev/null @@ -1,73 +0,0 @@ -package cn.iocoder.dashboard.framework.redis.core.listener; - -import cn.hutool.core.util.ArrayUtil; -import cn.iocoder.dashboard.util.json.JSONUtils; -import org.springframework.data.redis.connection.Message; -import org.springframework.data.redis.connection.MessageListener; -import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl; - -import java.lang.reflect.Type; - -/** - * Redis Pub/Sub 监听器抽象类,用于实现广播消费 - * - * @param 消息类型。一定要填写噢,不然会报错 - * - * @author 芋道源码 - */ -public abstract class AbstractMessageListener implements MessageListener { - - /** - * 消息类型 - */ - private final Class messageType; - - protected AbstractMessageListener() { - this.messageType = getMessageClass(); - } - - /** - * 获得 Sub 订阅的 Redis Channel 通道 - * - * @return channel - */ - public abstract String getChannel(); - - @Override - public final void onMessage(Message message, byte[] bytes) { - T messageObj = JSONUtils.parseObject(message.getBody(), messageType); - this.onMessage(messageObj); - } - - /** - * 处理消息 - * - * @param message 消息 - */ - public abstract void onMessage(T message); - - /** - * 通过解析类上的泛型,获得消息类型 - * - * @return 消息类型 - */ - @SuppressWarnings("unchecked") - private Class getMessageClass() { - Class targetClass = getClass(); - while (targetClass.getSuperclass() != null) { - // 如果不是 AbstractMessageListener 父类,继续向上查找 - if (targetClass.getSuperclass() != AbstractMessageListener.class) { - targetClass = targetClass.getSuperclass(); - continue; - } - // 如果是 AbstractMessageListener 父类,则解析泛型 - Type[] types = ((ParameterizedTypeImpl) targetClass.getGenericSuperclass()).getActualTypeArguments(); - if (ArrayUtil.isEmpty(types)) { - throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); - } - return (Class) types[0]; - } - throw new IllegalStateException(String.format("类型(%s) 找不到 AbstractMessageListener 父类", getClass().getName())); - } - -} diff --git a/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java b/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java new file mode 100644 index 0000000..98443e4 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/redis/core/pubsub/AbstractChannelMessageListener.java @@ -0,0 +1,82 @@ +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; + +import java.lang.reflect.Type; + +/** + * Redis Pub/Sub 监听器抽象类,用于实现广播消费 + * + * @param 消息类型。一定要填写噢,不然会报错 + * + * @author 芋道源码 + */ +public abstract class AbstractChannelMessageListener implements MessageListener { + + /** + * 消息类型 + */ + private final Class messageType; + /** + * Redis Channel + */ + private final String channel; + + @SneakyThrows + protected AbstractChannelMessageListener() { + this.messageType = getMessageClass(); + this.channel = messageType.newInstance().getChannel(); + } + + /** + * 获得 Sub 订阅的 Redis Channel 通道 + * + * @return channel + */ + public final String getChannel() { + return channel; + } + + @Override + public final void onMessage(Message message, byte[] bytes) { + T messageObj = JSONUtils.parseObject(message.getBody(), messageType); + this.onMessage(messageObj); + } + + /** + * 处理消息 + * + * @param message 消息 + */ + public abstract void onMessage(T message); + + /** + * 通过解析类上的泛型,获得消息类型 + * + * @return 消息类型 + */ + @SuppressWarnings("unchecked") + private Class getMessageClass() { + Class targetClass = getClass(); + while (targetClass.getSuperclass() != null) { + // 如果不是 AbstractMessageListener 父类,继续向上查找 + if (targetClass.getSuperclass() != AbstractChannelMessageListener.class) { + targetClass = targetClass.getSuperclass(); + continue; + } + // 如果是 AbstractMessageListener 父类,则解析泛型 + Type[] types = ((ParameterizedTypeImpl) targetClass.getGenericSuperclass()).getActualTypeArguments(); + if (ArrayUtil.isEmpty(types)) { + throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName())); + } + return (Class) types[0]; + } + throw new IllegalStateException(String.format("类型(%s) 找不到 AbstractMessageListener 父类", getClass().getName())); + } + +} 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 0000000..7df4447 --- /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 0000000..2db4016 --- /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 3669652..bc98641 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 cc6fee1..61426b2 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/SysMenuRefreshConsumer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/SysMenuRefreshConsumer.java deleted file mode 100644 index fd88df1..0000000 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/SysMenuRefreshConsumer.java +++ /dev/null @@ -1,29 +0,0 @@ -package cn.iocoder.dashboard.modules.system.mq.consumer; - -import cn.iocoder.dashboard.framework.redis.core.listener.AbstractMessageListener; -import cn.iocoder.dashboard.modules.system.mq.message.permission.SysMenuRefreshMessage; -import cn.iocoder.dashboard.modules.system.service.permission.SysMenuService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import javax.annotation.Resource; - -@Component -@Slf4j -public class SysMenuRefreshConsumer extends AbstractMessageListener { - - @Resource - private SysMenuService menuService; - - @Override - public void onMessage(SysMenuRefreshMessage message) { - log.info("[onMessage][收到 Menu 刷新消息]"); - menuService.init(); - } - - @Override - public String getChannel() { - return SysMenuRefreshMessage.TOPIC; - } - -} 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 0000000..12bf134 --- /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/permission/SysMenuRefreshConsumer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysMenuRefreshConsumer.java new file mode 100644 index 0000000..3615242 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/consumer/permission/SysMenuRefreshConsumer.java @@ -0,0 +1,29 @@ +package cn.iocoder.dashboard.modules.system.mq.consumer.permission; + +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; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * 针对 {@link SysMenuRefreshMessage} 的消费者 + * + * @author 芋道源码 + */ +@Component +@Slf4j +public class SysMenuRefreshConsumer extends AbstractChannelMessageListener { + + @Resource + private SysMenuService menuService; + + @Override + public void onMessage(SysMenuRefreshMessage message) { + log.info("[onMessage][收到 Menu 刷新消息]"); + menuService.initLocalCache(); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/dict/SysDictDataRefreshMessage.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/dict/SysDictDataRefreshMessage.java new file mode 100644 index 0000000..7fad277 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/dict/SysDictDataRefreshMessage.java @@ -0,0 +1,17 @@ +package cn.iocoder.dashboard.modules.system.mq.message.dict; + +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; +import lombok.Data; + +/** + * 字典数据数据刷新 Message + */ +@Data +public class SysDictDataRefreshMessage implements ChannelMessage { + + @Override + public String getChannel() { + return "system.dict-data.refresh"; + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/package-info.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/package-info.java deleted file mode 100644 index 1096439..0000000 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package cn.iocoder.dashboard.modules.system.mq.message; diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysMenuRefreshMessage.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysMenuRefreshMessage.java index 17c4ef8..159682a 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysMenuRefreshMessage.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/message/permission/SysMenuRefreshMessage.java @@ -1,13 +1,17 @@ package cn.iocoder.dashboard.modules.system.mq.message.permission; +import cn.iocoder.dashboard.framework.redis.core.pubsub.ChannelMessage; import lombok.Data; /** * 菜单数据刷新 Message */ @Data -public class SysMenuRefreshMessage { +public class SysMenuRefreshMessage implements ChannelMessage { - public static final String TOPIC = "system.menu.refresh"; + @Override + public String getChannel() { + return "system.menu.refresh"; + } } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/dict/SysDictDataProducer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/dict/SysDictDataProducer.java new file mode 100644 index 0000000..603db6b --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/dict/SysDictDataProducer.java @@ -0,0 +1,27 @@ +package cn.iocoder.dashboard.modules.system.mq.producer.dict; + +import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils; +import cn.iocoder.dashboard.modules.system.mq.message.dict.SysDictDataRefreshMessage; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Component; + +import javax.annotation.Resource; + +/** + * DictData 字典数据相关消息的 Producer + */ +@Component +public class SysDictDataProducer { + + @Resource + private StringRedisTemplate stringRedisTemplate; + + /** + * 发送 {@link SysDictDataRefreshMessage} 消息 + */ + public void sendMenuRefreshMessage() { + SysDictDataRefreshMessage message = new SysDictDataRefreshMessage(); + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/package-info.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/package-info.java deleted file mode 100644 index e16b5ca..0000000 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package cn.iocoder.dashboard.modules.system.mq.producer; diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysMenuProducer.java b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysMenuProducer.java index 240886d..6d664c7 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysMenuProducer.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/mq/producer/permission/SysMenuProducer.java @@ -1,7 +1,7 @@ package cn.iocoder.dashboard.modules.system.mq.producer.permission; +import cn.iocoder.dashboard.framework.redis.core.util.RedisMessageUtils; import cn.iocoder.dashboard.modules.system.mq.message.permission.SysMenuRefreshMessage; -import cn.iocoder.dashboard.util.json.JSONUtils; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @@ -21,7 +21,7 @@ public class SysMenuProducer { */ public void sendMenuRefreshMessage() { SysMenuRefreshMessage message = new SysMenuRefreshMessage(); - stringRedisTemplate.convertAndSend(SysMenuRefreshMessage.TOPIC, JSONUtils.toJSONString(message)); + RedisMessageUtils.sendChannelMessage(stringRedisTemplate, message); } } diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataService.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataService.java index 4b093be..83a5562 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataService.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/SysDictDataService.java @@ -18,9 +18,9 @@ import java.util.List; public interface SysDictDataService extends DictDataFrameworkService { /** - * 初始化,主要是初始化缓存 + * 初始化字典数据的本地缓存 */ - void init(); + void initLocalCache(); /** * 获得字典数据列表 diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java b/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java index 6e5bcfb..98d46f8 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/service/dict/impl/SysDictDataServiceImpl.java @@ -1,8 +1,10 @@ package cn.iocoder.dashboard.modules.system.service.dict.impl; +import cn.hutool.core.collection.CollUtil; import cn.iocoder.dashboard.common.enums.CommonStatusEnum; import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil; import cn.iocoder.dashboard.common.pojo.PageResult; +import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataCreateReqVO; import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataExportReqVO; import cn.iocoder.dashboard.modules.system.controller.dict.vo.data.SysDictDataPageReqVO; @@ -11,15 +13,19 @@ import cn.iocoder.dashboard.modules.system.convert.dict.SysDictDataConvert; import cn.iocoder.dashboard.modules.system.dal.mysql.dao.dict.SysDictDataMapper; import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dict.SysDictDataDO; import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.dict.SysDictTypeDO; +import cn.iocoder.dashboard.modules.system.mq.producer.dict.SysDictDataProducer; import cn.iocoder.dashboard.modules.system.service.dict.SysDictDataService; import cn.iocoder.dashboard.modules.system.service.dict.SysDictTypeService; import com.google.common.collect.ImmutableTable; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import javax.annotation.Resource; import java.util.ArrayList; import java.util.Comparator; +import java.util.Date; import java.util.List; import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; @@ -30,13 +36,23 @@ import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*; * @author ruoyi */ @Service +@Slf4j public class SysDictDataServiceImpl implements SysDictDataService { + /** + * 排序 dictType > 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 * * key1:字典类型 dictType @@ -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 f0b999d..42b91c8 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 92d228f..d284650 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(); } /** -- libgit2 0.21.4