The best way to configure WebSocket in spring cloud

preface

Recently, there is a private message function in the project, which needs to use websocket, so I looked for information on the Internet and summarized some experience in practice to share with you.

problem

Before I do this, let me ask you a question:

The SpringBoot project integrates webSocket. When the client establishes a connection with the server, it is found that the server object is not injected but null.
Cause: spring manages singleton s, which conflicts with websocket.
Detailed explanation: when the project is initialized at startup, websocket (not connected by the user) will be initialized, and spring will inject service for it. The service of the object is not null and is successfully injected. However, since spring manages singletons by default, it will inject services only once. When the client connects with the server, the server will create a new websocket object. At this time, the problem arises: Spring manages single instances and does not inject services into the second websocket object. Therefore, as long as the websocket object created by the user connection can no longer be injected.
For example, there is a service in the controller and a dao in the service. Because controller, service and dao are singletons, null will not be reported during injection. However, websocket is not a singleton, so after spring injection, the following objects will not be injected again, and NullException will be reported.

The solution will be discussed below.

operation

1. Introducing websocket dependency package

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

2. Configure websocket

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        // webSocket channel
        // Specify the processor and path, such as: http://www.baidu.com/service-name/websocket?uid=xxxx
        webSocketHandlerRegistry.addHandler(new WebSocketHandler(), "/websocket")
//                //Specify custom interceptors
                .addInterceptors(new WebSocketInterceptor())
                // Allow cross domain
                .setAllowedOrigins("*");
    }
}

3. Add a parameter class to get the websocket address

public class WebSocketInterceptor implements HandshakeInterceptor {

    /**
     * handler Before processing, the attributes attribute is finally in WebSocketSession, possibly through webSocketSession.. getAttributes(). Get (key value)
     */
    @Override
    public boolean beforeHandshake(org.springframework.http.server.ServerHttpRequest request, ServerHttpResponse serverHttpResponse, org.springframework.web.socket.WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
            // Get the parameters carried by the request path
            String uid = serverHttpRequest.getServletRequest().getParameter("uid");
            map.put("uid", uid);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public void afterHandshake(org.springframework.http.server.ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, org.springframework.web.socket.WebSocketHandler webSocketHandler, Exception e) {

    }
}

4. Add a class that resolves the null injection of the server object @ Autowired

@Component
public class SpringContext implements ApplicationContextAware {

    /**
     * Print log
     */
    private Logger logger = LoggerFactory.getLogger(getClass());

    /**
     * Get context object
     */
    private static ApplicationContext applicationContext;


    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContext.applicationContext = applicationContext;
        logger.info("set applicationContext");
    }

    /**
     * Get applicationContext
     *
     * @return
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * Get bean object by name
     *
     * @param name
     * @return
     */
    public static Object getBean(String name) {

        return getApplicationContext().getBean(name);
    }

    /**
     * Get bean object through class
     *
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {
        return getApplicationContext().getBean(clazz);
    }

    /**
     * Get the specified bean object through name and clazz
     *
     * @param name
     * @param clazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name, Class<T> clazz) {
        return getApplicationContext().getBean(name, clazz);
    }

}

5. Add websocket receive send message class

@Component
public class WebSocketHandler extends AbstractWebSocketHandler {
    private static Logger log = LoggerFactory.getLogger(WebSocketHandler.class);

    public AccountFeignClient getAccountFeignClient() {
        return SpringContext.getBean(AccountFeignClient.class);
    }

    public NotifyMailboxService getNotifyMailboxService() {
        return SpringContext.getBean(NotifyMailboxService.class);
    }

    public NotifyMailboxMessageService getNotifyMailboxMessageService() {
        return SpringContext.getBean(NotifyMailboxMessageService.class);
    }

    /**
     * Store sessionId and webSocketSession
     * It should be noted that webSocketSession does not provide a parameterless structure and cannot be serialized or stored through redis
     * In the distributed system, we should find another way to realize webSocketSession sharing
     */
    private static Map<String, WebSocketSession> sessionMap = new ConcurrentHashMap<>();
    private static Map<String, String> userMap = new ConcurrentHashMap<>();

    /**
     * webSocket Call after connection creation
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession session) {
        // Get parameters
        String uid = String.valueOf(session.getAttributes().get("uid"));
        String sessionId = session.getId();
        log.info("init websocket uid={},sessionId={}", uid, sessionId);
        userMap.put(uid, sessionId);
        sessionMap.put(sessionId, session);
    }

    /**
     * The front end sends messages to the background
     * Called when a message is received
     */
    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        //User A sends the front-end message to the background. The background should save the A message and push the message to user B
        if (message instanceof TextMessage) {
            log.info("message={}", message);
        } else if (message instanceof BinaryMessage) {

        } else if (message instanceof PongMessage) {

        } else {
            log.info("Unexpected WebSocket message type: " + message);
        }
        String uid = String.valueOf(session.getAttributes().get("uid"));
        String messages = (String) message.getPayload();
        ObjectMapper mapper = new ObjectMapper();
        HashMap<String, Object> map = mapper.readValue(messages, HashMap.class);
        String _uid = (String) map.get("uid");
//        String _dialogId = (String) map.get("dialogId");
        String _friendId = (String) map.get("friendId");
        String _message = (String) map.get("message");

        String sessionId = session.getId();
        log.info("sessionId={},uid={},_uid={},_friendId={},_message={}", sessionId, uid, _uid, _friendId, _message);

        if (!StringUtils.hasLength(sessionId) || !StringUtils.hasLength(_uid) || !StringUtils.hasLength(_friendId)) {
            log.info("sessionId&_uid&_friendId Cannot be empty");
            session.sendMessage(new TextMessage("error:sessionId&_uid&_friendId Cannot be empty"));
            return;
        }
        String dialogId = pushMessage(_uid, _friendId, _message);
        if (dialogId != null) {
            TextMessage textMessage = new TextMessage("dialogId:" + dialogId);
            // Push messages to your ws
            session.sendMessage(textMessage);
            String sessionIdForFriend = userMap.get(_friendId);
            log.info("sessionIdForFriend={}", sessionIdForFriend);
            if (StringUtils.hasLength(sessionIdForFriend)) {
                WebSocketSession friendSession = sessionMap.get(sessionIdForFriend);
                if (friendSession != null && friendSession.isOpen())
                    // Push messages to friends
                    friendSession.sendMessage(textMessage);
            }
        }
    }

    /**
     * Connection error will call
     */
    @Override
    public void handleTransportError(WebSocketSession session, Throwable exception) {
        String uid = String.valueOf(session.getAttributes().get("uid"));
        String sessionId = session.getId();
        log.info("CLOSED uid= ={},sessionId={}", uid, sessionId);
        sessionMap.remove(sessionId);
    }

    /**
     * Called when the connection is closed
     */
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
        String uid = String.valueOf(session.getAttributes().get("uid"));
        String sessionId = session.getId();
        log.info("CLOSED uid= ={},sessionId={}", uid, sessionId);
        sessionMap.remove(sessionId);
    }

    @Override
    public boolean supportsPartialMessages() {
        return false;
    }

    /**
     * Send messages from the background to the front end
     * Encapsulation method sends a message to the client
     */
    public static void sendMessage(String uid, String dialogId) {
        log.info("Send message to:");
    }

    /**
     * @param uid
     * @param friendId
     * @param message
     * @return
     */
    private String pushMessage(String uid, String friendId, String message) {
        log.info("uid={},friendId={},message={}", uid, friendId, message);
        NotifyMailboxService notifyMailboxService = getNotifyMailboxService();
        NotifyMailboxMessageService notifyMailboxMessageService = getNotifyMailboxMessageService();
        try {
            NotifyMailbox notifyMailbox = notifyMailboxService.queryBy(uid, friendId);
            
        } catch (Exception e) {
            log.info("exception msg={}", e.getMessage());
            return null;
        }

    }
}

websocket front end status code readyState

0        CONNECTING        The connection has not been established
1        OPEN            WebSocket Your link has been established
2        CLOSING            Connection closing
3        CLOSED            The connection is closed or unavailable


summary

1. The server object is not injected but null, so you should add the above SpringContext class and reference it in the following way

public AccountFeignClient getAccountFeignClient() {
    return SpringContext.getBean(AccountFeignClient.class);
}

public NotifyMailboxService getNotifyMailboxService() {
    return SpringContext.getBean(NotifyMailboxService.class);
}

public NotifyMailboxMessageService getNotifyMailboxMessageService() {
    return SpringContext.getBean(NotifyMailboxMessageService.class);
}

2. The connection of websocket corresponds to the closed session. There is no problem using the above code, otherwise there will be problems with the connection.

3. WebSocketInterceptor will get https://www.baidu.com/service-name/websocket?uid=xxx And inject the uid into the session, so the WebSocketHandler class can get the uid parameter in the session.

quote

WebSocket tutorial
The @ Autowired injection in webSocket corresponds to null

Keywords: Spring Cloud websocket

Added by mhenke on Fri, 14 Jan 2022 11:53:13 +0200