SpringBoot集成Websocket实现后台消息推送给前端
1.pom依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/net.sf.json-lib/json-lib -->
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.3</version>
<classifier>jdk15</classifier>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2.Websocket相关类
2.1.Websocket配置类
package com.alcode.websocket.config;
import com.alcode.websocket.handler.MyWebSocketHandler;
import com.alcode.websocket.interceptor.WebSocketInterceptor;
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;
/**
* @ClassName: WebSocketConfig
* @Description: websocket配置类
* @Author: Jane
* @Date: 2020/7/4 12:58
* @Version: V1.0
**/
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
//handler是webSocket的核心,配置入口,设置跨域允许
registry.addHandler(new MyWebSocketHandler(), "/websocket/{id}").setAllowedOrigins("*").addInterceptors(new WebSocketInterceptor());
}
}
2.2.配置拦截器
配置一个拦截器,所有请去访问时先经过拦截器
package com.alcode.websocket.interceptor;
import lombok.extern.slf4j.Slf4j;
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;
/**
* @ClassName: WebSocketInterceptor
* @Description: websocket拦截器
* @Author: Jane
* @Date: 2020/7/4 13:14
* @Version: V1.0
**/
@Slf4j
public class WebSocketInterceptor implements HandshakeInterceptor {
/**
* 在握手之前执行该方法, 继续握手返回true, 中断握手返回false. 通过attributes参数设置WebSocketSession的属性
*
* @param request
* @param response
* @param webSocketHandler
* @param attributes
* @return
* @throws Exception
*/
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler webSocketHandler, Map<String, Object> attributes) throws Exception {
if (request instanceof ServletServerHttpRequest){
// 这里获取用户id,用这个id知道是谁发的消息和给这个用户发消息
String id = request.getURI().toString().split("id=")[1];
log.info("当前按session的id={}", id);
// 添加用户
attributes.put("WEBSOCKET_USER_ID", id);
}
return true;
}
/**
* 完成握手后执行该方法
*
* @param serverHttpRequest
* @param serverHttpResponse
* @param webSocketHandler
* @param e
*/
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
log.info("握手成功,进入websocket");
}
}
2.3.编写handler
package com.alcode.websocket.handler;
import com.alcode.common.utils.SpringUtil;
import com.alcode.modules.Service.sys.SysNoticeService;
import com.alcode.modules.pojo.SysNotice;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import net.sf.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.socket.*;
import java.io.IOException;
import java.util.*;
/**
* @ClassName: MyHandler
* @Description: websocket处理器
* @Author: Jane
* @Date: 2020/7/4 13:23
* @Version: V1.0
**/
@Slf4j
@Component
public class MyWebSocketHandler implements WebSocketHandler {
/**
* 在线用户列表
*/
private static final Map<String, WebSocketSession> users;
static {
users = Maps.newHashMap();
}
/**
* 新增socket,建立新的连接
*
* @param session
* @throws Exception
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
log.info("开始建立socket连接,session={}", session);
String id = session.getUri().toString().split("id=")[1];
log.info("获取到用户的id={}", id);
if (!StringUtils.isEmpty(id)) {
users.put(id, session);
// websocket连接成功后,查一下通知表,把新消息推送给前端
Map<String, Object> params = Maps.newHashMap();
params.put("receive_user_id", id);
params.put("is_read", 0);
params.put("is_deleted", 0);
params.put("msg_type", 1);
SysNoticeService sysNoticeService = SpringUtil.getBean(SysNoticeService.class);
Collection<SysNotice> sysNoticeCollection = sysNoticeService.listByMap(params);
if (!ObjectUtils.isEmpty(sysNoticeCollection) && sysNoticeCollection.size() > 0) {
for (SysNotice sysNotice : sysNoticeCollection) {
session.sendMessage(new TextMessage(sysNotice.getDetails() + "-" + sysNotice.getId()));
}
}
}
log.info("当前在线人数:num={}", users.size());
}
/**
* 接收前端socket消息
*
* @param webSocketSession
* @param webSocketMessage
* @throws Exception
*/
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
try {
JSONObject jsonObject = JSONObject.fromObject(webSocketMessage.getPayload());
log.info("解析前端发的消息jsonObject={}", jsonObject.toString());
// System.out.println(jsonObject.get("message") + ":来自" + (String) webSocketSession.getAttributes().get("WEBSOCKET_USERID") + "的消息");
// 反馈前端
sendMessageToUser(jsonObject.get("id") + "", new TextMessage("用户" + jsonObject.get("id") + ",服务器收到了你的消息,这是给你的反馈"));
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 发送消息给指定用户
*
* @param userId
* @param message
* @return
*/
public boolean sendMessageToUser(String userId, TextMessage message) {
// 用户不在线发送失败
if (ObjectUtils.isEmpty(users.get(userId))) {
return false;
}
WebSocketSession session = users.get(userId);
log.info("获取用户{}的session={}", userId, session);
System.out.println("sendMessage:" + session);
// socket未连接,发送失败
if (!session.isOpen()) {
return false;
}
try {
// 使用用户对应的session发送消息
session.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 发送消息给所有用户
*
* @param message
* @return
*/
public boolean sendMessageToAllUsers(TextMessage message) {
boolean allSendSuccess = true;
Set<String> userIds = users.keySet();
WebSocketSession session = null;
// 遍历用户列表,发送消息
for (String userId : userIds) {
try {
session = users.get(userId);
if (session.isOpen()) {
session.sendMessage(message);
}
} catch (IOException e) {
e.printStackTrace();
allSendSuccess = false;
}
}
return allSendSuccess;
}
/**
* 连接出错时调用
*
* @param session
* @param exception
* @throws Exception
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
// 连接出错,关闭连接
if (session.isOpen()) {
session.close();
}
log.info("用户{}连接出错", getUserId(session));
// 从在线列表中移除该用户
users.remove(getUserId(session));
}
/**
* 关闭连接
*
* @param session
* @param status
* @throws Exception
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
log.info("用户{}关闭了连接", getUserId(session));
// 移除用户
users.remove(getUserId(session));
}
@Override
public boolean supportsPartialMessages() {
return false;
}
/**
* 获取用户ID
*
* @param session
* @return
*/
private Integer getUserId(WebSocketSession session) {
try {
Integer userId = (Integer) session.getAttributes().get("WEBSOCKET_USER_ID");
return userId;
} catch (Exception e) {
return null;
}
}
}
3.后台发送消息接口
调用handler中的方法即可推送消息
package com.alcode.websocket.controller.impl;
import com.alcode.common.config.RabbitmqConfig;
import com.alcode.websocket.handler.MyWebSocketHandler;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.socket.TextMessage;
import java.util.Map;
/**
* @ClassName: WebsocketController
* @Description: TODO
* @Author: Jane
* @Date: 2020/7/5 10:22
* @Version: V1.0
**/
@Slf4j
@Controller
@RequestMapping("/websocket")
public class WebsocketControllerImpl {
@Autowired
private MyWebSocketHandler handler;
/**
* 给指定用户发送消息
*
* @param userId
*/
@RequestMapping("/send/{userId}")
@ResponseBody
public void sendTopicMessage(@PathVariable String userId) {
log.info("后台给用户{}发送消息!", userId);
handler.sendMessageToUser(userId, new TextMessage("用户" + userId + "服务器给你发了一条消息"));
}
/**
* 给所有用户发送消息
*/
@RequestMapping("/all")
@ResponseBody
public void sendMessageToAll(){
log.info("后台给所有发送了消息");
handler.sendMessageToAllUsers(new TextMessage("后台给所有用户发的消息"));
}
}
4.前端代码
这里截取了部分我项目中的代码,前端采用Vue实现的
// 一下是new Vue中的一部分
<script>
data() {
return {
websocket: null,
}
},
created: {
//创建WebSocket,项目中的逻辑是用户登陆成功后在回调中去才去创建连接的,这里是页面一创建就连接websocket
this.connectWebSocket();
},
methods:{
// 浏览器强制关闭时,关闭websocket连接
window.onunload = () => {
this.closeWebSocket()
},
connectWebSocket() {
console.log('开始...')
// 建立websocket连接, 这里的userI在我项目是用户登陆成功后获取到的
this.websocket = new WebSocket('ws://localhost:8000/codeGod/websocket/id=' + this.userId);
// 打开webSocket连接时调用
this.websocket.onopen = () => {
console.log('onopen')
}
// 关闭webSocket连接时调用
this.websocket.onclose = () => {
console.log('onclose')
}
// 接收消息
this.websocket.onmessage = (msg) => {
console.log(msg.data);
// 调用消息提示方法
this.messageNotice(msg.data);
}
},
// 发送消息
send() {
let postValue = {};
postValue.sendUserId = this.userId;
postValue.receiveUserId =
postValue.message = "aaa";
this.websocket.send(JSON.stringify(postValue));
},
// 关闭连接
closeWebSocket(){
if (null != this.websocket){
this.websocket.close()
}
},
// 消息提示,使用Element-UI中的控件来实现
messageNotice(notice) {
this.$notify({
title: '通知',
message: notice,
duration: 0,
});
},
}
</script>
5.遇到的问题
5.1.Websocket不能与springboot中的定时任务一起使用
websocket与springboot中的定时任务Scheduleding同时使用是会报java.lang.IllegalStateException: Unexpected use of scheduler ,原因是没有找到合适的Scheduler对象;需要我们自己注入一个 TaskScheduler对象,如下:
package com.alcode.websocket.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
/**
* @ClassName: WebSocketScheduledConfig
* @Description: 解决不能同时使用Spring定时注解和WebSocket的问题
* @Author: Jane
* @Date: 2020/7/5 23:40
* @Version: V1.0
**/
@Configuration
public class WebSocketScheduledConfig {
@Bean
public TaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduling = new ThreadPoolTaskScheduler();
scheduling.setPoolSize(10);
scheduling.initialize();
return scheduling;
}
}
5.2.在MyWebSocketHandlet中无法注入Bean
在项目中注入service层时提示不能注入,原因是因为要注入的这些Bean都是单例的,通过上下文来获取即可,如下:
手动管理Bean工具类,SpringUtil
package com.alcode.common.utils;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
/**
* @Description: 提供手动获取被spring管理的bean对象
*/
public class SpringUtil implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (SpringUtil.applicationContext == null) {
SpringUtil.applicationContext = applicationContext;
}
}
// 获取applicationContext
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
// 通过name获取 Bean.
public static Object getBean(String name) {
return getApplicationContext().getBean(name);
}
// 通过class获取Bean.
public static <T> T getBean(Class<T> clazz) {
return getApplicationContext().getBean(clazz);
}
// 通过name,以及Clazz返回指定的Bean
public static <T> T getBean(String name, Class<T> clazz) {
return getApplicationContext().getBean(name, clazz);
}
}
先注入SpringUtil工具类
@SpringBootApplication
public class App {
public static void main(String[] args){
SpringApplication.run(App.class,args);
}
// 注入SpringUtil
@Bean
public SpringUtil springUtil(){
return new SpringUtil();
}
}
注入后,直接通过SpringUtil调用getBean(Xxx.class)方法即可获取对应的Bean