项目背景:Java环境,Get请求根据前端查询条件建立WebSocket连接,每5秒主动实时推送最新查询结果给前端展示。其中也遇到定时器、WebSocket无法注册Bean、No encoder specified for object of class [class java.util.xxx]等问题,相关解决方案也有列举~
文章目录
- 1、WebSocket简介
- 2、实现WebSocket
- 2.1 pom.xml引入WebSocket依赖
- 2.2 WebSocketConfig
- 2.3 WebSocketServer
- 2.4 List<Map<String, Object>>转JSON编码器
- 2.5 ws升级为wss
- 2.6 WebSocket无法注册Bean问题解决方案
1、WebSocket简介
Web Sockets 的是在一个单独的持久连接上提供全双工、双向通信。在 JavaScript 中创建了 Web Socket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会从 HTTP 协议升级为 Web Socket 协议。
WebSocket是HTML5开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。
在WebSocket API中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。
Spring框架实现WebSocket 的官方教程:https://spring.io/guides/gs/messaging-stomp-websocket/
2、实现WebSocket
2.1 pom.xml引入WebSocket依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2.2 WebSocketConfig
创建WebSocketConfig类,它检测所有带有@serverEndpoint注解的bean并注册他们。
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
@Slf4j
public class WebSocketConfig {
/**
* 给spring容器注入这个ServerEndpointExporter对象
* 相当于xml:
* <beans>
* <bean id="serverEndpointExporter" class="org.springframework.web.socket.server.standard.ServerEndpointExporter"/>
* </beans>
* <p>
* 检测所有带有@serverEndpoint注解的bean并注册他们。
*
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
log.info("serverEndpointExporter was injected");
return new ServerEndpointExporter();
}
}
2.3 WebSocketServer
创建WebSocketServer类,并在其中实现具体WebSocket的方法。
@serverEndpoint
主要是将目前的类定义成一个websocket服务器端, 注解的值将被用于监听用户连接的终端访问URL地址,客户端可以通过这个URL来连接到WebSocket服务器端。
import com.xx.util.ListMapEncoder;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
//对应的接口路径
//encoders是所需的编码器
@ServerEndpoint(value = "/websocket",encoders = {ListMapEncoder.class})
@Component
public class WebSocketServer extends Endpoint {
private static Logger log = LoggerFactory.getLogger(WebSocketServer.class);
private static XXService xxService;
private static final AtomicInteger OnlineCount = new AtomicInteger(0);
// concurrent包的线程安全Set,用来存放每个客户端对应的Session对象。
private static CopyOnWriteArraySet<Session> SessionSet = new CopyOnWriteArraySet<Session>();
private ScheduledExecutorService executorService;
//spring 或 springboot 的 websocket 里面使用 @Autowired 注入 service 或 bean 时,报空指针异常,service 为 null(并不是不能被注入)。解决方法:将要注入的 service 改成 static,就不会为null了。(注意set 方法上面不要加static)
//本质原因:spring管理的都是单例(singleton)和 websocket (多对象)相冲突。
@Autowired
public void setXXService(XXService xxService) {
WebSocketServer.xxService = xxService;
}
@PostConstruct
public void init() {
log.info("webSocket loading");
}
//最基础的websocket方式,在业务函数中调用BroadCastInfo()引入需发送的消息参数请求即可以WebSocket方式推送回前端
/**
* 连接建立成功调用的方法
*/
// @OnOpen
// public void onOpen(Session session) throws IOException{
// SessionSet.add(session);
// int cnt = OnlineCount.incrementAndGet();
// log.info("有连接加入,当前连接数为:{}", cnt);
// SendMessage(session, "连接成功");
// }
/**
* 连接关闭调用的方法
*/
// @OnClose
// public void onClose(Session session) {
// SessionSet.remove(session);
// int cnt = OnlineCount.decrementAndGet();
// log.info("有连接关闭,当前连接数为:{}", cnt);
// }
@Override
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
this.session = session;
int cnt = OnlineCount.incrementAndGet();
log.info("有连接加入,当前连接数为:{}", cnt);
// 每5秒向前端发送最新数据
executorService = Executors.newSingleThreadScheduledExecutor();
executorService.scheduleAtFixedRate(() -> {
//业务逻辑
//获取前端GET请求中的查询条件,这里用param1和param2举例
String param1 = session.getRequestParameterMap().get("param1").get(0);}
String param2 = session.getRequestParameterMap().get("param2").get(0);}
List<Map<String, Object>> latestData = new ArrayList<>();
try{
// 调用查询函数,获取最新数据
latestData = xxService.getXX(param1,param2);
}catch (Exception e){
log.warn(e.getMessage());
}
try {
//转JSON格式推送数据到前端
session.getBasicRemote().sendObject(latestData);
log.info("webSocket send! timeStamp is " + sdf.format(new Date()));
} catch (IOException | EncodeException e) {
e.printStackTrace();
}
}, 0, 5, TimeUnit.SECONDS);
}
@Override
@OnClose
public void onClose(Session session, CloseReason closeReason) {
// 关闭连接时关闭定时任务
executorService.shutdown();
}
/**
* 收到客户端消息后调用的方法
*
* @param message
* 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
log.info("来自客户端的消息:{}",message);
SendMessage(session, "收到消息,消息内容:"+message);
}
/**
* 出现错误
* @param session
* @param error
*/
@Override
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误:{},Session ID: {}",error.getMessage(),session.getId());
error.printStackTrace();
}
/**
* 发送消息,实践表明,每次浏览器刷新,session会发生变化。
* @param session
* @param message
*/
public static void SendMessage(Session session, String message) throws IOException {
try {
// session.getBasicRemote().sendText(String.format("%s (From Server,Session ID=%s)",message,session.getId()));
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("发送消息出错:{}", e.getMessage());
e.printStackTrace();
}
}
public static void SendMessageObject(Session session, Object message) throws IOException {
try {
session.getBasicRemote().sendObject(message);
} catch (IOException | EncodeException e) {
log.error("发送消息出错:{}", e.getMessage());
e.printStackTrace();
}
}
/**
* 群发消息
* @param message
* @throws IOException
*/
//object类型的返参,List<Map<String, Object>>类似其他类型的需用编码器Encoder.Text<List<Map<String, Object>>>
public void BroadCastInfoObject(List<Map<String, Object>> message) throws IOException {
for (Session session : SessionSet) {
if(session.isOpen()){
SendMessageObject(session, message);
}
}
}
//string类型的返参
public static void BroadCastInfo(String message) throws IOException {
for (Session session : SessionSet) {
if(session.isOpen()){
SendMessage(session, message);
}
}
}
/**
* 指定Session发送消息
* @param sessionId
* @param message
* @throws IOException
*/
public static void SendMessage(String message,String sessionId) throws IOException {
Session session = null;
for (Session s : SessionSet) {
if(s.getId().equals(sessionId)){
session = s;
break;
}
}
if(session!=null){
SendMessage(session, message);
}
else{
log.warn("没有找到你指定ID的会话:{}",sessionId);
}
}
}
2.4 List<Map<String, Object>>转JSON编码器
解决No encoder specified for object of class [class java.util.xxx]的问题需使用编码器
这儿是List<Map<String, Object>>转JSON编码器示例:
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.core.codec.DecodingException;
import org.springframework.core.codec.EncodingException;
import javax.websocket.Encoder;
import javax.websocket.EndpointConfig;
public class ListMapEncoder implements Encoder.Text<List<Map<String, Object>>> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public String encode(List<Map<String, Object>> list) throws EncodingException {
try {
return objectMapper.writeValueAsString(list);
} catch (JsonProcessingException e) {
throw new EncodingException("Error encoding List<Map<String, Object>> to JSON", e);
}
}
// @Override
public List<Map<String, Object>> decode(String s) throws DecodingException {
try {
return objectMapper.readValue(s, objectMapper.getTypeFactory().constructCollectionType(List.class, Map.class));
} catch (JsonProcessingException e) {
throw new DecodingException("Error decoding JSON to List<Map<String, Object>>", e);
}
}
@Override
public void init(EndpointConfig endpointConfig) {
}
@Override
public void destroy() {
}
}
2.5 ws升级为wss
其实wss就是ws的加密版本,可将其理解为http与https的关系。
只需要配置nginx代理,在nginx配置文件server中增加一个websocket的location即可。
server{
location /test{
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection Upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /wss{
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade websocket;
proxy_set_header Connection Upgrade;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
2.6 WebSocket无法注册Bean问题解决方案
spring 或 springboot 的 websocket 里面使用 @Autowired 注入 service 或 bean 时,报空指针异常,service 为 null(并不是不能被注入)。解决方法:将要注入的 service 改成 static,就不会为null了。(注意set 方法上面不要加static)。
本质原因:spring管理的都是单例(singleton)和 WebSocket(多对象)相冲突。
private static XXService xxService;
@Autowired
public void setXXService(XXService xxService) {
WebSocketServer.xxService = xxService;
}