项目背景: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;
    }