简易地使用 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
!”
我只是一个默默无闻的小程序员,和老师、同学们创业学习。虽然我进不去华为腾讯,虽然我没写过开源项目;但是我知道,写代码做开发,要遵守规范。
一个团队写出来的代码,就像一个人写出来的一样。