引言
简易地使用 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
!”
总结
我只是一个默默无闻的小程序员,和老师、同学们创业学习。虽然我进不去华为腾讯,虽然我没写过开源项目;但是我知道,写代码做开发,要遵守规范。
一个团队写出来的代码,就像一个人写出来的一样。
——《天津市红桥区梦云智软件开发中心》