feat(websocket): 重构 WebSocket 实现数据推送
- 新增 DataPushTask 类实现定时推送数据- 重构 WebSocketConfig 配置类 - 新增 DeviceWebSocketHandler、RecordWebSocketHandler 和 ProductWebSocketHandler 处理不同类型的 WebSocket 连接 - 移除 GenericWebSocketHandler 和 WebSocketInterceptor 类 - 更新 IotGatewayApplication 启用定时任务 - 调整 SpringSecurityConfig 允许 WebSocket 请求通过
This commit is contained in:
parent
42a9f9490f
commit
11d972e875
@ -3,9 +3,11 @@ package com.zsc.edu.gateway;
|
|||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
import org.springframework.context.annotation.EnableAspectJAutoProxy;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
@EnableAspectJAutoProxy
|
@EnableAspectJAutoProxy
|
||||||
|
@EnableScheduling
|
||||||
public class IotGatewayApplication {
|
public class IotGatewayApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
package com.zsc.edu.gateway.framework.message.websocket;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.socket.TextMessage;
|
|
||||||
import org.springframework.web.socket.WebSocketSession;
|
|
||||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.concurrent.ExecutorService;
|
|
||||||
import java.util.concurrent.Executors;
|
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author zhuang
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class GenericWebSocketHandler extends TextWebSocketHandler {
|
|
||||||
|
|
||||||
private final ExecutorService executorService = Executors.newCachedThreadPool();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
|
||||||
// 从 session 中获取业务逻辑的 Supplier
|
|
||||||
Supplier<Object> dataSupplier = (Supplier<Object>) session.getAttributes().get("dataSupplier");
|
|
||||||
|
|
||||||
if (dataSupplier == null) {
|
|
||||||
session.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
AtomicBoolean isCompleted = new AtomicBoolean(false);
|
|
||||||
|
|
||||||
executorService.execute(() -> {
|
|
||||||
try {
|
|
||||||
while (!isCompleted.get()) {
|
|
||||||
Object data = dataSupplier.get();
|
|
||||||
if (data == null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (!isCompleted.get()) {
|
|
||||||
session.sendMessage(new TextMessage(data.toString()));
|
|
||||||
}
|
|
||||||
Thread.sleep(5000); // 每隔 5 秒发送一次数据
|
|
||||||
}
|
|
||||||
} catch (IOException | InterruptedException e) {
|
|
||||||
try {
|
|
||||||
session.close();
|
|
||||||
} catch (IOException ex) {
|
|
||||||
ex.printStackTrace();
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
try {
|
|
||||||
session.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
package com.zsc.edu.gateway.framework.message.websocket;
|
|
||||||
|
|
||||||
import org.springframework.context.annotation.Configuration;
|
|
||||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
|
||||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author zhuang
|
|
||||||
*/
|
|
||||||
@Configuration
|
|
||||||
@EnableWebSocket
|
|
||||||
public class WebSocketConfig implements WebSocketConfigurer {
|
|
||||||
|
|
||||||
private final GenericWebSocketHandler genericWebSocketHandler;
|
|
||||||
|
|
||||||
public WebSocketConfig(GenericWebSocketHandler genericWebSocketHandler) {
|
|
||||||
this.genericWebSocketHandler = genericWebSocketHandler;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
|
||||||
registry.addHandler(genericWebSocketHandler, "/api/rest/device/ws/device/status", "/api/rest/device/ws/record/status", "/api/rest/product/ws/product/status")
|
|
||||||
.setAllowedOrigins("*")
|
|
||||||
.addInterceptors(new WebSocketInterceptor());
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
package com.zsc.edu.gateway.framework.message.websocket;
|
|
||||||
|
|
||||||
import com.zsc.edu.gateway.modules.iot.device.service.DeviceService;
|
|
||||||
import com.zsc.edu.gateway.modules.iot.product.service.ProductService;
|
|
||||||
import com.zsc.edu.gateway.modules.iot.record.service.RecordDataService;
|
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
|
||||||
import org.springframework.http.server.ServerHttpRequest;
|
|
||||||
import org.springframework.http.server.ServerHttpResponse;
|
|
||||||
import org.springframework.http.server.ServletServerHttpRequest;
|
|
||||||
import org.springframework.web.socket.WebSocketHandler;
|
|
||||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @author lenovo
|
|
||||||
*/
|
|
||||||
public class WebSocketInterceptor implements HandshakeInterceptor {
|
|
||||||
@Resource
|
|
||||||
private DeviceService deviceService;
|
|
||||||
@Resource
|
|
||||||
private RecordDataService recordService;
|
|
||||||
@Resource
|
|
||||||
private ProductService productService;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
|
||||||
if (request instanceof ServletServerHttpRequest) {
|
|
||||||
HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest();
|
|
||||||
// 获取完整路径
|
|
||||||
String path = servletRequest.getRequestURI();
|
|
||||||
|
|
||||||
// 根据路径设置不同的业务逻辑 Supplier
|
|
||||||
switch (path) {
|
|
||||||
case "/api/rest/device/ws/device/status":
|
|
||||||
attributes.put("dataSupplier", (Supplier<String>) () -> String.valueOf(deviceService.status()));
|
|
||||||
break;
|
|
||||||
case "/api/rest/device/ws/record/status":
|
|
||||||
attributes.put("dataSupplier", (Supplier<String>) () -> String.valueOf(recordService.recordDataStatus()));
|
|
||||||
break;
|
|
||||||
case "/api/rest/product/ws/product/status":
|
|
||||||
attributes.put("dataSupplier", (Supplier<String>) () -> String.valueOf(productService.status()));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
attributes.put("dataSupplier", (Supplier<String>) () -> "Unknown path: " + path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
|
|
||||||
// 握手完成后不需要额外操作
|
|
||||||
}
|
|
||||||
}
|
|
@ -88,6 +88,7 @@ public class SpringSecurityConfig {
|
|||||||
.requestMatchers(HttpMethod.GET, "/api/rest/user/menu","/api/rest/user/register","/api/rest/user/send-email").permitAll()
|
.requestMatchers(HttpMethod.GET, "/api/rest/user/menu","/api/rest/user/register","/api/rest/user/send-email").permitAll()
|
||||||
.requestMatchers(HttpMethod.POST, "/api/rest/user/login","/api/rest/user/register").permitAll()
|
.requestMatchers(HttpMethod.POST, "/api/rest/user/login","/api/rest/user/register").permitAll()
|
||||||
.requestMatchers("/api/rest/user/me").permitAll()
|
.requestMatchers("/api/rest/user/me").permitAll()
|
||||||
|
.requestMatchers("/api/rest/ws/**").permitAll()
|
||||||
.requestMatchers("/api/**").authenticated()
|
.requestMatchers("/api/**").authenticated()
|
||||||
)
|
)
|
||||||
// 不用注解,直接通过判断路径实现动态访问权限
|
// 不用注解,直接通过判断路径实现动态访问权限
|
||||||
@ -144,7 +145,7 @@ public class SpringSecurityConfig {
|
|||||||
.rememberMe(rememberMe -> rememberMe
|
.rememberMe(rememberMe -> rememberMe
|
||||||
.userDetailsService(userDetailsService)
|
.userDetailsService(userDetailsService)
|
||||||
.tokenRepository(persistentTokenRepository()))
|
.tokenRepository(persistentTokenRepository()))
|
||||||
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/internal/**", "/api/rest/user/logout","/api/rest/user/register"))
|
.csrf(csrf -> csrf.ignoringRequestMatchers("/api/internal/**", "/api/rest/user/logout", "/api/rest/user/register", "/api/rest/ws/**"))
|
||||||
.sessionManagement(session -> session
|
.sessionManagement(session -> session
|
||||||
.maximumSessions(3)
|
.maximumSessions(3)
|
||||||
.sessionRegistry(sessionRegistry)
|
.sessionRegistry(sessionRegistry)
|
||||||
|
@ -0,0 +1,46 @@
|
|||||||
|
package com.zsc.edu.gateway.framework.websocket;
|
||||||
|
|
||||||
|
import org.springframework.web.socket.CloseStatus;
|
||||||
|
import org.springframework.web.socket.TextMessage;
|
||||||
|
import org.springframework.web.socket.WebSocketSession;
|
||||||
|
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
|
||||||
|
public abstract class BaseWebSocketHandler extends TextWebSocketHandler {
|
||||||
|
private final Set<WebSocketSession> sessions = new CopyOnWriteArraySet<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(WebSocketSession session) {
|
||||||
|
sessions.add(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
|
||||||
|
sessions.remove(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void broadcast(String message) {
|
||||||
|
sessions.removeIf(session -> {
|
||||||
|
if (!session.isOpen()) return true;
|
||||||
|
try {
|
||||||
|
session.sendMessage(new TextMessage(message));
|
||||||
|
} catch (IOException e) {
|
||||||
|
// 生产环境应使用日志记录
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeviceWebSocketHandler extends BaseWebSocketHandler {
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProductWebSocketHandler extends BaseWebSocketHandler {
|
||||||
|
}
|
||||||
|
|
||||||
|
class RecordWebSocketHandler extends BaseWebSocketHandler {
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package com.zsc.edu.gateway.framework.websocket;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.zsc.edu.gateway.modules.iot.device.service.DeviceService;
|
||||||
|
import com.zsc.edu.gateway.modules.iot.product.service.ProductService;
|
||||||
|
import com.zsc.edu.gateway.modules.iot.record.service.RecordDataService;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DataPushTask {
|
||||||
|
private final DeviceService deviceService;
|
||||||
|
private final RecordDataService recordDataService;
|
||||||
|
private final ProductService productService;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
private final DeviceWebSocketHandler deviceHandler;
|
||||||
|
private final RecordWebSocketHandler recordHandler;
|
||||||
|
private final ProductWebSocketHandler productHandler;
|
||||||
|
|
||||||
|
@Scheduled(fixedRate = 20000)
|
||||||
|
public void pushData() {
|
||||||
|
pushAndBroadcast(deviceService::status, deviceHandler);
|
||||||
|
pushAndBroadcast(recordDataService::recordDataStatus, recordHandler);
|
||||||
|
pushAndBroadcast(productService::status, productHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void pushAndBroadcast(Supplier<Object> dataSupplier, BaseWebSocketHandler handler) {
|
||||||
|
try {
|
||||||
|
Object status = dataSupplier.get();
|
||||||
|
String json = objectMapper.writeValueAsString(status);
|
||||||
|
handler.broadcast(json);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 生产环境应使用日志记录
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
package com.zsc.edu.gateway.framework.websocket;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocket
|
||||||
|
public class WebSocketConfig implements WebSocketConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||||
|
registry.addHandler(deviceWebSocketHandler(), "/api/rest/ws/device/status")
|
||||||
|
.setAllowedOrigins("*");
|
||||||
|
registry.addHandler(recordWebSocketHandler(), "/api/rest/ws/record/status")
|
||||||
|
.setAllowedOrigins("*");
|
||||||
|
registry.addHandler(productWebSocketHandler(), "/api/rest/ws/product/status")
|
||||||
|
.setAllowedOrigins("*");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public DeviceWebSocketHandler deviceWebSocketHandler() {
|
||||||
|
return new DeviceWebSocketHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RecordWebSocketHandler recordWebSocketHandler() {
|
||||||
|
return new RecordWebSocketHandler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ProductWebSocketHandler productWebSocketHandler() {
|
||||||
|
return new ProductWebSocketHandler();
|
||||||
|
}
|
||||||
|
}
|
@ -12,13 +12,13 @@ import java.util.List;
|
|||||||
* @author zhuang
|
* @author zhuang
|
||||||
*/
|
*/
|
||||||
public interface RecordDataRepository extends BaseMapper<RecordData> {
|
public interface RecordDataRepository extends BaseMapper<RecordData> {
|
||||||
@Select("SELECT COUNT(*) FROM iot_record_data WHERE content->>'warning' IS NOT NULL AND (content->>'warning')::int & 1 = 0 AND ((content->>'warning')::int & ~1) > 0")
|
@Select("SELECT COUNT(*) FROM iot_record_data WHERE content::jsonb->>'warning' IS NOT NULL AND (content::jsonb->>'warning')::int & 1 = 0 AND ((content::jsonb->>'warning')::int & ~1) > 0")
|
||||||
long countWarnings();
|
long countWarnings();
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM iot_record_data WHERE content->>'warning' IS NOT NULL AND ((content->>'warning')::int & 1 = 0) AND ((content->>'warning')::int & ~1) > 0 AND record_time >= #{todayStart}")
|
@Select("SELECT COUNT(*) FROM iot_record_data WHERE content::jsonb->>'warning' IS NOT NULL AND ((content::jsonb->>'warning')::int & 1 = 0) AND ((content::jsonb->>'warning')::int & ~1) > 0 AND record_time >= #{todayStart}")
|
||||||
long countTodayWarnings(@Param("todayStart") LocalDateTime todayStart);
|
long countTodayWarnings(@Param("todayStart") LocalDateTime todayStart);
|
||||||
|
|
||||||
@Select("SELECT COUNT(*) FROM iot_record_data WHERE content->>'warning' IS NOT NULL AND (content->>'warning')::int & #{bitPosition} = #{bitPosition}")
|
@Select("SELECT COUNT(*) FROM iot_record_data WHERE content::jsonb->>'warning' IS NOT NULL AND (content::jsonb->>'warning')::int & #{bitPosition} = #{bitPosition}")
|
||||||
long countWarningsByBit(@Param("bitPosition") int bitPosition);
|
long countWarningsByBit(@Param("bitPosition") int bitPosition);
|
||||||
|
|
||||||
List<RecordData> selectByClientId(@Param("clientId") String clientId);
|
List<RecordData> selectByClientId(@Param("clientId") String clientId);
|
||||||
|
Loading…
Reference in New Issue
Block a user