转载

神坑中间件:spring-boot-starter-websocket

引言

简易地使用 WebSocket 时,使用 spring-boot-starter-websocket 没什么问题,虽然路由部分设计得有些缺陷,但不影响正常使用。

但当我使用 spring-boot-starter-websocket 实现复杂业务的时候,发现这个中间件虽然是 spring 官方提供的中间件,但是却像是从来没有用过 spring 的人写出来的一样。

灵异事件

需求描述

想实现一个外网向企业内局域网转发数据的雏形,就是下面这张图:

secret 为内网服务, server 为外网服务,该服务向 server 注册,建立 WebSocket 连接,这样在外网的 server 接收到指令就能通过 WebSocket 通道转发给内网的 secret

神坑中间件:spring-boot-starter-websocket

神奇代码

WebSocket 服务端 Endpoint ,路由映射 /websocket/{name}name 为注册的服务实例的名字。

客户端连接 ws://127.0.0.1:8000/websocket/HEBUT ,注册一个名为 HEBUT 的服务实例。

服务端将服务实例名称到 Session 的映射存到了一个 ConcurrentHashMap 里。

@Component
@ServerEndpoint("/websocket/{name}")
public class YunzhiWebSocket {

    private static final Logger logger = LoggerFactory.getLogger(YunzhiWebSocket.class);

    private Map<String, Session> sessionMap = new ConcurrentHashMap<>();

    @OnOpen
    public void onOpen(@PathParam(value = "name") String name, Session session) throws IOException {
        if (name != null && !name.equals("")) {
            logger.debug("名称合法,添加到Map中");
            sessionMap.put(name, session);
        } else {
            logger.debug("关闭连接");
            session.close();
        }
    }

    @OnMessage
    public void onMessage(String message) {
        logger.error("接收到消息 {}", message);
    }

    @OnError
    public void onError(@PathParam(value = "name") String name, Throwable throwable) {
        logger.error("连接发生错误 {}", throwable.getMessage());
        sessionMap.remove(name);
    }

    @OnClose
    public void onClose(@PathParam(value = "name") String name) {
        logger.debug("关闭连接");
        sessionMap.remove(name);
    }

    public Map<String, Session> getSessionMap() {
        return sessionMap;
    }
}

一个映射 ** 的方法,将所有其他的请求都交给当前 action 处理,根据要访问的实例名去 SessionMap 里找相应的 Session

@RequestMapping("{name}/**")
public void dispatcher(@PathVariable String name, HttpServletRequest request) {
    logger.debug("根据服务名查询Session");
    Session session = yunzhiWebSocket.getSessionMap().get(name);

    logger.debug("未找到服务,抛出异常");
    if (session == null) {
        throw new ServiceNotFoundException("找不到该服务实例");
    }
}

诡异

WebSocket 连接之后,执行 onOpen 方法,将映射 putsessionMap 中,中断可看到 sessionMap 中已有当前实例名 HEBUTSession 的映射。

神坑中间件:spring-boot-starter-websocket

可是在执行控制器的方法时, getSessionMap 却获取到了一个空的 Map

神坑中间件:spring-boot-starter-websocket

我当时就很蒙圈呀~,明明 put 进去了,怎么再 get 就没了呢?怎么也想不明白呀?

神坑中间件:spring-boot-starter-websocket

原因

掉坑的原因是:因为这个是 spring 官方提供的 starter ,我默认认为它是使用了 spring ioc 的。

震惊。这个 ServerEndpoint 的对象实例竟然不是从上下文里拿的!!!

神坑中间件:spring-boot-starter-websocket

WebSocket 建立连接时 ServerEndpoint 对象的地址编号是 5653

spring 上下文里 autowire 进来的 ServerEndpoint 对象的地址编号是 6719

这个 Beansingleton 的。由此推测, spring-boot-starter-websocket 使用的对象没有从上下文里拿,就是自己造的。

回忆

我记得上次我遇到这个问题是在编写 hibernate 拦截器的时候, autowire 的时候一直注不进来。

因为 hibernate 拦截器组件并非 spring 官方编写,所以很自然就想到可能是 hibernate 没有遵循 spring ioc 的规范,没有获取上下文的对象,很快便解决了。

问题是时间已经过了一年半,技术提升巨大,可是我再次碰到类似问题的时候,居然花了两个小时解决!!!

最后反思就是中间件 spring-boot-starter-websocket 背锅, hibernate 拦截器不好使,我第一个想到的就是上下文对象的获取问题,因为 hibernate 是第三方 orm 框架。

一直没有往这方面想,在我的印象里, spring-boot 十分优秀,整合的每一个 starter 都是 spring 这样式的。

可是谁想到官方提供的 starter 给我整了这么一出,“没想到吧,别看我是 spring 开头的,其实我没用 ioc !”

总结

我只是一个默默无闻的小程序员,和老师、同学们创业学习。虽然我进不去华为腾讯,虽然我没写过开源项目;但是我知道,写代码做开发,要遵守规范。

一个团队写出来的代码,就像一个人写出来的一样。

原文  https://segmentfault.com/a/1190000020864438
正文到此结束
Loading...