Commit ae763f727fe1a4dc3c251d07002f2cc5b7f6f548
Committed by
Gitee
!749 增加 WebSocket 的全新支持
Merge pull request !749 from 芋道源码/feature/websocket
Showing
58 changed files
with
1687 additions
and
555 deletions
yudao-dependencies/pom.xml
| @@ -176,6 +176,12 @@ | @@ -176,6 +176,12 @@ | ||
| 176 | </dependency> | 176 | </dependency> |
| 177 | 177 | ||
| 178 | <dependency> | 178 | <dependency> |
| 179 | + <groupId>cn.iocoder.boot</groupId> | ||
| 180 | + <artifactId>yudao-spring-boot-starter-websocket</artifactId> | ||
| 181 | + <version>${revision}</version> | ||
| 182 | + </dependency> | ||
| 183 | + | ||
| 184 | + <dependency> | ||
| 179 | <groupId>com.github.xiaoymin</groupId> | 185 | <groupId>com.github.xiaoymin</groupId> |
| 180 | <artifactId>knife4j-openapi3-spring-boot-starter</artifactId> | 186 | <artifactId>knife4j-openapi3-spring-boot-starter</artifactId> |
| 181 | <version>${knife4j.version}</version> | 187 | <version>${knife4j.version}</version> |
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQAutoConfiguration.java renamed to yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQConsumerAutoConfiguration.java
| @@ -5,7 +5,6 @@ import cn.hutool.core.util.StrUtil; | @@ -5,7 +5,6 @@ import cn.hutool.core.util.StrUtil; | ||
| 5 | import cn.hutool.system.SystemUtil; | 5 | import cn.hutool.system.SystemUtil; |
| 6 | import cn.iocoder.yudao.framework.common.enums.DocumentEnum; | 6 | import cn.iocoder.yudao.framework.common.enums.DocumentEnum; |
| 7 | import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; | 7 | import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; |
| 8 | -import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor; | ||
| 9 | import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob; | 8 | import cn.iocoder.yudao.framework.mq.redis.core.job.RedisPendingMessageResendJob; |
| 10 | import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; | 9 | import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; |
| 11 | import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; | 10 | import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessageListener; |
| @@ -23,7 +22,6 @@ import org.springframework.data.redis.connection.stream.ReadOffset; | @@ -23,7 +22,6 @@ import org.springframework.data.redis.connection.stream.ReadOffset; | ||
| 23 | import org.springframework.data.redis.connection.stream.StreamOffset; | 22 | import org.springframework.data.redis.connection.stream.StreamOffset; |
| 24 | import org.springframework.data.redis.core.RedisCallback; | 23 | import org.springframework.data.redis.core.RedisCallback; |
| 25 | import org.springframework.data.redis.core.RedisTemplate; | 24 | import org.springframework.data.redis.core.RedisTemplate; |
| 26 | -import org.springframework.data.redis.core.StringRedisTemplate; | ||
| 27 | import org.springframework.data.redis.listener.ChannelTopic; | 25 | import org.springframework.data.redis.listener.ChannelTopic; |
| 28 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; | 26 | import org.springframework.data.redis.listener.RedisMessageListenerContainer; |
| 29 | import org.springframework.data.redis.stream.StreamMessageListenerContainer; | 27 | import org.springframework.data.redis.stream.StreamMessageListenerContainer; |
| @@ -33,30 +31,19 @@ import java.util.List; | @@ -33,30 +31,19 @@ import java.util.List; | ||
| 33 | import java.util.Properties; | 31 | import java.util.Properties; |
| 34 | 32 | ||
| 35 | /** | 33 | /** |
| 36 | - * 消息队列配置类 | 34 | + * Redis 消息队列 Consumer 配置类 |
| 37 | * | 35 | * |
| 38 | * @author 芋道源码 | 36 | * @author 芋道源码 |
| 39 | */ | 37 | */ |
| 40 | @Slf4j | 38 | @Slf4j |
| 41 | @EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息 | 39 | @EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息 |
| 42 | @AutoConfiguration(after = YudaoRedisAutoConfiguration.class) | 40 | @AutoConfiguration(after = YudaoRedisAutoConfiguration.class) |
| 43 | -public class YudaoRedisMQAutoConfiguration { | ||
| 44 | - | ||
| 45 | - @Bean | ||
| 46 | - public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate, | ||
| 47 | - List<RedisMessageInterceptor> interceptors) { | ||
| 48 | - RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate); | ||
| 49 | - // 添加拦截器 | ||
| 50 | - interceptors.forEach(redisMQTemplate::addInterceptor); | ||
| 51 | - return redisMQTemplate; | ||
| 52 | - } | ||
| 53 | - | ||
| 54 | - // ========== 消费者相关 ========== | 41 | +public class YudaoRedisMQConsumerAutoConfiguration { |
| 55 | 42 | ||
| 56 | /** | 43 | /** |
| 57 | * 创建 Redis Pub/Sub 广播消费的容器 | 44 | * 创建 Redis Pub/Sub 广播消费的容器 |
| 58 | */ | 45 | */ |
| 59 | - @Bean(initMethod = "start", destroyMethod = "stop") | 46 | + @Bean |
| 60 | @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听 | 47 | @ConditionalOnBean(AbstractRedisChannelMessageListener.class) // 只有 AbstractChannelMessageListener 存在的时候,才需要注册 Redis pubsub 监听 |
| 61 | public RedisMessageListenerContainer redisMessageListenerContainer( | 48 | public RedisMessageListenerContainer redisMessageListenerContainer( |
| 62 | RedisMQTemplate redisMQTemplate, List<AbstractRedisChannelMessageListener<?>> listeners) { | 49 | RedisMQTemplate redisMQTemplate, List<AbstractRedisChannelMessageListener<?>> listeners) { |
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/redis/config/YudaoRedisMQProducerAutoConfiguration.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.mq.redis.config; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; | ||
| 4 | +import cn.iocoder.yudao.framework.mq.redis.core.interceptor.RedisMessageInterceptor; | ||
| 5 | +import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | +import org.springframework.boot.autoconfigure.AutoConfiguration; | ||
| 8 | +import org.springframework.context.annotation.Bean; | ||
| 9 | +import org.springframework.data.redis.core.StringRedisTemplate; | ||
| 10 | + | ||
| 11 | +import java.util.List; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * Redis 消息队列 Producer 配置类 | ||
| 15 | + * | ||
| 16 | + * @author 芋道源码 | ||
| 17 | + */ | ||
| 18 | +@Slf4j | ||
| 19 | +@AutoConfiguration(after = YudaoRedisAutoConfiguration.class) | ||
| 20 | +public class YudaoRedisMQProducerAutoConfiguration { | ||
| 21 | + | ||
| 22 | + @Bean | ||
| 23 | + public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate, | ||
| 24 | + List<RedisMessageInterceptor> interceptors) { | ||
| 25 | + RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate); | ||
| 26 | + // 添加拦截器 | ||
| 27 | + interceptors.forEach(redisMQTemplate::addInterceptor); | ||
| 28 | + return redisMQTemplate; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | +} |
yudao-framework/yudao-spring-boot-starter-mq/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
| 1 | -cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQAutoConfiguration | 1 | +cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQProducerAutoConfiguration |
| 2 | +cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration | ||
| 2 | cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration | 3 | cn.iocoder.yudao.framework.mq.rabbitmq.config.YudaoRabbitMQAutoConfiguration |
| 3 | \ No newline at end of file | 4 | \ No newline at end of file |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/SecurityProperties.java
| @@ -19,6 +19,13 @@ public class SecurityProperties { | @@ -19,6 +19,13 @@ public class SecurityProperties { | ||
| 19 | */ | 19 | */ |
| 20 | @NotEmpty(message = "Token Header 不能为空") | 20 | @NotEmpty(message = "Token Header 不能为空") |
| 21 | private String tokenHeader = "Authorization"; | 21 | private String tokenHeader = "Authorization"; |
| 22 | + /** | ||
| 23 | + * HTTP 请求时,访问令牌的请求参数 | ||
| 24 | + * | ||
| 25 | + * 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接 | ||
| 26 | + */ | ||
| 27 | + @NotEmpty(message = "Token Parameter 不能为空") | ||
| 28 | + private String tokenParameter = "token"; | ||
| 22 | 29 | ||
| 23 | /** | 30 | /** |
| 24 | * mock 模式的开关 | 31 | * mock 模式的开关 |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/config/YudaoWebSecurityConfigurerAdapter.java
| @@ -129,8 +129,6 @@ public class YudaoWebSecurityConfigurerAdapter { | @@ -129,8 +129,6 @@ public class YudaoWebSecurityConfigurerAdapter { | ||
| 129 | .antMatchers(buildAppApi("/**")).permitAll() | 129 | .antMatchers(buildAppApi("/**")).permitAll() |
| 130 | // 1.5 验证码captcha 允许匿名访问 | 130 | // 1.5 验证码captcha 允许匿名访问 |
| 131 | .antMatchers("/captcha/get", "/captcha/check").permitAll() | 131 | .antMatchers("/captcha/get", "/captcha/check").permitAll() |
| 132 | - // 1.6 webSocket 允许匿名访问 | ||
| 133 | - .antMatchers("/websocket/message").permitAll() | ||
| 134 | // ②:每个项目的自定义规则 | 132 | // ②:每个项目的自定义规则 |
| 135 | .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 | 133 | .and().authorizeRequests(registry -> // 下面,循环设置自定义规则 |
| 136 | authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) | 134 | authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(registry))) |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/filter/TokenAuthenticationFilter.java
| @@ -41,7 +41,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { | @@ -41,7 +41,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { | ||
| 41 | @SuppressWarnings("NullableProblems") | 41 | @SuppressWarnings("NullableProblems") |
| 42 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) | 42 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) |
| 43 | throws ServletException, IOException { | 43 | throws ServletException, IOException { |
| 44 | - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); | 44 | + String token = SecurityFrameworkUtils.obtainAuthorization(request, |
| 45 | + securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); | ||
| 45 | if (StrUtil.isNotEmpty(token)) { | 46 | if (StrUtil.isNotEmpty(token)) { |
| 46 | Integer userType = WebFrameworkUtils.getLoginUserType(request); | 47 | Integer userType = WebFrameworkUtils.getLoginUserType(request); |
| 47 | try { | 48 | try { |
| @@ -74,7 +75,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { | @@ -74,7 +75,10 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter { | ||
| 74 | return null; | 75 | return null; |
| 75 | } | 76 | } |
| 76 | // 用户类型不匹配,无权限 | 77 | // 用户类型不匹配,无权限 |
| 77 | - if (ObjectUtil.notEqual(accessToken.getUserType(), userType)) { | 78 | + // 注意:只有 /admin-api/* 和 /app-api/* 有 userType,才需要比对用户类型 |
| 79 | + // TODO 芋艿:ws 要不要区分开? | ||
| 80 | + if (userType != null | ||
| 81 | + && ObjectUtil.notEqual(accessToken.getUserType(), userType)) { | ||
| 78 | throw new AccessDeniedException("错误的用户类型"); | 82 | throw new AccessDeniedException("错误的用户类型"); |
| 79 | } | 83 | } |
| 80 | // 构建登录用户 | 84 | // 构建登录用户 |
yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java
| 1 | package cn.iocoder.yudao.framework.security.core.util; | 1 | package cn.iocoder.yudao.framework.security.core.util; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 3 | import cn.iocoder.yudao.framework.security.core.LoginUser; | 4 | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| 4 | import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; | 5 | import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; |
| 5 | import org.springframework.lang.Nullable; | 6 | import org.springframework.lang.Nullable; |
| @@ -20,6 +21,9 @@ import java.util.Collections; | @@ -20,6 +21,9 @@ import java.util.Collections; | ||
| 20 | */ | 21 | */ |
| 21 | public class SecurityFrameworkUtils { | 22 | public class SecurityFrameworkUtils { |
| 22 | 23 | ||
| 24 | + /** | ||
| 25 | + * HEADER 认证头 value 的前缀 | ||
| 26 | + */ | ||
| 23 | public static final String AUTHORIZATION_BEARER = "Bearer"; | 27 | public static final String AUTHORIZATION_BEARER = "Bearer"; |
| 24 | 28 | ||
| 25 | private SecurityFrameworkUtils() {} | 29 | private SecurityFrameworkUtils() {} |
| @@ -28,19 +32,23 @@ public class SecurityFrameworkUtils { | @@ -28,19 +32,23 @@ public class SecurityFrameworkUtils { | ||
| 28 | * 从请求中,获得认证 Token | 32 | * 从请求中,获得认证 Token |
| 29 | * | 33 | * |
| 30 | * @param request 请求 | 34 | * @param request 请求 |
| 31 | - * @param header 认证 Token 对应的 Header 名字 | 35 | + * @param headerName 认证 Token 对应的 Header 名字 |
| 36 | + * @param parameterName 认证 Token 对应的 Parameter 名字 | ||
| 32 | * @return 认证 Token | 37 | * @return 认证 Token |
| 33 | */ | 38 | */ |
| 34 | - public static String obtainAuthorization(HttpServletRequest request, String header) { | ||
| 35 | - String authorization = request.getHeader(header); | ||
| 36 | - if (!StringUtils.hasText(authorization)) { | ||
| 37 | - return null; | 39 | + public static String obtainAuthorization(HttpServletRequest request, |
| 40 | + String headerName, String parameterName) { | ||
| 41 | + // 1. 获得 Token。优先级:Header > Parameter | ||
| 42 | + String token = request.getHeader(headerName); | ||
| 43 | + if (StrUtil.isEmpty(token)) { | ||
| 44 | + token = request.getParameter(parameterName); | ||
| 38 | } | 45 | } |
| 39 | - int index = authorization.indexOf(AUTHORIZATION_BEARER + " "); | ||
| 40 | - if (index == -1) { // 未找到 | 46 | + if (!StringUtils.hasText(token)) { |
| 41 | return null; | 47 | return null; |
| 42 | } | 48 | } |
| 43 | - return authorization.substring(index + 7).trim(); | 49 | + // 2. 去除 Token 中带的 Bearer |
| 50 | + int index = token.indexOf(AUTHORIZATION_BEARER + " "); | ||
| 51 | + return index >= 0 ? token.substring(index + 7).trim() : token; | ||
| 44 | } | 52 | } |
| 45 | 53 | ||
| 46 | /** | 54 | /** |
yudao-framework/yudao-spring-boot-starter-websocket/pom.xml
| @@ -12,26 +12,73 @@ | @@ -12,26 +12,73 @@ | ||
| 12 | <packaging>jar</packaging> | 12 | <packaging>jar</packaging> |
| 13 | 13 | ||
| 14 | <name>${project.artifactId}</name> | 14 | <name>${project.artifactId}</name> |
| 15 | - <description>WebSocket</description> | 15 | + <description>WebSocket 框架,支持多节点的广播</description> |
| 16 | <url>https://github.com/YunaiV/ruoyi-vue-pro</url> | 16 | <url>https://github.com/YunaiV/ruoyi-vue-pro</url> |
| 17 | 17 | ||
| 18 | 18 | ||
| 19 | <dependencies> | 19 | <dependencies> |
| 20 | - | ||
| 21 | <dependency> | 20 | <dependency> |
| 22 | <groupId>cn.iocoder.boot</groupId> | 21 | <groupId>cn.iocoder.boot</groupId> |
| 23 | <artifactId>yudao-common</artifactId> | 22 | <artifactId>yudao-common</artifactId> |
| 24 | </dependency> | 23 | </dependency> |
| 25 | 24 | ||
| 25 | + <!-- Web 相关 --> | ||
| 26 | <dependency> | 26 | <dependency> |
| 27 | + <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢? | ||
| 28 | + 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。 | ||
| 29 | + 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。 | ||
| 30 | + --> | ||
| 27 | <groupId>cn.iocoder.boot</groupId> | 31 | <groupId>cn.iocoder.boot</groupId> |
| 28 | <artifactId>yudao-spring-boot-starter-security</artifactId> | 32 | <artifactId>yudao-spring-boot-starter-security</artifactId> |
| 33 | + <scope>provided</scope> | ||
| 29 | </dependency> | 34 | </dependency> |
| 30 | 35 | ||
| 31 | <dependency> | 36 | <dependency> |
| 32 | <groupId>org.springframework.boot</groupId> | 37 | <groupId>org.springframework.boot</groupId> |
| 33 | <artifactId>spring-boot-starter-websocket</artifactId> | 38 | <artifactId>spring-boot-starter-websocket</artifactId> |
| 34 | </dependency> | 39 | </dependency> |
| 40 | + | ||
| 41 | + <!-- Web 相关 --> | ||
| 42 | + <dependency> | ||
| 43 | + <!-- 为什么是 websocket 依赖 security 呢?而不是 security 拓展 websocket 呢? | ||
| 44 | + 因为 websocket 和 LoginUser 当前登录的用户有一定的相关性,具体可见 WebSocketSessionManagerImpl 逻辑。 | ||
| 45 | + 如果让 security 拓展 websocket 的话,会导致 websocket 组件的封装很散,进而增大理解成本。 | ||
| 46 | + --> | ||
| 47 | + <groupId>cn.iocoder.boot</groupId> | ||
| 48 | + <artifactId>yudao-spring-boot-starter-security</artifactId> | ||
| 49 | + <scope>provided</scope> | ||
| 50 | + </dependency> | ||
| 51 | + | ||
| 52 | + <!-- 消息队列相关 --> | ||
| 53 | + <dependency> | ||
| 54 | + <groupId>cn.iocoder.boot</groupId> | ||
| 55 | + <artifactId>yudao-spring-boot-starter-mq</artifactId> | ||
| 56 | + </dependency> | ||
| 57 | + <dependency> | ||
| 58 | + <groupId>org.springframework.kafka</groupId> | ||
| 59 | + <artifactId>spring-kafka</artifactId> | ||
| 60 | + <optional>true</optional> | ||
| 61 | + </dependency> | ||
| 62 | + <dependency> | ||
| 63 | + <groupId>org.springframework.amqp</groupId> | ||
| 64 | + <artifactId>spring-rabbit</artifactId> | ||
| 65 | + <optional>true</optional> | ||
| 66 | + </dependency> | ||
| 67 | + <dependency> | ||
| 68 | + <groupId>org.apache.rocketmq</groupId> | ||
| 69 | + <artifactId>rocketmq-spring-boot-starter</artifactId> | ||
| 70 | + <optional>true</optional> | ||
| 71 | + </dependency> | ||
| 72 | + | ||
| 73 | + <!-- 业务组件 --> | ||
| 74 | + <dependency> | ||
| 75 | + <!-- 为什么要依赖 tenant 组件? | ||
| 76 | + 因为广播某个类型的用户时候,需要根据租户过滤下,避免广播到别的租户! | ||
| 77 | + --> | ||
| 78 | + <groupId>cn.iocoder.boot</groupId> | ||
| 79 | + <artifactId>yudao-spring-boot-starter-biz-tenant</artifactId> | ||
| 80 | + <scope>provided</scope> | ||
| 81 | + </dependency> | ||
| 35 | </dependencies> | 82 | </dependencies> |
| 36 | 83 | ||
| 37 | </project> | 84 | </project> |
| 38 | \ No newline at end of file | 85 | \ No newline at end of file |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java deleted
| 1 | -package cn.iocoder.yudao.framework.websocket.config; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor; | ||
| 4 | -import org.springframework.boot.context.properties.EnableConfigurationProperties; | ||
| 5 | -import org.springframework.context.annotation.Bean; | ||
| 6 | -import org.springframework.web.socket.server.HandshakeInterceptor; | ||
| 7 | - | ||
| 8 | -@EnableConfigurationProperties(WebSocketProperties.class) | ||
| 9 | -public class WebSocketHandlerConfig { | ||
| 10 | - @Bean | ||
| 11 | - public HandshakeInterceptor handshakeInterceptor() { | ||
| 12 | - return new UserHandshakeInterceptor(); | ||
| 13 | - } | ||
| 14 | -} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketProperties.java
| @@ -4,6 +4,9 @@ import lombok.Data; | @@ -4,6 +4,9 @@ import lombok.Data; | ||
| 4 | import org.springframework.boot.context.properties.ConfigurationProperties; | 4 | import org.springframework.boot.context.properties.ConfigurationProperties; |
| 5 | import org.springframework.validation.annotation.Validated; | 5 | import org.springframework.validation.annotation.Validated; |
| 6 | 6 | ||
| 7 | +import javax.validation.constraints.NotEmpty; | ||
| 8 | +import javax.validation.constraints.NotNull; | ||
| 9 | + | ||
| 7 | /** | 10 | /** |
| 8 | * WebSocket 配置项 | 11 | * WebSocket 配置项 |
| 9 | * | 12 | * |
| @@ -15,15 +18,17 @@ import org.springframework.validation.annotation.Validated; | @@ -15,15 +18,17 @@ import org.springframework.validation.annotation.Validated; | ||
| 15 | public class WebSocketProperties { | 18 | public class WebSocketProperties { |
| 16 | 19 | ||
| 17 | /** | 20 | /** |
| 18 | - * 路径 | ||
| 19 | - */ | ||
| 20 | - private String path = ""; | ||
| 21 | - /** | ||
| 22 | - * 默认最多允许同时在线用户数 | 21 | + * WebSocket 的连接路径 |
| 23 | */ | 22 | */ |
| 24 | - private int maxOnlineCount = 0; | 23 | + @NotEmpty(message = "WebSocket 的连接路径不能为空") |
| 24 | + private String path = "/ws"; | ||
| 25 | + | ||
| 25 | /** | 26 | /** |
| 26 | - * 是否保存session | 27 | + * 消息发送器的类型 |
| 28 | + * | ||
| 29 | + * 可选值:local、redis、rocketmq、kafka、rabbitmq | ||
| 27 | */ | 30 | */ |
| 28 | - private boolean sessionMap = true; | 31 | + @NotNull(message = "WebSocket 的消息发送者不能为空") |
| 32 | + private String senderType = "local"; | ||
| 33 | + | ||
| 29 | } | 34 | } |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/YudaoWebSocketAutoConfiguration.java
| 1 | package cn.iocoder.yudao.framework.websocket.config; | 1 | package cn.iocoder.yudao.framework.websocket.config; |
| 2 | 2 | ||
| 3 | +import cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration; | ||
| 4 | +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; | ||
| 5 | +import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler; | ||
| 6 | +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; | ||
| 7 | +import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor; | ||
| 8 | +import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageConsumer; | ||
| 9 | +import cn.iocoder.yudao.framework.websocket.core.sender.kafka.KafkaWebSocketMessageSender; | ||
| 10 | +import cn.iocoder.yudao.framework.websocket.core.sender.local.LocalWebSocketMessageSender; | ||
| 11 | +import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageConsumer; | ||
| 12 | +import cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq.RabbitMQWebSocketMessageSender; | ||
| 13 | +import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageConsumer; | ||
| 14 | +import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender; | ||
| 15 | +import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer; | ||
| 16 | +import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender; | ||
| 17 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator; | ||
| 18 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; | ||
| 19 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl; | ||
| 20 | +import org.apache.rocketmq.spring.core.RocketMQTemplate; | ||
| 21 | +import org.springframework.amqp.core.TopicExchange; | ||
| 22 | +import org.springframework.amqp.rabbit.core.RabbitTemplate; | ||
| 23 | +import org.springframework.beans.factory.annotation.Value; | ||
| 3 | import org.springframework.boot.autoconfigure.AutoConfiguration; | 24 | import org.springframework.boot.autoconfigure.AutoConfiguration; |
| 4 | -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; | ||
| 5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | 25 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; |
| 6 | import org.springframework.boot.context.properties.EnableConfigurationProperties; | 26 | import org.springframework.boot.context.properties.EnableConfigurationProperties; |
| 7 | import org.springframework.context.annotation.Bean; | 27 | import org.springframework.context.annotation.Bean; |
| 28 | +import org.springframework.context.annotation.Configuration; | ||
| 29 | +import org.springframework.kafka.core.KafkaTemplate; | ||
| 8 | import org.springframework.web.socket.WebSocketHandler; | 30 | import org.springframework.web.socket.WebSocketHandler; |
| 31 | +import org.springframework.web.socket.config.annotation.EnableWebSocket; | ||
| 9 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; | 32 | import org.springframework.web.socket.config.annotation.WebSocketConfigurer; |
| 10 | import org.springframework.web.socket.server.HandshakeInterceptor; | 33 | import org.springframework.web.socket.server.HandshakeInterceptor; |
| 11 | 34 | ||
| @@ -16,19 +39,139 @@ import java.util.List; | @@ -16,19 +39,139 @@ import java.util.List; | ||
| 16 | * | 39 | * |
| 17 | * @author xingyu4j | 40 | * @author xingyu4j |
| 18 | */ | 41 | */ |
| 19 | -@AutoConfiguration | ||
| 20 | -// 允许使用 yudao.websocket.enable=false 禁用websocket | ||
| 21 | -@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) | 42 | +@AutoConfiguration(before = YudaoRedisMQConsumerAutoConfiguration.class) // before YudaoRedisMQConsumerAutoConfiguration 的原因是,需要保证 RedisWebSocketMessageConsumer 先创建,才能创建 RedisMessageListenerContainer |
| 43 | +@EnableWebSocket // 开启 websocket | ||
| 44 | +@ConditionalOnProperty(prefix = "yudao.websocket", value = "enable", matchIfMissing = true) // 允许使用 yudao.websocket.enable=false 禁用 websocket | ||
| 22 | @EnableConfigurationProperties(WebSocketProperties.class) | 45 | @EnableConfigurationProperties(WebSocketProperties.class) |
| 23 | public class YudaoWebSocketAutoConfiguration { | 46 | public class YudaoWebSocketAutoConfiguration { |
| 47 | + | ||
| 24 | @Bean | 48 | @Bean |
| 25 | - @ConditionalOnMissingBean | ||
| 26 | - public WebSocketConfigurer webSocketConfigurer(List<HandshakeInterceptor> handshakeInterceptor, | 49 | + public WebSocketConfigurer webSocketConfigurer(HandshakeInterceptor[] handshakeInterceptors, |
| 27 | WebSocketHandler webSocketHandler, | 50 | WebSocketHandler webSocketHandler, |
| 28 | WebSocketProperties webSocketProperties) { | 51 | WebSocketProperties webSocketProperties) { |
| 29 | - | ||
| 30 | return registry -> registry | 52 | return registry -> registry |
| 53 | + // 添加 WebSocketHandler | ||
| 31 | .addHandler(webSocketHandler, webSocketProperties.getPath()) | 54 | .addHandler(webSocketHandler, webSocketProperties.getPath()) |
| 32 | - .addInterceptors(handshakeInterceptor.toArray(new HandshakeInterceptor[0])); | 55 | + .addInterceptors(handshakeInterceptors) |
| 56 | + // 允许跨域,否则前端连接会直接断开 | ||
| 57 | + .setAllowedOriginPatterns("*"); | ||
| 58 | + } | ||
| 59 | + | ||
| 60 | + @Bean | ||
| 61 | + public HandshakeInterceptor handshakeInterceptor() { | ||
| 62 | + return new LoginUserHandshakeInterceptor(); | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | + @Bean | ||
| 66 | + public WebSocketHandler webSocketHandler(WebSocketSessionManager sessionManager, | ||
| 67 | + List<? extends WebSocketMessageListener<?>> messageListeners) { | ||
| 68 | + // 1. 创建 JsonWebSocketMessageHandler 对象,处理消息 | ||
| 69 | + JsonWebSocketMessageHandler messageHandler = new JsonWebSocketMessageHandler(messageListeners); | ||
| 70 | + // 2. 创建 WebSocketSessionHandlerDecorator 对象,处理连接 | ||
| 71 | + return new WebSocketSessionHandlerDecorator(messageHandler, sessionManager); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + @Bean | ||
| 75 | + public WebSocketSessionManager webSocketSessionManager() { | ||
| 76 | + return new WebSocketSessionManagerImpl(); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + // ==================== Sender 相关 ==================== | ||
| 80 | + | ||
| 81 | + @Configuration | ||
| 82 | + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true) | ||
| 83 | + public class LocalWebSocketMessageSenderConfiguration { | ||
| 84 | + | ||
| 85 | + @Bean | ||
| 86 | + public LocalWebSocketMessageSender localWebSocketMessageSender(WebSocketSessionManager sessionManager) { | ||
| 87 | + return new LocalWebSocketMessageSender(sessionManager); | ||
| 88 | + } | ||
| 89 | + | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + @Configuration | ||
| 93 | + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true) | ||
| 94 | + public class RedisWebSocketMessageSenderConfiguration { | ||
| 95 | + | ||
| 96 | + @Bean | ||
| 97 | + public RedisWebSocketMessageSender redisWebSocketMessageSender(WebSocketSessionManager sessionManager, | ||
| 98 | + RedisMQTemplate redisMQTemplate) { | ||
| 99 | + return new RedisWebSocketMessageSender(sessionManager, redisMQTemplate); | ||
| 100 | + } | ||
| 101 | + | ||
| 102 | + @Bean | ||
| 103 | + public RedisWebSocketMessageConsumer redisWebSocketMessageConsumer( | ||
| 104 | + RedisWebSocketMessageSender redisWebSocketMessageSender) { | ||
| 105 | + return new RedisWebSocketMessageConsumer(redisWebSocketMessageSender); | ||
| 106 | + } | ||
| 107 | + | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + @Configuration | ||
| 111 | + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true) | ||
| 112 | + public class RocketMQWebSocketMessageSenderConfiguration { | ||
| 113 | + | ||
| 114 | + @Bean | ||
| 115 | + public RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender( | ||
| 116 | + WebSocketSessionManager sessionManager, RocketMQTemplate rocketMQTemplate, | ||
| 117 | + @Value("${yudao.websocket.sender-rocketmq.topic}") String topic) { | ||
| 118 | + return new RocketMQWebSocketMessageSender(sessionManager, rocketMQTemplate, topic); | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + @Bean | ||
| 122 | + public RocketMQWebSocketMessageConsumer rocketMQWebSocketMessageConsumer( | ||
| 123 | + RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender) { | ||
| 124 | + return new RocketMQWebSocketMessageConsumer(rocketMQWebSocketMessageSender); | ||
| 125 | + } | ||
| 126 | + | ||
| 33 | } | 127 | } |
| 34 | -} | 128 | + |
| 129 | + @Configuration | ||
| 130 | + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true) | ||
| 131 | + public class RabbitMQWebSocketMessageSenderConfiguration { | ||
| 132 | + | ||
| 133 | + @Bean | ||
| 134 | + public RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender( | ||
| 135 | + WebSocketSessionManager sessionManager, RabbitTemplate rabbitTemplate, | ||
| 136 | + TopicExchange websocketTopicExchange) { | ||
| 137 | + return new RabbitMQWebSocketMessageSender(sessionManager, rabbitTemplate, websocketTopicExchange); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + @Bean | ||
| 141 | + public RabbitMQWebSocketMessageConsumer rabbitMQWebSocketMessageConsumer( | ||
| 142 | + RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender) { | ||
| 143 | + return new RabbitMQWebSocketMessageConsumer(rabbitMQWebSocketMessageSender); | ||
| 144 | + } | ||
| 145 | + | ||
| 146 | + /** | ||
| 147 | + * 创建 Topic Exchange | ||
| 148 | + */ | ||
| 149 | + @Bean | ||
| 150 | + public TopicExchange websocketTopicExchange(@Value("${yudao.websocket.sender-rabbitmq.exchange}") String exchange) { | ||
| 151 | + return new TopicExchange(exchange, | ||
| 152 | + true, // durable: 是否持久化 | ||
| 153 | + false); // exclusive: 是否排它 | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + @Configuration | ||
| 159 | + @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true) | ||
| 160 | + public class KafkaWebSocketMessageSenderConfiguration { | ||
| 161 | + | ||
| 162 | + @Bean | ||
| 163 | + public KafkaWebSocketMessageSender kafkaWebSocketMessageSender( | ||
| 164 | + WebSocketSessionManager sessionManager, KafkaTemplate<Object, Object> kafkaTemplate, | ||
| 165 | + @Value("${yudao.websocket.sender-kafka.topic}") String topic) { | ||
| 166 | + return new KafkaWebSocketMessageSender(sessionManager, kafkaTemplate, topic); | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + @Bean | ||
| 170 | + public KafkaWebSocketMessageConsumer kafkaWebSocketMessageConsumer( | ||
| 171 | + KafkaWebSocketMessageSender kafkaWebSocketMessageSender) { | ||
| 172 | + return new KafkaWebSocketMessageConsumer(kafkaWebSocketMessageSender); | ||
| 173 | + } | ||
| 174 | + | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | +} | ||
| 35 | \ No newline at end of file | 178 | \ No newline at end of file |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java deleted
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java deleted
| 1 | -package cn.iocoder.yudao.framework.websocket.core; | ||
| 2 | - | ||
| 3 | -import lombok.Data; | ||
| 4 | -import lombok.experimental.Accessors; | ||
| 5 | - | ||
| 6 | -import java.util.List; | ||
| 7 | - | ||
| 8 | -@Data | ||
| 9 | -@Accessors(chain = true) | ||
| 10 | -public class WebSocketMessageDO { | ||
| 11 | - /** | ||
| 12 | - * 接收消息的seesion | ||
| 13 | - */ | ||
| 14 | - private List<Object> seesionKeyList; | ||
| 15 | - /** | ||
| 16 | - * 发送消息 | ||
| 17 | - */ | ||
| 18 | - private String msgText; | ||
| 19 | - | ||
| 20 | - public static WebSocketMessageDO build(List<Object> seesionKeyList, String msgText) { | ||
| 21 | - return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList); | ||
| 22 | - } | ||
| 23 | - | ||
| 24 | -} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java deleted
| 1 | -package cn.iocoder.yudao.framework.websocket.core; | ||
| 2 | - | ||
| 3 | -import org.springframework.web.socket.WebSocketSession; | ||
| 4 | - | ||
| 5 | -import java.util.Collection; | ||
| 6 | -import java.util.Map; | ||
| 7 | -import java.util.Set; | ||
| 8 | -import java.util.concurrent.ConcurrentHashMap; | ||
| 9 | - | ||
| 10 | -public final class WebSocketSessionHandler { | ||
| 11 | - private WebSocketSessionHandler() { | ||
| 12 | - } | ||
| 13 | - | ||
| 14 | - private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>(); | ||
| 15 | - | ||
| 16 | - public static void addSession(Object sessionKey, WebSocketSession session) { | ||
| 17 | - SESSION_MAP.put(sessionKey.toString(), session); | ||
| 18 | - } | ||
| 19 | - | ||
| 20 | - public static void removeSession(Object sessionKey) { | ||
| 21 | - SESSION_MAP.remove(sessionKey.toString()); | ||
| 22 | - } | ||
| 23 | - | ||
| 24 | - public static WebSocketSession getSession(Object sessionKey) { | ||
| 25 | - return SESSION_MAP.get(sessionKey.toString()); | ||
| 26 | - } | ||
| 27 | - | ||
| 28 | - public static Collection<WebSocketSession> getSessions() { | ||
| 29 | - return SESSION_MAP.values(); | ||
| 30 | - } | ||
| 31 | - | ||
| 32 | - public static Set<String> getSessionKeys() { | ||
| 33 | - return SESSION_MAP.keySet(); | ||
| 34 | - } | ||
| 35 | - | ||
| 36 | -} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java deleted
| 1 | -package cn.iocoder.yudao.framework.websocket.core; | ||
| 2 | - | ||
| 3 | -import lombok.extern.slf4j.Slf4j; | ||
| 4 | -import org.springframework.web.socket.TextMessage; | ||
| 5 | -import org.springframework.web.socket.WebSocketSession; | ||
| 6 | - | ||
| 7 | -import java.io.IOException; | ||
| 8 | - | ||
| 9 | -@Slf4j | ||
| 10 | -public class WebSocketUtils { | ||
| 11 | - public static boolean sendMessage(WebSocketSession seesion, String message) { | ||
| 12 | - if (seesion == null) { | ||
| 13 | - log.error("seesion 不存在"); | ||
| 14 | - return false; | ||
| 15 | - } | ||
| 16 | - if (seesion.isOpen()) { | ||
| 17 | - try { | ||
| 18 | - seesion.sendMessage(new TextMessage(message)); | ||
| 19 | - } catch (IOException e) { | ||
| 20 | - log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e); | ||
| 21 | - return false; | ||
| 22 | - } | ||
| 23 | - } | ||
| 24 | - return true; | ||
| 25 | - } | ||
| 26 | - | ||
| 27 | - public static boolean sendMessage(Object sessionKey, String message) { | ||
| 28 | - WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey); | ||
| 29 | - return sendMessage(session, message); | ||
| 30 | - } | ||
| 31 | -} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java deleted
| 1 | -package cn.iocoder.yudao.framework.websocket.core; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.security.core.LoginUser; | ||
| 4 | -import org.springframework.web.socket.CloseStatus; | ||
| 5 | -import org.springframework.web.socket.WebSocketHandler; | ||
| 6 | -import org.springframework.web.socket.WebSocketSession; | ||
| 7 | -import org.springframework.web.socket.handler.WebSocketHandlerDecorator; | ||
| 8 | - | ||
| 9 | -public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator { | ||
| 10 | - public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) { | ||
| 11 | - super(delegate); | ||
| 12 | - } | ||
| 13 | - | ||
| 14 | - /** | ||
| 15 | - * websocket 连接时执行的动作 | ||
| 16 | - * @param session websocket session 对象 | ||
| 17 | - * @throws Exception 异常对象 | ||
| 18 | - */ | ||
| 19 | - @Override | ||
| 20 | - public void afterConnectionEstablished(final WebSocketSession session) throws Exception { | ||
| 21 | - Object sessionKey = sessionKeyGen(session); | ||
| 22 | - WebSocketSessionHandler.addSession(sessionKey, session); | ||
| 23 | - } | ||
| 24 | - | ||
| 25 | - /** | ||
| 26 | - * websocket 关闭连接时执行的动作 | ||
| 27 | - * @param session websocket session 对象 | ||
| 28 | - * @param closeStatus 关闭状态对象 | ||
| 29 | - * @throws Exception 异常对象 | ||
| 30 | - */ | ||
| 31 | - @Override | ||
| 32 | - public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception { | ||
| 33 | - Object sessionKey = sessionKeyGen(session); | ||
| 34 | - WebSocketSessionHandler.removeSession(sessionKey); | ||
| 35 | - } | ||
| 36 | - | ||
| 37 | - public Object sessionKeyGen(WebSocketSession webSocketSession) { | ||
| 38 | - | ||
| 39 | - Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER); | ||
| 40 | - | ||
| 41 | - if (obj instanceof LoginUser) { | ||
| 42 | - LoginUser loginUser = (LoginUser) obj; | ||
| 43 | - // userId 作为唯一区分 | ||
| 44 | - return String.valueOf(loginUser.getId()); | ||
| 45 | - } | ||
| 46 | - | ||
| 47 | - return null; | ||
| 48 | - } | ||
| 49 | -} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/handler/JsonWebSocketMessageHandler.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.handler; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.util.StrUtil; | ||
| 4 | +import cn.hutool.core.util.TypeUtil; | ||
| 5 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 6 | +import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; | ||
| 7 | +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; | ||
| 8 | +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage; | ||
| 9 | +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; | ||
| 10 | +import lombok.extern.slf4j.Slf4j; | ||
| 11 | +import org.springframework.web.socket.TextMessage; | ||
| 12 | +import org.springframework.web.socket.WebSocketHandler; | ||
| 13 | +import org.springframework.web.socket.WebSocketSession; | ||
| 14 | +import org.springframework.web.socket.handler.TextWebSocketHandler; | ||
| 15 | + | ||
| 16 | +import java.lang.reflect.Type; | ||
| 17 | +import java.util.HashMap; | ||
| 18 | +import java.util.List; | ||
| 19 | +import java.util.Map; | ||
| 20 | +import java.util.Objects; | ||
| 21 | +import java.util.function.Consumer; | ||
| 22 | + | ||
| 23 | +/** | ||
| 24 | + * JSON 格式 {@link WebSocketHandler} 实现类 | ||
| 25 | + * | ||
| 26 | + * 基于 {@link JsonWebSocketMessage#getType()} 消息类型,调度到对应的 {@link WebSocketMessageListener} 监听器。 | ||
| 27 | + * | ||
| 28 | + * @author 芋道源码 | ||
| 29 | + */ | ||
| 30 | +@Slf4j | ||
| 31 | +public class JsonWebSocketMessageHandler extends TextWebSocketHandler { | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * type 与 WebSocketMessageListener 的映射 | ||
| 35 | + */ | ||
| 36 | + private final Map<String, WebSocketMessageListener<Object>> listeners = new HashMap<>(); | ||
| 37 | + | ||
| 38 | + @SuppressWarnings({"rawtypes", "unchecked"}) | ||
| 39 | + public JsonWebSocketMessageHandler(List<? extends WebSocketMessageListener> listenersList) { | ||
| 40 | + listenersList.forEach((Consumer<WebSocketMessageListener>) | ||
| 41 | + listener -> listeners.put(listener.getType(), listener)); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + @Override | ||
| 45 | + protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { | ||
| 46 | + // 1.1 空消息,跳过 | ||
| 47 | + if (message.getPayloadLength() == 0) { | ||
| 48 | + return; | ||
| 49 | + } | ||
| 50 | + // 1.2 ping 心跳消息,直接返回 pong 消息。 | ||
| 51 | + if (message.getPayloadLength() == 4 && Objects.equals(message.getPayload(), "ping")) { | ||
| 52 | + session.sendMessage(new TextMessage("pong")); | ||
| 53 | + return; | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + // 2.1 解析消息 | ||
| 57 | + try { | ||
| 58 | + JsonWebSocketMessage jsonMessage = JsonUtils.parseObject(message.getPayload(), JsonWebSocketMessage.class); | ||
| 59 | + if (jsonMessage == null) { | ||
| 60 | + log.error("[handleTextMessage][session({}) message({}) 解析为空]", session.getId(), message.getPayload()); | ||
| 61 | + return; | ||
| 62 | + } | ||
| 63 | + if (StrUtil.isEmpty(jsonMessage.getType())) { | ||
| 64 | + log.error("[handleTextMessage][session({}) message({}) 类型为空]", session.getId(), message.getPayload()); | ||
| 65 | + return; | ||
| 66 | + } | ||
| 67 | + // 2.2 获得对应的 WebSocketMessageListener | ||
| 68 | + WebSocketMessageListener<Object> messageListener = listeners.get(jsonMessage.getType()); | ||
| 69 | + if (messageListener == null) { | ||
| 70 | + log.error("[handleTextMessage][session({}) message({}) 监听器为空]", session.getId(), message.getPayload()); | ||
| 71 | + return; | ||
| 72 | + } | ||
| 73 | + // 2.3 处理消息 | ||
| 74 | + Type type = TypeUtil.getTypeArgument(messageListener.getClass(), 0); | ||
| 75 | + Object messageObj = JsonUtils.parseObject(jsonMessage.getContent(), type); | ||
| 76 | + Long tenantId = WebSocketFrameworkUtils.getTenantId(session); | ||
| 77 | + TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj)); | ||
| 78 | + } catch (Throwable ex) { | ||
| 79 | + log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload()); | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/listener/WebSocketMessageListener.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.listener; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage; | ||
| 4 | +import org.springframework.web.socket.WebSocketSession; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * WebSocket 消息监听器接口 | ||
| 8 | + * | ||
| 9 | + * 目的:前端发送消息给后端后,处理对应 {@link #getType()} 类型的消息 | ||
| 10 | + * | ||
| 11 | + * @param <T> 泛型,消息类型 | ||
| 12 | + */ | ||
| 13 | +public interface WebSocketMessageListener<T> { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * 处理消息 | ||
| 17 | + * | ||
| 18 | + * @param session Session | ||
| 19 | + * @param message 消息 | ||
| 20 | + */ | ||
| 21 | + void onMessage(WebSocketSession session, T message); | ||
| 22 | + | ||
| 23 | + /** | ||
| 24 | + * 获得消息类型 | ||
| 25 | + * | ||
| 26 | + * @see JsonWebSocketMessage#getType() | ||
| 27 | + * @return 消息类型 | ||
| 28 | + */ | ||
| 29 | + String getType(); | ||
| 30 | + | ||
| 31 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/message/JsonWebSocketMessage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +import java.io.Serializable; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * JSON 格式的 WebSocket 消息帧 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +@Data | ||
| 14 | +public class JsonWebSocketMessage implements Serializable { | ||
| 15 | + | ||
| 16 | + /** | ||
| 17 | + * 消息类型 | ||
| 18 | + * | ||
| 19 | + * 目的:用于分发到对应的 {@link WebSocketMessageListener} 实现类 | ||
| 20 | + */ | ||
| 21 | + private String type; | ||
| 22 | + /** | ||
| 23 | + * 消息内容 | ||
| 24 | + * | ||
| 25 | + * 要求 JSON 对象 | ||
| 26 | + */ | ||
| 27 | + private String content; | ||
| 28 | + | ||
| 29 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java renamed to yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/LoginUserHandshakeInterceptor.java
| 1 | -package cn.iocoder.yudao.framework.websocket.core; | 1 | +package cn.iocoder.yudao.framework.websocket.core.security; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.framework.security.core.LoginUser; | 3 | import cn.iocoder.yudao.framework.security.core.LoginUser; |
| 4 | +import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; | ||
| 4 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; | 5 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| 6 | +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; | ||
| 5 | import org.springframework.http.server.ServerHttpRequest; | 7 | import org.springframework.http.server.ServerHttpRequest; |
| 6 | import org.springframework.http.server.ServerHttpResponse; | 8 | import org.springframework.http.server.ServerHttpResponse; |
| 7 | import org.springframework.web.socket.WebSocketHandler; | 9 | import org.springframework.web.socket.WebSocketHandler; |
| 10 | +import org.springframework.web.socket.WebSocketSession; | ||
| 8 | import org.springframework.web.socket.server.HandshakeInterceptor; | 11 | import org.springframework.web.socket.server.HandshakeInterceptor; |
| 9 | 12 | ||
| 10 | import java.util.Map; | 13 | import java.util.Map; |
| 11 | 14 | ||
| 12 | -public class UserHandshakeInterceptor implements HandshakeInterceptor { | 15 | +/** |
| 16 | + * 登录用户的 {@link HandshakeInterceptor} 实现类 | ||
| 17 | + * | ||
| 18 | + * 流程如下: | ||
| 19 | + * 1. 前端连接 websocket 时,会通过拼接 ?token={token} 到 ws:// 连接后,这样它可以被 {@link TokenAuthenticationFilter} 所认证通过 | ||
| 20 | + * 2. {@link LoginUserHandshakeInterceptor} 负责把 {@link LoginUser} 添加到 {@link WebSocketSession} 中 | ||
| 21 | + * | ||
| 22 | + * @author 芋道源码 | ||
| 23 | + */ | ||
| 24 | +public class LoginUserHandshakeInterceptor implements HandshakeInterceptor { | ||
| 25 | + | ||
| 13 | @Override | 26 | @Override |
| 14 | - public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { | 27 | + public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, |
| 28 | + WebSocketHandler wsHandler, Map<String, Object> attributes) { | ||
| 15 | LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); | 29 | LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); |
| 16 | - attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser); | 30 | + if (loginUser != null) { |
| 31 | + WebSocketFrameworkUtils.setLoginUser(loginUser, attributes); | ||
| 32 | + } | ||
| 17 | return true; | 33 | return true; |
| 18 | } | 34 | } |
| 19 | 35 | ||
| 20 | @Override | 36 | @Override |
| 21 | - public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) { | ||
| 22 | - | 37 | + public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, |
| 38 | + WebSocketHandler wsHandler, Exception exception) { | ||
| 39 | + // do nothing | ||
| 23 | } | 40 | } |
| 41 | + | ||
| 24 | } | 42 | } |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/security/WebSocketAuthorizeRequestsCustomizer.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.security; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.security.config.AuthorizeRequestsCustomizer; | ||
| 4 | +import cn.iocoder.yudao.framework.websocket.config.WebSocketProperties; | ||
| 5 | +import lombok.RequiredArgsConstructor; | ||
| 6 | +import org.springframework.security.config.annotation.web.builders.HttpSecurity; | ||
| 7 | +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * WebSocket 的权限自定义 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +@RequiredArgsConstructor | ||
| 15 | +public class WebSocketAuthorizeRequestsCustomizer extends AuthorizeRequestsCustomizer { | ||
| 16 | + | ||
| 17 | + private final WebSocketProperties webSocketProperties; | ||
| 18 | + | ||
| 19 | + @Override | ||
| 20 | + public void customize(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) { | ||
| 21 | + registry.antMatchers(webSocketProperties.getPath()).permitAll(); | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/AbstractWebSocketMessageSender.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 5 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 6 | +import cn.iocoder.yudao.framework.websocket.core.message.JsonWebSocketMessage; | ||
| 7 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; | ||
| 8 | +import lombok.RequiredArgsConstructor; | ||
| 9 | +import lombok.extern.slf4j.Slf4j; | ||
| 10 | +import org.springframework.web.socket.TextMessage; | ||
| 11 | +import org.springframework.web.socket.WebSocketSession; | ||
| 12 | + | ||
| 13 | +import java.io.IOException; | ||
| 14 | +import java.util.Collection; | ||
| 15 | +import java.util.Collections; | ||
| 16 | +import java.util.List; | ||
| 17 | + | ||
| 18 | +/** | ||
| 19 | + * WebSocketMessageSender 实现类 | ||
| 20 | + * | ||
| 21 | + * @author 芋道源码 | ||
| 22 | + */ | ||
| 23 | +@Slf4j | ||
| 24 | +@RequiredArgsConstructor | ||
| 25 | +public abstract class AbstractWebSocketMessageSender implements WebSocketMessageSender { | ||
| 26 | + | ||
| 27 | + private final WebSocketSessionManager sessionManager; | ||
| 28 | + | ||
| 29 | + @Override | ||
| 30 | + public void send(Integer userType, Long userId, String messageType, String messageContent) { | ||
| 31 | + send(null, userType, userId, messageType, messageContent); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + @Override | ||
| 35 | + public void send(Integer userType, String messageType, String messageContent) { | ||
| 36 | + send(null, userType, null, messageType, messageContent); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @Override | ||
| 40 | + public void send(String sessionId, String messageType, String messageContent) { | ||
| 41 | + send(sessionId, null, null, messageType, messageContent); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 发送消息 | ||
| 46 | + * | ||
| 47 | + * @param sessionId Session 编号 | ||
| 48 | + * @param userType 用户类型 | ||
| 49 | + * @param userId 用户编号 | ||
| 50 | + * @param messageType 消息类型 | ||
| 51 | + * @param messageContent 消息内容 | ||
| 52 | + */ | ||
| 53 | + public void send(String sessionId, Integer userType, Long userId, String messageType, String messageContent) { | ||
| 54 | + // 1. 获得 Session 列表 | ||
| 55 | + List<WebSocketSession> sessions = Collections.emptyList(); | ||
| 56 | + if (StrUtil.isNotEmpty(sessionId)) { | ||
| 57 | + WebSocketSession session = sessionManager.getSession(sessionId); | ||
| 58 | + if (session != null) { | ||
| 59 | + sessions = Collections.singletonList(session); | ||
| 60 | + } | ||
| 61 | + } else if (userType != null && userId != null) { | ||
| 62 | + sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType, userId); | ||
| 63 | + } else if (userType != null) { | ||
| 64 | + sessions = (List<WebSocketSession>) sessionManager.getSessionList(userType); | ||
| 65 | + } | ||
| 66 | + if (CollUtil.isEmpty(sessions)) { | ||
| 67 | + log.info("[send][sessionId({}) userType({}) userId({}) messageType({}) messageContent({}) 未匹配到会话]", | ||
| 68 | + sessionId, userType, userId, messageType, messageContent); | ||
| 69 | + } | ||
| 70 | + // 2. 执行发送 | ||
| 71 | + doSend(sessions, messageType, messageContent); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + /** | ||
| 75 | + * 发送消息的具体实现 | ||
| 76 | + * | ||
| 77 | + * @param sessions Session 列表 | ||
| 78 | + * @param messageType 消息类型 | ||
| 79 | + * @param messageContent 消息内容 | ||
| 80 | + */ | ||
| 81 | + public void doSend(Collection<WebSocketSession> sessions, String messageType, String messageContent) { | ||
| 82 | + JsonWebSocketMessage message = new JsonWebSocketMessage().setType(messageType).setContent(messageContent); | ||
| 83 | + String payload = JsonUtils.toJsonString(message); // 关键,使用 JSON 序列化 | ||
| 84 | + sessions.forEach(session -> { | ||
| 85 | + // 1. 各种校验,保证 Session 可以被发送 | ||
| 86 | + if (session == null) { | ||
| 87 | + log.error("[doSend][session 为空, message({})]", message); | ||
| 88 | + return; | ||
| 89 | + } | ||
| 90 | + if (!session.isOpen()) { | ||
| 91 | + log.error("[doSend][session({}) 已关闭, message({})]", session.getId(), message); | ||
| 92 | + return; | ||
| 93 | + } | ||
| 94 | + // 2. 执行发送 | ||
| 95 | + try { | ||
| 96 | + session.sendMessage(new TextMessage(payload)); | ||
| 97 | + log.info("[doSend][session({}) 发送消息成功,message({})]", session.getId(), message); | ||
| 98 | + } catch (IOException ex) { | ||
| 99 | + log.error("[doSend][session({}) 发送消息失败,message({})]", session.getId(), message, ex); | ||
| 100 | + } | ||
| 101 | + }); | ||
| 102 | + } | ||
| 103 | + | ||
| 104 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/WebSocketMessageSender.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * WebSocket 消息的发送器接口 | ||
| 7 | + * | ||
| 8 | + * @author 芋道源码 | ||
| 9 | + */ | ||
| 10 | +public interface WebSocketMessageSender { | ||
| 11 | + | ||
| 12 | + /** | ||
| 13 | + * 发送消息给指定用户 | ||
| 14 | + * | ||
| 15 | + * @param userType 用户类型 | ||
| 16 | + * @param userId 用户编号 | ||
| 17 | + * @param messageType 消息类型 | ||
| 18 | + * @param messageContent 消息内容,JSON 格式 | ||
| 19 | + */ | ||
| 20 | + void send(Integer userType, Long userId, String messageType, String messageContent); | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 发送消息给指定用户类型 | ||
| 24 | + * | ||
| 25 | + * @param userType 用户类型 | ||
| 26 | + * @param messageType 消息类型 | ||
| 27 | + * @param messageContent 消息内容,JSON 格式 | ||
| 28 | + */ | ||
| 29 | + void send(Integer userType, String messageType, String messageContent); | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * 发送消息给指定 Session | ||
| 33 | + * | ||
| 34 | + * @param sessionId Session 编号 | ||
| 35 | + * @param messageType 消息类型 | ||
| 36 | + * @param messageContent 消息内容,JSON 格式 | ||
| 37 | + */ | ||
| 38 | + void send(String sessionId, String messageType, String messageContent); | ||
| 39 | + | ||
| 40 | + default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) { | ||
| 41 | + send(userType, userId, messageType, JsonUtils.toJsonString(messageContent)); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + default void sendObject(Integer userType, String messageType, Object messageContent) { | ||
| 45 | + send(userType, messageType, JsonUtils.toJsonString(messageContent)); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + default void sendObject(String sessionId, String messageType, Object messageContent) { | ||
| 49 | + send(sessionId, messageType, JsonUtils.toJsonString(messageContent)); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.kafka; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * Kafka 广播 WebSocket 的消息 | ||
| 7 | + * | ||
| 8 | + * @author 芋道源码 | ||
| 9 | + */ | ||
| 10 | +@Data | ||
| 11 | +public class KafkaWebSocketMessage { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * Session 编号 | ||
| 15 | + */ | ||
| 16 | + private String sessionId; | ||
| 17 | + /** | ||
| 18 | + * 用户类型 | ||
| 19 | + */ | ||
| 20 | + private Integer userType; | ||
| 21 | + /** | ||
| 22 | + * 用户编号 | ||
| 23 | + */ | ||
| 24 | + private Long userId; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 消息类型 | ||
| 28 | + */ | ||
| 29 | + private String messageType; | ||
| 30 | + /** | ||
| 31 | + * 消息内容 | ||
| 32 | + */ | ||
| 33 | + private String messageContent; | ||
| 34 | + | ||
| 35 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageConsumer.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.kafka; | ||
| 2 | + | ||
| 3 | +import lombok.RequiredArgsConstructor; | ||
| 4 | +import org.springframework.amqp.rabbit.annotation.RabbitHandler; | ||
| 5 | +import org.springframework.kafka.annotation.KafkaListener; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * {@link KafkaWebSocketMessage} 广播消息的消费者,真正把消息发送出去 | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +@RequiredArgsConstructor | ||
| 13 | +public class KafkaWebSocketMessageConsumer { | ||
| 14 | + | ||
| 15 | + private final KafkaWebSocketMessageSender rabbitMQWebSocketMessageSender; | ||
| 16 | + | ||
| 17 | + @RabbitHandler | ||
| 18 | + @KafkaListener( | ||
| 19 | + topics = "${yudao.websocket.sender-kafka.topic}", | ||
| 20 | + // 在 Group 上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Group 不同,以达到广播消费的目的 | ||
| 21 | + groupId = "${yudao.websocket.sender-kafka.consumer-group}" + "-" + "#{T(java.util.UUID).randomUUID()}") | ||
| 22 | + public void onMessage(KafkaWebSocketMessage message) { | ||
| 23 | + rabbitMQWebSocketMessageSender.send(message.getSessionId(), | ||
| 24 | + message.getUserType(), message.getUserId(), | ||
| 25 | + message.getMessageType(), message.getMessageContent()); | ||
| 26 | + } | ||
| 27 | + | ||
| 28 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/kafka/KafkaWebSocketMessageSender.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.kafka; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; | ||
| 4 | +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 5 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | +import org.springframework.kafka.core.KafkaTemplate; | ||
| 8 | + | ||
| 9 | +import java.util.concurrent.ExecutionException; | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * 基于 Kafka 的 {@link WebSocketMessageSender} 实现类 | ||
| 13 | + * | ||
| 14 | + * @author 芋道源码 | ||
| 15 | + */ | ||
| 16 | +@Slf4j | ||
| 17 | +public class KafkaWebSocketMessageSender extends AbstractWebSocketMessageSender { | ||
| 18 | + | ||
| 19 | + private final KafkaTemplate<Object, Object> kafkaTemplate; | ||
| 20 | + | ||
| 21 | + private final String topic; | ||
| 22 | + | ||
| 23 | + public KafkaWebSocketMessageSender(WebSocketSessionManager sessionManager, | ||
| 24 | + KafkaTemplate<Object, Object> kafkaTemplate, | ||
| 25 | + String topic) { | ||
| 26 | + super(sessionManager); | ||
| 27 | + this.kafkaTemplate = kafkaTemplate; | ||
| 28 | + this.topic = topic; | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + @Override | ||
| 32 | + public void send(Integer userType, Long userId, String messageType, String messageContent) { | ||
| 33 | + sendKafkaMessage(null, userId, userType, messageType, messageContent); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public void send(Integer userType, String messageType, String messageContent) { | ||
| 38 | + sendKafkaMessage(null, null, userType, messageType, messageContent); | ||
| 39 | + } | ||
| 40 | + | ||
| 41 | + @Override | ||
| 42 | + public void send(String sessionId, String messageType, String messageContent) { | ||
| 43 | + sendKafkaMessage(sessionId, null, null, messageType, messageContent); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 通过 Kafka 广播消息 | ||
| 48 | + * | ||
| 49 | + * @param sessionId Session 编号 | ||
| 50 | + * @param userId 用户编号 | ||
| 51 | + * @param userType 用户类型 | ||
| 52 | + * @param messageType 消息类型 | ||
| 53 | + * @param messageContent 消息内容 | ||
| 54 | + */ | ||
| 55 | + private void sendKafkaMessage(String sessionId, Long userId, Integer userType, | ||
| 56 | + String messageType, String messageContent) { | ||
| 57 | + KafkaWebSocketMessage mqMessage = new KafkaWebSocketMessage() | ||
| 58 | + .setSessionId(sessionId).setUserId(userId).setUserType(userType) | ||
| 59 | + .setMessageType(messageType).setMessageContent(messageContent); | ||
| 60 | + try { | ||
| 61 | + kafkaTemplate.send(topic, mqMessage).get(); | ||
| 62 | + } catch (InterruptedException | ExecutionException e) { | ||
| 63 | + log.error("[sendKafkaMessage][发送消息({}) 到 Kafka 失败]", mqMessage, e); | ||
| 64 | + } | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/local/LocalWebSocketMessageSender.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.local; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; | ||
| 4 | +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 5 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 本地的 {@link WebSocketMessageSender} 实现类 | ||
| 9 | + * | ||
| 10 | + * 注意:仅仅适合单机场景!!! | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +public class LocalWebSocketMessageSender extends AbstractWebSocketMessageSender { | ||
| 15 | + | ||
| 16 | + public LocalWebSocketMessageSender(WebSocketSessionManager sessionManager) { | ||
| 17 | + super(sessionManager); | ||
| 18 | + } | ||
| 19 | + | ||
| 20 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +import java.io.Serializable; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * RabbitMQ 广播 WebSocket 的消息 | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +@Data | ||
| 13 | +public class RabbitMQWebSocketMessage implements Serializable { | ||
| 14 | + | ||
| 15 | + /** | ||
| 16 | + * Session 编号 | ||
| 17 | + */ | ||
| 18 | + private String sessionId; | ||
| 19 | + /** | ||
| 20 | + * 用户类型 | ||
| 21 | + */ | ||
| 22 | + private Integer userType; | ||
| 23 | + /** | ||
| 24 | + * 用户编号 | ||
| 25 | + */ | ||
| 26 | + private Long userId; | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 消息类型 | ||
| 30 | + */ | ||
| 31 | + private String messageType; | ||
| 32 | + /** | ||
| 33 | + * 消息内容 | ||
| 34 | + */ | ||
| 35 | + private String messageContent; | ||
| 36 | + | ||
| 37 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageConsumer.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq; | ||
| 2 | + | ||
| 3 | +import lombok.RequiredArgsConstructor; | ||
| 4 | +import org.springframework.amqp.core.ExchangeTypes; | ||
| 5 | +import org.springframework.amqp.rabbit.annotation.*; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * {@link RabbitMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去 | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +@RabbitListener( | ||
| 13 | + bindings = @QueueBinding( | ||
| 14 | + value = @Queue( | ||
| 15 | + // 在 Queue 的名字上,使用 UUID 生成其后缀。这样,启动的 Consumer 的 Queue 不同,以达到广播消费的目的 | ||
| 16 | + name = "${yudao.websocket.sender-rabbitmq.queue}" + "-" + "#{T(java.util.UUID).randomUUID()}", | ||
| 17 | + // Consumer 关闭时,该队列就可以被自动删除了 | ||
| 18 | + autoDelete = "true" | ||
| 19 | + ), | ||
| 20 | + exchange = @Exchange( | ||
| 21 | + name = "${yudao.websocket.sender-rabbitmq.exchange}", | ||
| 22 | + type = ExchangeTypes.TOPIC, | ||
| 23 | + declare = "false" | ||
| 24 | + ) | ||
| 25 | + ) | ||
| 26 | +) | ||
| 27 | +@RequiredArgsConstructor | ||
| 28 | +public class RabbitMQWebSocketMessageConsumer { | ||
| 29 | + | ||
| 30 | + private final RabbitMQWebSocketMessageSender rabbitMQWebSocketMessageSender; | ||
| 31 | + | ||
| 32 | + @RabbitHandler | ||
| 33 | + public void onMessage(RabbitMQWebSocketMessage message) { | ||
| 34 | + rabbitMQWebSocketMessageSender.send(message.getSessionId(), | ||
| 35 | + message.getUserType(), message.getUserId(), | ||
| 36 | + message.getMessageType(), message.getMessageContent()); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rabbitmq/RabbitMQWebSocketMessageSender.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.rabbitmq; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; | ||
| 4 | +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 5 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | +import org.springframework.amqp.core.TopicExchange; | ||
| 8 | +import org.springframework.amqp.rabbit.core.RabbitTemplate; | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 基于 RabbitMQ 的 {@link WebSocketMessageSender} 实现类 | ||
| 12 | + * | ||
| 13 | + * @author 芋道源码 | ||
| 14 | + */ | ||
| 15 | +@Slf4j | ||
| 16 | +public class RabbitMQWebSocketMessageSender extends AbstractWebSocketMessageSender { | ||
| 17 | + | ||
| 18 | + private final RabbitTemplate rabbitTemplate; | ||
| 19 | + | ||
| 20 | + private final TopicExchange topicExchange; | ||
| 21 | + | ||
| 22 | + public RabbitMQWebSocketMessageSender(WebSocketSessionManager sessionManager, | ||
| 23 | + RabbitTemplate rabbitTemplate, | ||
| 24 | + TopicExchange topicExchange) { | ||
| 25 | + super(sessionManager); | ||
| 26 | + this.rabbitTemplate = rabbitTemplate; | ||
| 27 | + this.topicExchange = topicExchange; | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + @Override | ||
| 31 | + public void send(Integer userType, Long userId, String messageType, String messageContent) { | ||
| 32 | + sendRabbitMQMessage(null, userId, userType, messageType, messageContent); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + @Override | ||
| 36 | + public void send(Integer userType, String messageType, String messageContent) { | ||
| 37 | + sendRabbitMQMessage(null, null, userType, messageType, messageContent); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public void send(String sessionId, String messageType, String messageContent) { | ||
| 42 | + sendRabbitMQMessage(sessionId, null, null, messageType, messageContent); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + /** | ||
| 46 | + * 通过 RabbitMQ 广播消息 | ||
| 47 | + * | ||
| 48 | + * @param sessionId Session 编号 | ||
| 49 | + * @param userId 用户编号 | ||
| 50 | + * @param userType 用户类型 | ||
| 51 | + * @param messageType 消息类型 | ||
| 52 | + * @param messageContent 消息内容 | ||
| 53 | + */ | ||
| 54 | + private void sendRabbitMQMessage(String sessionId, Long userId, Integer userType, | ||
| 55 | + String messageType, String messageContent) { | ||
| 56 | + RabbitMQWebSocketMessage mqMessage = new RabbitMQWebSocketMessage() | ||
| 57 | + .setSessionId(sessionId).setUserId(userId).setUserType(userType) | ||
| 58 | + .setMessageType(messageType).setMessageContent(messageContent); | ||
| 59 | + rabbitTemplate.convertAndSend(topicExchange.getName(), null, mqMessage); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.redis; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessage; | ||
| 4 | +import lombok.Data; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * Redis 广播 WebSocket 的消息 | ||
| 8 | + */ | ||
| 9 | +@Data | ||
| 10 | +public class RedisWebSocketMessage extends AbstractRedisChannelMessage { | ||
| 11 | + | ||
| 12 | + /** | ||
| 13 | + * Session 编号 | ||
| 14 | + */ | ||
| 15 | + private String sessionId; | ||
| 16 | + /** | ||
| 17 | + * 用户类型 | ||
| 18 | + */ | ||
| 19 | + private Integer userType; | ||
| 20 | + /** | ||
| 21 | + * 用户编号 | ||
| 22 | + */ | ||
| 23 | + private Long userId; | ||
| 24 | + | ||
| 25 | + /** | ||
| 26 | + * 消息类型 | ||
| 27 | + */ | ||
| 28 | + private String messageType; | ||
| 29 | + /** | ||
| 30 | + * 消息内容 | ||
| 31 | + */ | ||
| 32 | + private String messageContent; | ||
| 33 | + | ||
| 34 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageConsumer.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.redis; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mq.redis.core.pubsub.AbstractRedisChannelMessageListener; | ||
| 4 | +import lombok.RequiredArgsConstructor; | ||
| 5 | + | ||
| 6 | +/** | ||
| 7 | + * {@link RedisWebSocketMessage} 广播消息的消费者,真正把消息发送出去 | ||
| 8 | + * | ||
| 9 | + * @author 芋道源码 | ||
| 10 | + */ | ||
| 11 | +@RequiredArgsConstructor | ||
| 12 | +public class RedisWebSocketMessageConsumer extends AbstractRedisChannelMessageListener<RedisWebSocketMessage> { | ||
| 13 | + | ||
| 14 | + private final RedisWebSocketMessageSender redisWebSocketMessageSender; | ||
| 15 | + | ||
| 16 | + @Override | ||
| 17 | + public void onMessage(RedisWebSocketMessage message) { | ||
| 18 | + redisWebSocketMessageSender.send(message.getSessionId(), | ||
| 19 | + message.getUserType(), message.getUserId(), | ||
| 20 | + message.getMessageType(), message.getMessageContent()); | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/redis/RedisWebSocketMessageSender.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.redis; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; | ||
| 4 | +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; | ||
| 5 | +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 6 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; | ||
| 7 | +import lombok.extern.slf4j.Slf4j; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 基于 Redis 的 {@link WebSocketMessageSender} 实现类 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +@Slf4j | ||
| 15 | +public class RedisWebSocketMessageSender extends AbstractWebSocketMessageSender { | ||
| 16 | + | ||
| 17 | + private final RedisMQTemplate redisMQTemplate; | ||
| 18 | + | ||
| 19 | + public RedisWebSocketMessageSender(WebSocketSessionManager sessionManager, | ||
| 20 | + RedisMQTemplate redisMQTemplate) { | ||
| 21 | + super(sessionManager); | ||
| 22 | + this.redisMQTemplate = redisMQTemplate; | ||
| 23 | + } | ||
| 24 | + | ||
| 25 | + @Override | ||
| 26 | + public void send(Integer userType, Long userId, String messageType, String messageContent) { | ||
| 27 | + sendRedisMessage(null, userId, userType, messageType, messageContent); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + @Override | ||
| 31 | + public void send(Integer userType, String messageType, String messageContent) { | ||
| 32 | + sendRedisMessage(null, null, userType, messageType, messageContent); | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + @Override | ||
| 36 | + public void send(String sessionId, String messageType, String messageContent) { | ||
| 37 | + sendRedisMessage(sessionId, null, null, messageType, messageContent); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + /** | ||
| 41 | + * 通过 Redis 广播消息 | ||
| 42 | + * | ||
| 43 | + * @param sessionId Session 编号 | ||
| 44 | + * @param userId 用户编号 | ||
| 45 | + * @param userType 用户类型 | ||
| 46 | + * @param messageType 消息类型 | ||
| 47 | + * @param messageContent 消息内容 | ||
| 48 | + */ | ||
| 49 | + private void sendRedisMessage(String sessionId, Long userId, Integer userType, | ||
| 50 | + String messageType, String messageContent) { | ||
| 51 | + RedisWebSocketMessage mqMessage = new RedisWebSocketMessage() | ||
| 52 | + .setSessionId(sessionId).setUserId(userId).setUserType(userType) | ||
| 53 | + .setMessageType(messageType).setMessageContent(messageContent); | ||
| 54 | + redisMQTemplate.send(mqMessage); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * RocketMQ 广播 WebSocket 的消息 | ||
| 7 | + * | ||
| 8 | + * @author 芋道源码 | ||
| 9 | + */ | ||
| 10 | +@Data | ||
| 11 | +public class RocketMQWebSocketMessage { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * Session 编号 | ||
| 15 | + */ | ||
| 16 | + private String sessionId; | ||
| 17 | + /** | ||
| 18 | + * 用户类型 | ||
| 19 | + */ | ||
| 20 | + private Integer userType; | ||
| 21 | + /** | ||
| 22 | + * 用户编号 | ||
| 23 | + */ | ||
| 24 | + private Long userId; | ||
| 25 | + | ||
| 26 | + /** | ||
| 27 | + * 消息类型 | ||
| 28 | + */ | ||
| 29 | + private String messageType; | ||
| 30 | + /** | ||
| 31 | + * 消息内容 | ||
| 32 | + */ | ||
| 33 | + private String messageContent; | ||
| 34 | + | ||
| 35 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageConsumer.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq; | ||
| 2 | + | ||
| 3 | +import lombok.RequiredArgsConstructor; | ||
| 4 | +import org.apache.rocketmq.spring.annotation.MessageModel; | ||
| 5 | +import org.apache.rocketmq.spring.annotation.RocketMQMessageListener; | ||
| 6 | +import org.apache.rocketmq.spring.core.RocketMQListener; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * {@link RocketMQWebSocketMessage} 广播消息的消费者,真正把消息发送出去 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +@RocketMQMessageListener( // 重点:添加 @RocketMQMessageListener 注解,声明消费的 topic | ||
| 14 | + topic = "${yudao.websocket.sender-rocketmq.topic}", | ||
| 15 | + consumerGroup = "${yudao.websocket.sender-rocketmq.consumer-group}", | ||
| 16 | + messageModel = MessageModel.BROADCASTING // 设置为广播模式,保证每个实例都能收到消息 | ||
| 17 | +) | ||
| 18 | +@RequiredArgsConstructor | ||
| 19 | +public class RocketMQWebSocketMessageConsumer implements RocketMQListener<RocketMQWebSocketMessage> { | ||
| 20 | + | ||
| 21 | + private final RocketMQWebSocketMessageSender rocketMQWebSocketMessageSender; | ||
| 22 | + | ||
| 23 | + @Override | ||
| 24 | + public void onMessage(RocketMQWebSocketMessage message) { | ||
| 25 | + rocketMQWebSocketMessageSender.send(message.getSessionId(), | ||
| 26 | + message.getUserType(), message.getUserId(), | ||
| 27 | + message.getMessageType(), message.getMessageContent()); | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/sender/rocketmq/RocketMQWebSocketMessageSender.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.sender.rocketmq; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.websocket.core.sender.AbstractWebSocketMessageSender; | ||
| 4 | +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 5 | +import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; | ||
| 6 | +import lombok.extern.slf4j.Slf4j; | ||
| 7 | +import org.apache.rocketmq.spring.core.RocketMQTemplate; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * 基于 RocketMQ 的 {@link WebSocketMessageSender} 实现类 | ||
| 11 | + * | ||
| 12 | + * @author 芋道源码 | ||
| 13 | + */ | ||
| 14 | +@Slf4j | ||
| 15 | +public class RocketMQWebSocketMessageSender extends AbstractWebSocketMessageSender { | ||
| 16 | + | ||
| 17 | + private final RocketMQTemplate rocketMQTemplate; | ||
| 18 | + | ||
| 19 | + private final String topic; | ||
| 20 | + | ||
| 21 | + public RocketMQWebSocketMessageSender(WebSocketSessionManager sessionManager, | ||
| 22 | + RocketMQTemplate rocketMQTemplate, | ||
| 23 | + String topic) { | ||
| 24 | + super(sessionManager); | ||
| 25 | + this.rocketMQTemplate = rocketMQTemplate; | ||
| 26 | + this.topic = topic; | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + @Override | ||
| 30 | + public void send(Integer userType, Long userId, String messageType, String messageContent) { | ||
| 31 | + sendRocketMQMessage(null, userId, userType, messageType, messageContent); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | + @Override | ||
| 35 | + public void send(Integer userType, String messageType, String messageContent) { | ||
| 36 | + sendRocketMQMessage(null, null, userType, messageType, messageContent); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @Override | ||
| 40 | + public void send(String sessionId, String messageType, String messageContent) { | ||
| 41 | + sendRocketMQMessage(sessionId, null, null, messageType, messageContent); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 通过 RocketMQ 广播消息 | ||
| 46 | + * | ||
| 47 | + * @param sessionId Session 编号 | ||
| 48 | + * @param userId 用户编号 | ||
| 49 | + * @param userType 用户类型 | ||
| 50 | + * @param messageType 消息类型 | ||
| 51 | + * @param messageContent 消息内容 | ||
| 52 | + */ | ||
| 53 | + private void sendRocketMQMessage(String sessionId, Long userId, Integer userType, | ||
| 54 | + String messageType, String messageContent) { | ||
| 55 | + RocketMQWebSocketMessage mqMessage = new RocketMQWebSocketMessage() | ||
| 56 | + .setSessionId(sessionId).setUserId(userId).setUserType(userType) | ||
| 57 | + .setMessageType(messageType).setMessageContent(messageContent); | ||
| 58 | + rocketMQTemplate.syncSend(topic, mqMessage); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionHandlerDecorator.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.session; | ||
| 2 | + | ||
| 3 | +import org.springframework.web.socket.CloseStatus; | ||
| 4 | +import org.springframework.web.socket.WebSocketHandler; | ||
| 5 | +import org.springframework.web.socket.WebSocketSession; | ||
| 6 | +import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator; | ||
| 7 | +import org.springframework.web.socket.handler.WebSocketHandlerDecorator; | ||
| 8 | + | ||
| 9 | +/** | ||
| 10 | + * {@link WebSocketHandler} 的装饰类,实现了以下功能: | ||
| 11 | + * | ||
| 12 | + * 1. {@link WebSocketSession} 连接或关闭时,使用 {@link #sessionManager} 进行管理 | ||
| 13 | + * 2. 封装 {@link WebSocketSession} 支持并发操作 | ||
| 14 | + * | ||
| 15 | + * @author 芋道源码 | ||
| 16 | + */ | ||
| 17 | +public class WebSocketSessionHandlerDecorator extends WebSocketHandlerDecorator { | ||
| 18 | + | ||
| 19 | + /** | ||
| 20 | + * 发送时间的限制,单位:毫秒 | ||
| 21 | + */ | ||
| 22 | + private static final Integer SEND_TIME_LIMIT = 1000 * 5; | ||
| 23 | + /** | ||
| 24 | + * 发送消息缓冲上线,单位:bytes | ||
| 25 | + */ | ||
| 26 | + private static final Integer BUFFER_SIZE_LIMIT = 1024 * 100; | ||
| 27 | + | ||
| 28 | + private final WebSocketSessionManager sessionManager; | ||
| 29 | + | ||
| 30 | + public WebSocketSessionHandlerDecorator(WebSocketHandler delegate, | ||
| 31 | + WebSocketSessionManager sessionManager) { | ||
| 32 | + super(delegate); | ||
| 33 | + this.sessionManager = sessionManager; | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + @Override | ||
| 37 | + public void afterConnectionEstablished(WebSocketSession session) { | ||
| 38 | + // 实现 session 支持并发,可参考 https://blog.csdn.net/abu935009066/article/details/131218149 | ||
| 39 | + session = new ConcurrentWebSocketSessionDecorator(session, SEND_TIME_LIMIT, BUFFER_SIZE_LIMIT); | ||
| 40 | + // 添加到 WebSocketSessionManager 中 | ||
| 41 | + sessionManager.addSession(session); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + @Override | ||
| 45 | + public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) { | ||
| 46 | + sessionManager.removeSession(session); | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManager.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.session; | ||
| 2 | + | ||
| 3 | +import org.springframework.web.socket.WebSocketSession; | ||
| 4 | + | ||
| 5 | +import java.util.Collection; | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * {@link WebSocketSession} 管理器的接口 | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +public interface WebSocketSessionManager { | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * 添加 Session | ||
| 16 | + * | ||
| 17 | + * @param session Session | ||
| 18 | + */ | ||
| 19 | + void addSession(WebSocketSession session); | ||
| 20 | + | ||
| 21 | + /** | ||
| 22 | + * 移除 Session | ||
| 23 | + * | ||
| 24 | + * @param session Session | ||
| 25 | + */ | ||
| 26 | + void removeSession(WebSocketSession session); | ||
| 27 | + | ||
| 28 | + /** | ||
| 29 | + * 获得指定编号的 Session | ||
| 30 | + * | ||
| 31 | + * @param id Session 编号 | ||
| 32 | + * @return Session | ||
| 33 | + */ | ||
| 34 | + WebSocketSession getSession(String id); | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 获得指定用户类型的 Session 列表 | ||
| 38 | + * | ||
| 39 | + * @param userType 用户类型 | ||
| 40 | + * @return Session 列表 | ||
| 41 | + */ | ||
| 42 | + Collection<WebSocketSession> getSessionList(Integer userType); | ||
| 43 | + | ||
| 44 | + /** | ||
| 45 | + * 获得指定用户编号的 Session 列表 | ||
| 46 | + * | ||
| 47 | + * @param userType 用户类型 | ||
| 48 | + * @param userId 用户编号 | ||
| 49 | + * @return Session 列表 | ||
| 50 | + */ | ||
| 51 | + Collection<WebSocketSession> getSessionList(Integer userType, Long userId); | ||
| 52 | + | ||
| 53 | +} | ||
| 0 | \ No newline at end of file | 54 | \ No newline at end of file |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/session/WebSocketSessionManagerImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.session; | ||
| 2 | + | ||
| 3 | +import cn.hutool.core.collection.CollUtil; | ||
| 4 | +import cn.iocoder.yudao.framework.security.core.LoginUser; | ||
| 5 | +import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; | ||
| 6 | +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; | ||
| 7 | +import org.springframework.web.socket.WebSocketSession; | ||
| 8 | + | ||
| 9 | +import java.util.ArrayList; | ||
| 10 | +import java.util.Collection; | ||
| 11 | +import java.util.LinkedList; | ||
| 12 | +import java.util.List; | ||
| 13 | +import java.util.concurrent.ConcurrentHashMap; | ||
| 14 | +import java.util.concurrent.ConcurrentMap; | ||
| 15 | +import java.util.concurrent.CopyOnWriteArrayList; | ||
| 16 | + | ||
| 17 | +/** | ||
| 18 | + * 默认的 {@link WebSocketSessionManager} 实现类 | ||
| 19 | + * | ||
| 20 | + * @author 芋道源码 | ||
| 21 | + */ | ||
| 22 | +public class WebSocketSessionManagerImpl implements WebSocketSessionManager { | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * id 与 WebSocketSession 映射 | ||
| 26 | + * | ||
| 27 | + * key:Session 编号 | ||
| 28 | + */ | ||
| 29 | + private final ConcurrentMap<String, WebSocketSession> idSessions = new ConcurrentHashMap<>(); | ||
| 30 | + | ||
| 31 | + /** | ||
| 32 | + * user 与 WebSocketSession 映射 | ||
| 33 | + * | ||
| 34 | + * key1:用户类型 | ||
| 35 | + * key2:用户编号 | ||
| 36 | + */ | ||
| 37 | + private final ConcurrentMap<Integer, ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>>> userSessions | ||
| 38 | + = new ConcurrentHashMap<>(); | ||
| 39 | + | ||
| 40 | + @Override | ||
| 41 | + public void addSession(WebSocketSession session) { | ||
| 42 | + // 添加到 idSessions 中 | ||
| 43 | + idSessions.put(session.getId(), session); | ||
| 44 | + // 添加到 userSessions 中 | ||
| 45 | + LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); | ||
| 46 | + if (user == null) { | ||
| 47 | + return; | ||
| 48 | + } | ||
| 49 | + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType()); | ||
| 50 | + if (userSessionsMap == null) { | ||
| 51 | + userSessionsMap = new ConcurrentHashMap<>(); | ||
| 52 | + if (userSessions.putIfAbsent(user.getUserType(), userSessionsMap) != null) { | ||
| 53 | + userSessionsMap = userSessions.get(user.getUserType()); | ||
| 54 | + } | ||
| 55 | + } | ||
| 56 | + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId()); | ||
| 57 | + if (sessions == null) { | ||
| 58 | + sessions = new CopyOnWriteArrayList<>(); | ||
| 59 | + if (userSessionsMap.putIfAbsent(user.getId(), sessions) != null) { | ||
| 60 | + sessions = userSessionsMap.get(user.getId()); | ||
| 61 | + } | ||
| 62 | + } | ||
| 63 | + sessions.add(session); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + @Override | ||
| 67 | + public void removeSession(WebSocketSession session) { | ||
| 68 | + // 移除从 idSessions 中 | ||
| 69 | + idSessions.remove(session.getId(), session); | ||
| 70 | + // 移除从 idSessions 中 | ||
| 71 | + LoginUser user = WebSocketFrameworkUtils.getLoginUser(session); | ||
| 72 | + if (user == null) { | ||
| 73 | + return; | ||
| 74 | + } | ||
| 75 | + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(user.getUserType()); | ||
| 76 | + if (userSessionsMap == null) { | ||
| 77 | + return; | ||
| 78 | + } | ||
| 79 | + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(user.getId()); | ||
| 80 | + sessions.removeIf(session0 -> session0.getId().equals(session.getId())); | ||
| 81 | + if (CollUtil.isEmpty(sessions)) { | ||
| 82 | + userSessionsMap.remove(user.getId(), sessions); | ||
| 83 | + } | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + @Override | ||
| 87 | + public WebSocketSession getSession(String id) { | ||
| 88 | + return idSessions.get(id); | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + @Override | ||
| 92 | + public Collection<WebSocketSession> getSessionList(Integer userType) { | ||
| 93 | + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType); | ||
| 94 | + if (CollUtil.isEmpty(userSessionsMap)) { | ||
| 95 | + return new ArrayList<>(); | ||
| 96 | + } | ||
| 97 | + LinkedList<WebSocketSession> result = new LinkedList<>(); // 避免扩容 | ||
| 98 | + Long contextTenantId = TenantContextHolder.getTenantId(); | ||
| 99 | + for (List<WebSocketSession> sessions : userSessionsMap.values()) { | ||
| 100 | + if (CollUtil.isEmpty(sessions)) { | ||
| 101 | + continue; | ||
| 102 | + } | ||
| 103 | + // 特殊:如果租户不匹配,则直接排除 | ||
| 104 | + if (contextTenantId != null) { | ||
| 105 | + Long userTenantId = WebSocketFrameworkUtils.getTenantId(sessions.get(0)); | ||
| 106 | + if (!contextTenantId.equals(userTenantId)) { | ||
| 107 | + continue; | ||
| 108 | + } | ||
| 109 | + } | ||
| 110 | + result.addAll(sessions); | ||
| 111 | + } | ||
| 112 | + return result; | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + @Override | ||
| 116 | + public Collection<WebSocketSession> getSessionList(Integer userType, Long userId) { | ||
| 117 | + ConcurrentMap<Long, CopyOnWriteArrayList<WebSocketSession>> userSessionsMap = userSessions.get(userType); | ||
| 118 | + if (CollUtil.isEmpty(userSessionsMap)) { | ||
| 119 | + return new ArrayList<>(); | ||
| 120 | + } | ||
| 121 | + CopyOnWriteArrayList<WebSocketSession> sessions = userSessionsMap.get(userId); | ||
| 122 | + return CollUtil.isNotEmpty(sessions) ? new ArrayList<>(sessions) : new ArrayList<>(); | ||
| 123 | + } | ||
| 124 | + | ||
| 125 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/util/WebSocketFrameworkUtils.java
0 → 100644
| 1 | +package cn.iocoder.yudao.framework.websocket.core.util; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.security.core.LoginUser; | ||
| 4 | +import org.springframework.web.socket.WebSocketSession; | ||
| 5 | + | ||
| 6 | +import java.util.Map; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * 专属于 web 包的工具类 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +public class WebSocketFrameworkUtils { | ||
| 14 | + | ||
| 15 | + public static final String ATTRIBUTE_LOGIN_USER = "LOGIN_USER"; | ||
| 16 | + | ||
| 17 | + /** | ||
| 18 | + * 设置当前用户 | ||
| 19 | + * | ||
| 20 | + * @param loginUser 登录用户 | ||
| 21 | + * @param attributes Session | ||
| 22 | + */ | ||
| 23 | + public static void setLoginUser(LoginUser loginUser, Map<String, Object> attributes) { | ||
| 24 | + attributes.put(ATTRIBUTE_LOGIN_USER, loginUser); | ||
| 25 | + } | ||
| 26 | + | ||
| 27 | + /** | ||
| 28 | + * 获取当前用户 | ||
| 29 | + * | ||
| 30 | + * @return 当前用户 | ||
| 31 | + */ | ||
| 32 | + public static LoginUser getLoginUser(WebSocketSession session) { | ||
| 33 | + return (LoginUser) session.getAttributes().get(ATTRIBUTE_LOGIN_USER); | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * 获得当前用户的编号 | ||
| 38 | + * | ||
| 39 | + * @return 用户编号 | ||
| 40 | + */ | ||
| 41 | + public static Long getLoginUserId(WebSocketSession session) { | ||
| 42 | + LoginUser loginUser = getLoginUser(session); | ||
| 43 | + return loginUser != null ? loginUser.getId() : null; | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + /** | ||
| 47 | + * 获得当前用户的类型 | ||
| 48 | + * | ||
| 49 | + * @return 用户编号 | ||
| 50 | + */ | ||
| 51 | + public static Integer getLoginUserType(WebSocketSession session) { | ||
| 52 | + LoginUser loginUser = getLoginUser(session); | ||
| 53 | + return loginUser != null ? loginUser.getUserType() : null; | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + /** | ||
| 57 | + * 获得当前用户的租户编号 | ||
| 58 | + * | ||
| 59 | + * @param session Session | ||
| 60 | + * @return 租户编号 | ||
| 61 | + */ | ||
| 62 | + public static Long getTenantId(WebSocketSession session) { | ||
| 63 | + LoginUser loginUser = getLoginUser(session); | ||
| 64 | + return loginUser != null ? loginUser.getTenantId() : null; | ||
| 65 | + } | ||
| 66 | + | ||
| 67 | +} |
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/package-info.java
yudao-framework/yudao-spring-boot-starter-websocket/《芋道 Spring Boot WebSocket 入门》.md
0 → 100644
| 1 | +<http://www.iocoder.cn/Spring-Boot/WebSocket/?yudao> |
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApi.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.infra.api.websocket; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * WebSocket 发送器的 API 接口 | ||
| 7 | + * | ||
| 8 | + * 对 WebSocketMessageSender 进行封装,提供给其它模块使用 | ||
| 9 | + * | ||
| 10 | + * @author 芋道源码 | ||
| 11 | + */ | ||
| 12 | +public interface WebSocketSenderApi { | ||
| 13 | + | ||
| 14 | + /** | ||
| 15 | + * 发送消息给指定用户 | ||
| 16 | + * | ||
| 17 | + * @param userType 用户类型 | ||
| 18 | + * @param userId 用户编号 | ||
| 19 | + * @param messageType 消息类型 | ||
| 20 | + * @param messageContent 消息内容,JSON 格式 | ||
| 21 | + */ | ||
| 22 | + void send(Integer userType, Long userId, String messageType, String messageContent); | ||
| 23 | + | ||
| 24 | + /** | ||
| 25 | + * 发送消息给指定用户类型 | ||
| 26 | + * | ||
| 27 | + * @param userType 用户类型 | ||
| 28 | + * @param messageType 消息类型 | ||
| 29 | + * @param messageContent 消息内容,JSON 格式 | ||
| 30 | + */ | ||
| 31 | + void send(Integer userType, String messageType, String messageContent); | ||
| 32 | + | ||
| 33 | + /** | ||
| 34 | + * 发送消息给指定 Session | ||
| 35 | + * | ||
| 36 | + * @param sessionId Session 编号 | ||
| 37 | + * @param messageType 消息类型 | ||
| 38 | + * @param messageContent 消息内容,JSON 格式 | ||
| 39 | + */ | ||
| 40 | + void send(String sessionId, String messageType, String messageContent); | ||
| 41 | + | ||
| 42 | + default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) { | ||
| 43 | + send(userType, userId, messageType, JsonUtils.toJsonString(messageContent)); | ||
| 44 | + } | ||
| 45 | + | ||
| 46 | + default void sendObject(Integer userType, String messageType, Object messageContent) { | ||
| 47 | + send(userType, messageType, JsonUtils.toJsonString(messageContent)); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + default void sendObject(String sessionId, String messageType, Object messageContent) { | ||
| 51 | + send(sessionId, messageType, JsonUtils.toJsonString(messageContent)); | ||
| 52 | + } | ||
| 53 | + | ||
| 54 | +} |
yudao-module-infra/yudao-module-infra-biz/pom.xml
| @@ -46,6 +46,11 @@ | @@ -46,6 +46,11 @@ | ||
| 46 | <artifactId>yudao-spring-boot-starter-security</artifactId> | 46 | <artifactId>yudao-spring-boot-starter-security</artifactId> |
| 47 | </dependency> | 47 | </dependency> |
| 48 | 48 | ||
| 49 | + <dependency> | ||
| 50 | + <groupId>cn.iocoder.boot</groupId> | ||
| 51 | + <artifactId>yudao-spring-boot-starter-websocket</artifactId> | ||
| 52 | + </dependency> | ||
| 53 | + | ||
| 49 | <!-- DB 相关 --> | 54 | <!-- DB 相关 --> |
| 50 | <dependency> | 55 | <dependency> |
| 51 | <groupId>cn.iocoder.boot</groupId> | 56 | <groupId>cn.iocoder.boot</groupId> |
| @@ -116,11 +121,6 @@ | @@ -116,11 +121,6 @@ | ||
| 116 | <artifactId>yudao-spring-boot-starter-file</artifactId> | 121 | <artifactId>yudao-spring-boot-starter-file</artifactId> |
| 117 | </dependency> | 122 | </dependency> |
| 118 | 123 | ||
| 119 | - <!-- WebSocket --> | ||
| 120 | - <dependency> | ||
| 121 | - <groupId>org.springframework.boot</groupId> | ||
| 122 | - <artifactId>spring-boot-starter-websocket</artifactId> | ||
| 123 | - </dependency> | ||
| 124 | </dependencies> | 124 | </dependencies> |
| 125 | 125 | ||
| 126 | </project> | 126 | </project> |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.infra.api.websocket; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 4 | +import org.springframework.stereotype.Component; | ||
| 5 | + | ||
| 6 | +import javax.annotation.Resource; | ||
| 7 | + | ||
| 8 | +/** | ||
| 9 | + * WebSocket 发送器的 API 实现类 | ||
| 10 | + * | ||
| 11 | + * @author 芋道源码 | ||
| 12 | + */ | ||
| 13 | +@Component | ||
| 14 | +public class WebSocketSenderApiImpl implements WebSocketSenderApi { | ||
| 15 | + | ||
| 16 | + @Resource | ||
| 17 | + private WebSocketMessageSender webSocketMessageSender; | ||
| 18 | + | ||
| 19 | + @Override | ||
| 20 | + public void send(Integer userType, Long userId, String messageType, String messageContent) { | ||
| 21 | + webSocketMessageSender.send(userType, userId, messageType, messageContent); | ||
| 22 | + } | ||
| 23 | + | ||
| 24 | + @Override | ||
| 25 | + public void send(Integer userType, String messageType, String messageContent) { | ||
| 26 | + webSocketMessageSender.send(userType, messageType, messageContent); | ||
| 27 | + } | ||
| 28 | + | ||
| 29 | + @Override | ||
| 30 | + public void send(String sessionId, String messageType, String messageContent) { | ||
| 31 | + webSocketMessageSender.send(sessionId, messageType, messageContent); | ||
| 32 | + } | ||
| 33 | + | ||
| 34 | +} |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.infra.websocket; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||
| 4 | +import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; | ||
| 5 | +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 6 | +import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; | ||
| 7 | +import cn.iocoder.yudao.module.infra.websocket.message.DemoReceiveMessage; | ||
| 8 | +import cn.iocoder.yudao.module.infra.websocket.message.DemoSendMessage; | ||
| 9 | +import org.springframework.stereotype.Component; | ||
| 10 | +import org.springframework.web.socket.WebSocketSession; | ||
| 11 | + | ||
| 12 | +import javax.annotation.Resource; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * WebSocket 示例:单发消息 | ||
| 16 | + * | ||
| 17 | + * @author 芋道源码 | ||
| 18 | + */ | ||
| 19 | +@Component | ||
| 20 | +public class DemoWebSocketMessageListener implements WebSocketMessageListener<DemoSendMessage> { | ||
| 21 | + | ||
| 22 | + @Resource | ||
| 23 | + private WebSocketMessageSender webSocketMessageSender; | ||
| 24 | + | ||
| 25 | + @Override | ||
| 26 | + public void onMessage(WebSocketSession session, DemoSendMessage message) { | ||
| 27 | + Long fromUserId = WebSocketFrameworkUtils.getLoginUserId(session); | ||
| 28 | + // 情况一:单发 | ||
| 29 | + if (message.getToUserId() != null) { | ||
| 30 | + DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId) | ||
| 31 | + .setText(message.getText()).setSingle(true); | ||
| 32 | + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), message.getToUserId(), // 给指定用户 | ||
| 33 | + "demo-message-receive", toMessage); | ||
| 34 | + return; | ||
| 35 | + } | ||
| 36 | + // 情况二:群发 | ||
| 37 | + DemoReceiveMessage toMessage = new DemoReceiveMessage().setFromUserId(fromUserId) | ||
| 38 | + .setText(message.getText()).setSingle(false); | ||
| 39 | + webSocketMessageSender.sendObject(UserTypeEnum.ADMIN.getValue(), // 给所有用户 | ||
| 40 | + "demo-message-receive", toMessage); | ||
| 41 | + } | ||
| 42 | + | ||
| 43 | + @Override | ||
| 44 | + public String getType() { | ||
| 45 | + return "demo-message-send"; | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | +} |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/SemaphoreUtils.java deleted
| 1 | -package cn.iocoder.yudao.module.infra.websocket; | ||
| 2 | - | ||
| 3 | -import lombok.extern.slf4j.Slf4j; | ||
| 4 | - | ||
| 5 | -import java.util.concurrent.Semaphore; | ||
| 6 | - | ||
| 7 | -/** | ||
| 8 | - * 信号量相关处理 | ||
| 9 | - * | ||
| 10 | - */ | ||
| 11 | -@Slf4j | ||
| 12 | -public class SemaphoreUtils { | ||
| 13 | - | ||
| 14 | - /** | ||
| 15 | - * 获取信号量 | ||
| 16 | - * | ||
| 17 | - * @param semaphore | ||
| 18 | - * @return | ||
| 19 | - */ | ||
| 20 | - public static boolean tryAcquire(Semaphore semaphore) { | ||
| 21 | - boolean flag = false; | ||
| 22 | - | ||
| 23 | - try { | ||
| 24 | - flag = semaphore.tryAcquire(); | ||
| 25 | - } catch (Exception e) { | ||
| 26 | - log.error("获取信号量异常", e); | ||
| 27 | - } | ||
| 28 | - | ||
| 29 | - return flag; | ||
| 30 | - } | ||
| 31 | - | ||
| 32 | - /** | ||
| 33 | - * 释放信号量 | ||
| 34 | - * | ||
| 35 | - * @param semaphore | ||
| 36 | - */ | ||
| 37 | - public static void release(Semaphore semaphore) { | ||
| 38 | - | ||
| 39 | - try { | ||
| 40 | - semaphore.release(); | ||
| 41 | - } catch (Exception e) { | ||
| 42 | - log.error("释放信号量异常", e); | ||
| 43 | - } | ||
| 44 | - } | ||
| 45 | -} |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketConfig.java deleted
| 1 | -package cn.iocoder.yudao.module.infra.websocket; | ||
| 2 | - | ||
| 3 | -import org.springframework.context.annotation.Bean; | ||
| 4 | -import org.springframework.context.annotation.Configuration; | ||
| 5 | -import org.springframework.web.socket.server.standard.ServerEndpointExporter; | ||
| 6 | - | ||
| 7 | -/** | ||
| 8 | - * websocket 配置 | ||
| 9 | - */ | ||
| 10 | -@Configuration | ||
| 11 | -public class WebSocketConfig { | ||
| 12 | - @Bean | ||
| 13 | - public ServerEndpointExporter serverEndpointExporter() { | ||
| 14 | - return new ServerEndpointExporter(); | ||
| 15 | - } | ||
| 16 | -} |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketServer.java deleted
| 1 | -package cn.iocoder.yudao.module.infra.websocket; | ||
| 2 | - | ||
| 3 | -import lombok.extern.slf4j.Slf4j; | ||
| 4 | -import org.springframework.stereotype.Component; | ||
| 5 | - | ||
| 6 | -import javax.websocket.*; | ||
| 7 | -import javax.websocket.server.ServerEndpoint; | ||
| 8 | -import java.util.concurrent.Semaphore; | ||
| 9 | - | ||
| 10 | -/** | ||
| 11 | - * websocket 消息处理 | ||
| 12 | - */ | ||
| 13 | -@Component | ||
| 14 | -@ServerEndpoint("/websocket/message") | ||
| 15 | -@Slf4j | ||
| 16 | -public class WebSocketServer { | ||
| 17 | - | ||
| 18 | - /** | ||
| 19 | - * 默认最多允许同时在线用户数100 | ||
| 20 | - */ | ||
| 21 | - public static int socketMaxOnlineCount = 100; | ||
| 22 | - | ||
| 23 | - private static final Semaphore SOCKET_SEMAPHORE = new Semaphore(socketMaxOnlineCount); | ||
| 24 | - | ||
| 25 | - /** | ||
| 26 | - * 连接建立成功调用的方法 | ||
| 27 | - */ | ||
| 28 | - @OnOpen | ||
| 29 | - public void onOpen(Session session) throws Exception { | ||
| 30 | - // 尝试获取信号量 | ||
| 31 | - boolean semaphoreFlag = SemaphoreUtils.tryAcquire(SOCKET_SEMAPHORE); | ||
| 32 | - if (!semaphoreFlag) { | ||
| 33 | - // 未获取到信号量 | ||
| 34 | - log.error("当前在线人数超过限制数:{}", socketMaxOnlineCount); | ||
| 35 | - WebSocketUsers.sendMessage(session, "当前在线人数超过限制数:" + socketMaxOnlineCount); | ||
| 36 | - session.close(); | ||
| 37 | - } else { | ||
| 38 | - String userId = WebSocketUsers.getParam("userId", session); | ||
| 39 | - if (userId != null) { | ||
| 40 | - // 添加用户 | ||
| 41 | - WebSocketUsers.addSession(userId, session); | ||
| 42 | - log.info("用户【userId={}】建立连接,当前连接用户总数:{}", userId, WebSocketUsers.getUsers().size()); | ||
| 43 | - WebSocketUsers.sendMessage(session, "接收内容:连接成功"); | ||
| 44 | - } else { | ||
| 45 | - WebSocketUsers.sendMessage(session, "接收内容:连接失败"); | ||
| 46 | - } | ||
| 47 | - } | ||
| 48 | - } | ||
| 49 | - | ||
| 50 | - /** | ||
| 51 | - * 连接关闭时处理 | ||
| 52 | - */ | ||
| 53 | - @OnClose | ||
| 54 | - public void onClose(Session session) { | ||
| 55 | - log.info("用户【sessionId={}】关闭连接!", session.getId()); | ||
| 56 | - // 移除用户 | ||
| 57 | - WebSocketUsers.removeSession(session); | ||
| 58 | - // 获取到信号量则需释放 | ||
| 59 | - SemaphoreUtils.release(SOCKET_SEMAPHORE); | ||
| 60 | - } | ||
| 61 | - | ||
| 62 | - /** | ||
| 63 | - * 抛出异常时处理 | ||
| 64 | - */ | ||
| 65 | - @OnError | ||
| 66 | - public void onError(Session session, Throwable exception) throws Exception { | ||
| 67 | - if (session.isOpen()) { | ||
| 68 | - // 关闭连接 | ||
| 69 | - session.close(); | ||
| 70 | - } | ||
| 71 | - String sessionId = session.getId(); | ||
| 72 | - log.info("用户【sessionId={}】连接异常!异常信息:{}", sessionId, exception); | ||
| 73 | - // 移出用户 | ||
| 74 | - WebSocketUsers.removeSession(session); | ||
| 75 | - // 获取到信号量则需释放 | ||
| 76 | - SemaphoreUtils.release(SOCKET_SEMAPHORE); | ||
| 77 | - } | ||
| 78 | - | ||
| 79 | - /** | ||
| 80 | - * 收到客户端消息时调用的方法 | ||
| 81 | - */ | ||
| 82 | - @OnMessage | ||
| 83 | - public void onMessage(Session session, String message) { | ||
| 84 | - WebSocketUsers.sendMessage(session, "接收内容:" + message); | ||
| 85 | - } | ||
| 86 | -} |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/WebSocketUsers.java deleted
| 1 | -package cn.iocoder.yudao.module.infra.websocket; | ||
| 2 | - | ||
| 3 | -import cn.hutool.core.map.MapUtil; | ||
| 4 | -import cn.hutool.core.util.StrUtil; | ||
| 5 | -import lombok.extern.slf4j.Slf4j; | ||
| 6 | -import org.bouncycastle.util.Strings; | ||
| 7 | - | ||
| 8 | -import javax.validation.constraints.NotNull; | ||
| 9 | -import javax.websocket.Session; | ||
| 10 | -import java.io.IOException; | ||
| 11 | -import java.util.List; | ||
| 12 | -import java.util.Map; | ||
| 13 | -import java.util.Set; | ||
| 14 | -import java.util.concurrent.ConcurrentHashMap; | ||
| 15 | - | ||
| 16 | -/** | ||
| 17 | - * websocket 客户端用户 | ||
| 18 | - */ | ||
| 19 | -@Slf4j | ||
| 20 | -public class WebSocketUsers { | ||
| 21 | - | ||
| 22 | - /** | ||
| 23 | - * 用户集 | ||
| 24 | - * TODO 需要登录用户的session? | ||
| 25 | - */ | ||
| 26 | - private static final Map<String, Session> SESSION_MAP = new ConcurrentHashMap<>(); | ||
| 27 | - | ||
| 28 | - /** | ||
| 29 | - * 存储用户 | ||
| 30 | - * | ||
| 31 | - * @param userId 唯一键 | ||
| 32 | - * @param session 用户信息 | ||
| 33 | - */ | ||
| 34 | - public static void addSession(String userId, Session session) { | ||
| 35 | - SESSION_MAP.put(userId, session); | ||
| 36 | - } | ||
| 37 | - | ||
| 38 | - /** | ||
| 39 | - * 移除用户 | ||
| 40 | - * | ||
| 41 | - * @param session 用户信息 | ||
| 42 | - * @return 移除结果 | ||
| 43 | - */ | ||
| 44 | - public static boolean removeSession(Session session) { | ||
| 45 | - String key = null; | ||
| 46 | - boolean flag = SESSION_MAP.containsValue(session); | ||
| 47 | - if (flag) { | ||
| 48 | - Set<Map.Entry<String, Session>> entries = SESSION_MAP.entrySet(); | ||
| 49 | - for (Map.Entry<String, Session> entry : entries) { | ||
| 50 | - Session value = entry.getValue(); | ||
| 51 | - if (value.equals(session)) { | ||
| 52 | - key = entry.getKey(); | ||
| 53 | - break; | ||
| 54 | - } | ||
| 55 | - } | ||
| 56 | - } else { | ||
| 57 | - return true; | ||
| 58 | - } | ||
| 59 | - return removeSession(key); | ||
| 60 | - } | ||
| 61 | - | ||
| 62 | - /** | ||
| 63 | - * 移出用户 | ||
| 64 | - * | ||
| 65 | - * @param userId 用户id | ||
| 66 | - */ | ||
| 67 | - public static boolean removeSession(String userId) { | ||
| 68 | - log.info("用户【userId={}】退出", userId); | ||
| 69 | - Session remove = SESSION_MAP.remove(userId); | ||
| 70 | - if (remove != null) { | ||
| 71 | - boolean containsValue = SESSION_MAP.containsValue(remove); | ||
| 72 | - log.info("用户【userId={}】退出{},当前连接用户总数:{}", userId, containsValue ? "失败" : "成功", SESSION_MAP.size()); | ||
| 73 | - return containsValue; | ||
| 74 | - } else { | ||
| 75 | - return true; | ||
| 76 | - } | ||
| 77 | - } | ||
| 78 | - | ||
| 79 | - /** | ||
| 80 | - * 获取在线用户列表 | ||
| 81 | - * | ||
| 82 | - * @return 返回用户集合 | ||
| 83 | - */ | ||
| 84 | - public static Map<String, Session> getUsers() { | ||
| 85 | - return SESSION_MAP; | ||
| 86 | - } | ||
| 87 | - | ||
| 88 | - /** | ||
| 89 | - * 向所有在线人发送消息 | ||
| 90 | - * | ||
| 91 | - * @param message 消息内容 | ||
| 92 | - */ | ||
| 93 | - public static void sendMessageToAll(String message) { | ||
| 94 | - SESSION_MAP.forEach((userId, session) -> { | ||
| 95 | - if (session.isOpen()) { | ||
| 96 | - sendMessage(session, message); | ||
| 97 | - } | ||
| 98 | - }); | ||
| 99 | - } | ||
| 100 | - | ||
| 101 | - /** | ||
| 102 | - * 异步发送文本消息 | ||
| 103 | - * | ||
| 104 | - * @param session 用户session | ||
| 105 | - * @param message 消息内容 | ||
| 106 | - */ | ||
| 107 | - public static void sendMessageAsync(Session session, String message) { | ||
| 108 | - if (session.isOpen()) { | ||
| 109 | - // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程? | ||
| 110 | - session.getAsyncRemote().sendText(message); | ||
| 111 | - } else { | ||
| 112 | - log.warn("用户【session={}】不在线", session.getId()); | ||
| 113 | - } | ||
| 114 | - } | ||
| 115 | - | ||
| 116 | - /** | ||
| 117 | - * 同步发送文本消息 | ||
| 118 | - * | ||
| 119 | - * @param session 用户session | ||
| 120 | - * @param message 消息内容 | ||
| 121 | - */ | ||
| 122 | - public static void sendMessage(Session session, String message) { | ||
| 123 | - try { | ||
| 124 | - if (session.isOpen()) { | ||
| 125 | - // TODO 需要加synchronized锁(synchronized(session))?单个session创建线程? | ||
| 126 | - session.getBasicRemote().sendText(message); | ||
| 127 | - } else { | ||
| 128 | - log.warn("用户【session={}】不在线", session.getId()); | ||
| 129 | - } | ||
| 130 | - } catch (IOException e) { | ||
| 131 | - log.error("发送消息异常", e); | ||
| 132 | - } | ||
| 133 | - | ||
| 134 | - } | ||
| 135 | - | ||
| 136 | - /** | ||
| 137 | - * 根据用户id发送消息 | ||
| 138 | - * | ||
| 139 | - * @param userId 用户id | ||
| 140 | - * @param message 消息内容 | ||
| 141 | - */ | ||
| 142 | - public static void sendMessage(String userId, String message) { | ||
| 143 | - Session session = SESSION_MAP.get(userId); | ||
| 144 | - //判断是否存在该用户的session,并且是否在线 | ||
| 145 | - if (session == null || !session.isOpen()) { | ||
| 146 | - return; | ||
| 147 | - } | ||
| 148 | - sendMessage(session, message); | ||
| 149 | - } | ||
| 150 | - | ||
| 151 | - | ||
| 152 | - /** | ||
| 153 | - * 获取session中的指定参数值 | ||
| 154 | - * | ||
| 155 | - * @param key 参数key | ||
| 156 | - * @param session 用户session | ||
| 157 | - */ | ||
| 158 | - public static String getParam(@NotNull String key, Session session) { | ||
| 159 | - //TODO 目前只针对获取一个key的值,后期根据情况拓展多个 或者直接在onClose onOpen上获取参数? | ||
| 160 | - String value = null; | ||
| 161 | - Map<String, List<String>> parameters = session.getRequestParameterMap(); | ||
| 162 | - if (MapUtil.isNotEmpty(parameters)) { | ||
| 163 | - value = parameters.get(key).get(0); | ||
| 164 | - } else { | ||
| 165 | - String queryString = session.getQueryString(); | ||
| 166 | - if (!StrUtil.isEmpty(queryString)) { | ||
| 167 | - String[] params = Strings.split(queryString, '&'); | ||
| 168 | - for (String paramPair : params) { | ||
| 169 | - String[] nameValues = Strings.split(paramPair, '='); | ||
| 170 | - if (key.equals(nameValues[0])) { | ||
| 171 | - value = nameValues[1]; | ||
| 172 | - } | ||
| 173 | - } | ||
| 174 | - } | ||
| 175 | - } | ||
| 176 | - return value; | ||
| 177 | - } | ||
| 178 | -} |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoReceiveMessage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.infra.websocket.message; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 示例:server -> client 同步消息 | ||
| 7 | + * | ||
| 8 | + * @author 芋道源码 | ||
| 9 | + */ | ||
| 10 | +@Data | ||
| 11 | +public class DemoReceiveMessage { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 接收人的编号 | ||
| 15 | + */ | ||
| 16 | + private Long fromUserId; | ||
| 17 | + /** | ||
| 18 | + * 内容 | ||
| 19 | + */ | ||
| 20 | + private String text; | ||
| 21 | + | ||
| 22 | + /** | ||
| 23 | + * 是否单聊 | ||
| 24 | + */ | ||
| 25 | + private Boolean single; | ||
| 26 | + | ||
| 27 | +} |
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/websocket/message/DemoSendMessage.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.infra.websocket.message; | ||
| 2 | + | ||
| 3 | +import lombok.Data; | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * 示例:client -> server 发送消息 | ||
| 7 | + * | ||
| 8 | + * @author 芋道源码 | ||
| 9 | + */ | ||
| 10 | +@Data | ||
| 11 | +public class DemoSendMessage { | ||
| 12 | + | ||
| 13 | + /** | ||
| 14 | + * 发送给谁 | ||
| 15 | + * | ||
| 16 | + * 如果为空,说明发送给所有人 | ||
| 17 | + */ | ||
| 18 | + private Long toUserId; | ||
| 19 | + /** | ||
| 20 | + * 内容 | ||
| 21 | + */ | ||
| 22 | + private String text; | ||
| 23 | + | ||
| 24 | +} |
yudao-module-member/yudao-module-member-biz/src/main/java/cn/iocoder/yudao/module/member/controller/app/auth/AppAuthController.java
| @@ -53,7 +53,8 @@ public class AppAuthController { | @@ -53,7 +53,8 @@ public class AppAuthController { | ||
| 53 | @PermitAll | 53 | @PermitAll |
| 54 | @Operation(summary = "登出系统") | 54 | @Operation(summary = "登出系统") |
| 55 | public CommonResult<Boolean> logout(HttpServletRequest request) { | 55 | public CommonResult<Boolean> logout(HttpServletRequest request) { |
| 56 | - String token = SecurityFrameworkUtils.obtainAuthorization(request, securityProperties.getTokenHeader()); | 56 | + String token = SecurityFrameworkUtils.obtainAuthorization(request, |
| 57 | + securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); | ||
| 57 | if (StrUtil.isNotBlank(token)) { | 58 | if (StrUtil.isNotBlank(token)) { |
| 58 | authService.logout(token); | 59 | authService.logout(token); |
| 59 | } | 60 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java
| @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | @@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||
| 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 7 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 8 | import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; | 8 | import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; |
| 9 | import cn.iocoder.yudao.framework.security.config.SecurityProperties; | 9 | import cn.iocoder.yudao.framework.security.config.SecurityProperties; |
| 10 | +import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; | ||
| 10 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; | 11 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; |
| 11 | import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; | 12 | import cn.iocoder.yudao.module.system.convert.auth.AuthConvert; |
| 12 | import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; | 13 | import cn.iocoder.yudao.module.system.dal.dataobject.permission.MenuDO; |
| @@ -38,7 +39,6 @@ import java.util.Set; | @@ -38,7 +39,6 @@ import java.util.Set; | ||
| 38 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; | 39 | import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; |
| 39 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; | 40 | import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; |
| 40 | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; | 41 | import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; |
| 41 | -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.obtainAuthorization; | ||
| 42 | 42 | ||
| 43 | @Tag(name = "管理后台 - 认证") | 43 | @Tag(name = "管理后台 - 认证") |
| 44 | @RestController | 44 | @RestController |
| @@ -76,7 +76,8 @@ public class AuthController { | @@ -76,7 +76,8 @@ public class AuthController { | ||
| 76 | @Operation(summary = "登出系统") | 76 | @Operation(summary = "登出系统") |
| 77 | @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | 77 | @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 |
| 78 | public CommonResult<Boolean> logout(HttpServletRequest request) { | 78 | public CommonResult<Boolean> logout(HttpServletRequest request) { |
| 79 | - String token = obtainAuthorization(request, securityProperties.getTokenHeader()); | 79 | + String token = SecurityFrameworkUtils.obtainAuthorization(request, |
| 80 | + securityProperties.getTokenHeader(), securityProperties.getTokenParameter()); | ||
| 80 | if (StrUtil.isNotBlank(token)) { | 81 | if (StrUtil.isNotBlank(token)) { |
| 81 | authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); | 82 | authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType()); |
| 82 | } | 83 | } |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/notice/NoticeController.java
| 1 | package cn.iocoder.yudao.module.system.controller.admin.notice; | 1 | package cn.iocoder.yudao.module.system.controller.admin.notice; |
| 2 | 2 | ||
| 3 | +import cn.hutool.core.lang.Assert; | ||
| 4 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||
| 3 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; | 5 | import cn.iocoder.yudao.framework.common.pojo.CommonResult; |
| 4 | import cn.iocoder.yudao.framework.common.pojo.PageResult; | 6 | import cn.iocoder.yudao.framework.common.pojo.PageResult; |
| 7 | +import cn.iocoder.yudao.module.infra.api.websocket.WebSocketSenderApi; | ||
| 5 | import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO; | 8 | import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeCreateReqVO; |
| 6 | import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO; | 9 | import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticePageReqVO; |
| 7 | import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeRespVO; | 10 | import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeRespVO; |
| 8 | import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; | 11 | import cn.iocoder.yudao.module.system.controller.admin.notice.vo.NoticeUpdateReqVO; |
| 9 | import cn.iocoder.yudao.module.system.convert.notice.NoticeConvert; | 12 | import cn.iocoder.yudao.module.system.convert.notice.NoticeConvert; |
| 13 | +import cn.iocoder.yudao.module.system.dal.dataobject.notice.NoticeDO; | ||
| 10 | import cn.iocoder.yudao.module.system.service.notice.NoticeService; | 14 | import cn.iocoder.yudao.module.system.service.notice.NoticeService; |
| 11 | import io.swagger.v3.oas.annotations.tags.Tag; | 15 | import io.swagger.v3.oas.annotations.tags.Tag; |
| 12 | import io.swagger.v3.oas.annotations.Parameter; | 16 | import io.swagger.v3.oas.annotations.Parameter; |
| @@ -29,6 +33,9 @@ public class NoticeController { | @@ -29,6 +33,9 @@ public class NoticeController { | ||
| 29 | @Resource | 33 | @Resource |
| 30 | private NoticeService noticeService; | 34 | private NoticeService noticeService; |
| 31 | 35 | ||
| 36 | + @Resource | ||
| 37 | + private WebSocketSenderApi webSocketSenderApi; | ||
| 38 | + | ||
| 32 | @PostMapping("/create") | 39 | @PostMapping("/create") |
| 33 | @Operation(summary = "创建通知公告") | 40 | @Operation(summary = "创建通知公告") |
| 34 | @PreAuthorize("@ss.hasPermission('system:notice:create')") | 41 | @PreAuthorize("@ss.hasPermission('system:notice:create')") |
| @@ -69,4 +76,16 @@ public class NoticeController { | @@ -69,4 +76,16 @@ public class NoticeController { | ||
| 69 | return success(NoticeConvert.INSTANCE.convert(noticeService.getNotice(id))); | 76 | return success(NoticeConvert.INSTANCE.convert(noticeService.getNotice(id))); |
| 70 | } | 77 | } |
| 71 | 78 | ||
| 79 | + @PostMapping("/push") | ||
| 80 | + @Operation(summary = "推送通知公告", description = "只发送给 websocket 连接在线的用户") | ||
| 81 | + @Parameter(name = "id", description = "编号", required = true, example = "1024") | ||
| 82 | + @PreAuthorize("@ss.hasPermission('system:notice:update')") | ||
| 83 | + public CommonResult<Boolean> push(@RequestParam("id") Long id) { | ||
| 84 | + NoticeDO notice = noticeService.getNotice(id); | ||
| 85 | + Assert.notNull(notice, "公告不能为空"); | ||
| 86 | + // 通过 websocket 推送给在线的用户 | ||
| 87 | + webSocketSenderApi.sendObject(UserTypeEnum.ADMIN.getValue(), "notice-push", notice); | ||
| 88 | + return success(true); | ||
| 89 | + } | ||
| 90 | + | ||
| 72 | } | 91 | } |
yudao-server/src/main/resources/application-local.yaml
| @@ -123,8 +123,8 @@ spring: | @@ -123,8 +123,8 @@ spring: | ||
| 123 | rabbitmq: | 123 | rabbitmq: |
| 124 | host: 127.0.0.1 # RabbitMQ 服务的地址 | 124 | host: 127.0.0.1 # RabbitMQ 服务的地址 |
| 125 | port: 5672 # RabbitMQ 服务的端口 | 125 | port: 5672 # RabbitMQ 服务的端口 |
| 126 | - username: guest # RabbitMQ 服务的账号 | ||
| 127 | - password: guest # RabbitMQ 服务的密码 | 126 | + username: rabbit # RabbitMQ 服务的账号 |
| 127 | + password: rabbit # RabbitMQ 服务的密码 | ||
| 128 | # Kafka 配置项,对应 KafkaProperties 配置类 | 128 | # Kafka 配置项,对应 KafkaProperties 配置类 |
| 129 | kafka: | 129 | kafka: |
| 130 | bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 | 130 | bootstrap-servers: 127.0.0.1:9092 # 指定 Kafka Broker 地址,可以设置多个,以逗号分隔 |
yudao-server/src/main/resources/application.yaml
| @@ -146,9 +146,17 @@ yudao: | @@ -146,9 +146,17 @@ yudao: | ||
| 146 | - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 | 146 | - /admin-api/mp/open/** # 微信公众号开放平台,微信回调接口,不需要登录 |
| 147 | websocket: | 147 | websocket: |
| 148 | enable: true # websocket的开关 | 148 | enable: true # websocket的开关 |
| 149 | - path: /websocket/message # 路径 | ||
| 150 | - maxOnlineCount: 0 # 最大连接人数 | ||
| 151 | - sessionMap: true # 保存sessionMap | 149 | + path: /infra/ws # 路径 |
| 150 | + sender-type: local # 消息发送的类型,可选值为 local、redis、rocketmq、kafka、rabbitmq | ||
| 151 | + sender-rocketmq: | ||
| 152 | + topic: ${spring.application.name}-websocket # 消息发送的 RocketMQ Topic | ||
| 153 | + consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 RocketMQ Consumer Group | ||
| 154 | + sender-rabbitmq: | ||
| 155 | + exchange: ${spring.application.name}-websocket-exchange # 消息发送的 RabbitMQ Exchange | ||
| 156 | + queue: ${spring.application.name}-websocket-queue # 消息发送的 RabbitMQ Queue | ||
| 157 | + sender-kafka: | ||
| 158 | + topic: ${spring.application.name}-websocket # 消息发送的 Kafka Topic | ||
| 159 | + consumer-group: ${spring.application.name}-websocket-consumer # 消息发送的 Kafka Consumer Group | ||
| 152 | swagger: | 160 | swagger: |
| 153 | title: 芋道快速开发平台 | 161 | title: 芋道快速开发平台 |
| 154 | description: 提供管理后台、用户 App 的所有功能 | 162 | description: 提供管理后台、用户 App 的所有功能 |