简易地使用 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 方法,将映射 put 到 sessionMap 中,中断可看到 sessionMap 中已有当前实例名 HEBUT 到 Session 的映射。
可是在执行控制器的方法时, getSessionMap 却获取到了一个空的 Map 。
我当时就很蒙圈呀~,明明 put 进去了,怎么再 get 就没了呢?怎么也想不明白呀?
掉坑的原因是:因为这个是 spring 官方提供的 starter ,我默认认为它是使用了 spring ioc 的。
震惊。这个 ServerEndpoint 的对象实例竟然不是从上下文里拿的!!!
WebSocket 建立连接时 ServerEndpoint 对象的地址编号是 5653 。
从 spring 上下文里 autowire 进来的 ServerEndpoint 对象的地址编号是 6719 。
这个 Bean 是 singleton 的。由此推测, 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 !”
我只是一个默默无闻的小程序员,和老师、同学们创业学习。虽然我进不去华为腾讯,虽然我没写过开源项目;但是我知道,写代码做开发,要遵守规范。
一个团队写出来的代码,就像一个人写出来的一样。