Java-WebSocket communication implements active real-time transmission of data to the front end based on query conditions&List<Map<String, Object>>convert to JSON encoder&WebSocket cannot register Bean problem solution

Project background: Java environment, Get request establishes a WebSocket connection based on the front-end query conditions, and actively pushes the latest query results to the front-end in real time every 5 seconds. Among them, we also encountered problems such as timers, WebSocket cannot register beans, No encoder specified for object of class [class java.util.xxx], etc. Related solutions are also listed~

1. Introduction to WebSocket

Web Sockets provide full-duplex, two-way communication over a single persistent connection. After the Web Socket is created in JavaScript, an HTTP request is sent to the browser to initiate the connection. After obtaining the server response, the established connection will be upgraded from the HTTP protocol to the Web Socket protocol.

WebSocket is a protocol for full-duplex communication on a single TCP connection that HTML5 began to provide.

In the WebSocket API, the browser and the server only need to perform a handshake action, and then a fast channel is formed between the browser and the server. Data can be transmitted directly between the two.

The browser sends a request to the server to establish a WebSocket connection through JavaScript. After the connection is established, the client and server can directly exchange data through the TCP connection.

Official tutorial for implementing WebSocket in Spring framework: https://spring.io/guides/gs/messaging-stomp-websocket/

2. Implement WebSocket

2.1 pom.xml introduces WebSocket dependency

	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-websocket</artifactId>
	</dependency>

2.2 WebSocketConfig

Create a WebSocketConfig class that detects all beans annotated with @serverEndpoint and registers them.

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

Create a WebSocketServer class and implement specific WebSocket methods in it.

@serverEndpointThe main purpose is to define the current class as a websocket server. The value of the annotation will be used to monitor the terminal access URL address of the user connection. The client can connect to the WebSocket server through this URL.

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>> to JSON encoder

To solve the problem of No encoder specified for object of class [class java.util.xxx], you need to use an encoder.
Here is an example of List<Map<String, Object>> to JSON encoder:

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 upgraded to wss

In fact, wss is the encrypted version of ws, which can be understood as the relationship between http and https.
You only need to configure the nginx proxy and add a websocket location to the nginx configuration file server.

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 cannot register Bean problem solution

When using @Autowired to inject a service or bean in a spring or springboot websocket, a null pointer exception is reported and the service is null (it does not mean that it cannot be injected). Solution: Change the service to be injected to static so that it will not be null. (Be careful not to add static above the set method).
The essential reason: spring manages singletons (singleton) and WebSocket (multiple objects), which conflict.

	private static XXService xxService;
	
    @Autowired
    public void setXXService(XXService xxService) {
        WebSocketServer.xxService = xxService;
    }

Reference: https://blog.csdn.net/weixin_35815479/article/details/128027542

https://blog.csdn.net/m0_52348758/article/details/123566273

https://blog.csdn.net/qq_40276626/article/details/120443632

webSocket+ scheduled task: https://blog.csdn.net/weixin_45003796/article/details/128470052?spm=1001.2101.3001.6650.5&utm_medium=distribute.pc_relevant.none-task-blog-2 default CTRLIST Rate-5-128470052-blog -128027542.235^v27^pc_relevant_default&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2 default CTRLIST Rate-5-128470052-blog-128027542.235 v27 pc_relevant_default&utm_relevant_index=10

Websocket cannot inject beans: https://www.jianshu.com/p/74759e5bacad

No encoder specified for object of class [class java.util.xxx] The problem requires the use of an encoder: https://blog.csdn.net/chang100111/article/details/119773635

nginx configuration: http://events.jianshu.io/p/954643bdba4a

postman-websocket test: https://blog.csdn.net/qq_34330916/article/details/122339251

WebSocket test site: http://coolaf.com/tool/chattest

Guess you like

Origin blog.csdn.net/weixin_44436677/article/details/130041350