JSR356 WebSocket中ServerEndpoint的PostConstruct为何被调用两次?
疑问:JSR356 WebSocket的ServerEndpoint中PostConstruct方法为何被调用两次?
我在Web应用中使用JSR356 WebSocket时,发现@ServerEndpoint标注的类里的@PostConstruct方法被调用了两次,这让我很困惑。
服务器日志(WildFly的server.log)
PushMessageEndpoint: -> onPostConstruct(): at.luxbau.mis2.web.controller.prozess.PushMessageEndpoint@653dde97[session=<null>,sessionId=<null>] PushMessageEndpoint: <- onPostConstruct() PushMessageEndpoint: -> onPostConstruct(): at.luxbau.mis2.web.controller.prozess.PushMessageEndpoint@653dde97[session=<null>,sessionId=<null>] PushMessageEndpoint: <- onPostConstruct() PushMessageEndpoint: -> onOpen(): io.undertow.websockets.jsr.UndertowSession@43a032ff, M3OLaoTSRxM7EZWLH1LSzp7hFQBe24QvwLG7jDqr PushMessageEndpoint: endpoints.size=1 PushMessageEndpoint: endpoint: at.luxbau.mis2.web.controller.prozess.PushMessageEndpoint@653dde97[session=io.undertow.websockets.jsr.UndertowSession@43a032ff,sessionId=M3OLaoTSRxM7EZWLH1LSzp7hFQBe24QvwLG7jDqr] PushMessageEndpoint: <- onOpen()
服务端端点代码(PushMessageEndpoint.java)
@ServerEndpoint(value = "/push/{sessionId}") public class PushMessageEndpoint { private static final ILogger logger = LogFactory.getLogger(PushMessageEndpoint.class); private static final Set<PushMessageEndpoint> ENDPOINTS = new CopyOnWriteArraySet<>(); private Session session; private String sessionId; @PostConstruct void onPostConstruct() { logger.entry(this); logger.exit(); } @OnOpen public void onOpen(Session session, @PathParam("sessionId") String sessionId) { logger.entry(session, sessionId); this.session = session; this.sessionId = sessionId; ENDPOINTS.add(this); logEndpoints(); logger.exit(); } private void logEndpoints() { logger.debugf("endpoints.size=%d", ENDPOINTS.size()); ENDPOINTS.forEach(ep -> logger.debugf("endpoint: %s", ep)); } @Override public String toString() { return new ToStringBuilder(this) .append("session", session) .append("sessionId", sessionId) .toString(); } }
页面模板(template.xhtml)
每个页面都会引入这个模板:
<!-- 每个页面都通过SessionId打开一个WebSocket --> <h:outputScript> var sessionId = "${request.session.id}" openWebSocket(sessionId); </h:outputScript> <h:outputScript library="js" name="websocket.js"/>
WebSocket客户端代码(websocket.js)
function openWebSocket(sessionId) { if (window.WebSocket) { var wsUri = getRootUri() + "/" + sessionId; var ws = new WebSocket(wsUri); ws.onmessage = function (event) { var message = event.data; console.log(message); if (window["updateMessage"]) { // 调用RemoteCommand 'updateMessage'并传入消息作为参数 window["updateMessage"]([{'name': 'message', 'value': message}]); } // 接收消息后关闭连接 // ws.close(); } ws.onopen = function (event) { console.log("open web socket : " + wsUri); } ws.onclose = function (event) { console.log("close web socket : " + wsUri + " (" + event.code + ") " + event.reason + ", wasClean is " + event.wasClean); } } else { console.log("Browser does not support WebSockets!"); } }
问题分析与解决
从日志能看到,两次onPostConstruct调用的是同一个实例(@653dde97),说明不是创建了两个端点实例,而是同一个实例被两个容器重复初始化了:
- CDI容器初始化:如果应用启用了CDI(比如存在
beans.xml或符合Java EE默认CDI启用规则),CDI会扫描到PushMessageEndpoint并将其视为CDI bean,调用@PostConstruct方法。 - WebSocket容器初始化:JSR356 WebSocket容器同样会初始化这个端点类,再次触发
@PostConstruct方法。
解决方法
- 方法一:阻止CDI扫描:在
PushMessageEndpoint类上添加@Vetoed注解,禁止CDI容器将其作为bean管理,这样只有WebSocket容器会初始化它,@PostConstruct只会被调用一次:@ServerEndpoint(value = "/push/{sessionId}") @Vetoed public class PushMessageEndpoint { // ... 原有代码 ... } - 方法二:替换初始化逻辑:如果不需要CDI管理该端点,直接去掉
@PostConstruct注解,将初始化逻辑移到@OnOpen方法中——@OnOpen是WebSocket规范定义的连接建立回调,更适合处理WebSocket相关的初始化操作。
另外,客户端日志仅显示一次连接建立,说明客户端没有重复发起请求,问题根源完全在服务端双容器初始化。
内容的提问来源于stack exchange,提问作者raho
相关产品推荐
相关产品推荐

