乐趣区

神坑中间件springbootstarterwebsocket

引言

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

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

灵异事件

需求描述

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

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

神奇代码

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的映射。

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

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

原因

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

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

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!”

总结

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

一个团队写出来的代码,就像一个人写出来的一样。
——《天津市红桥区梦云智软件开发中心》

退出移动版