在JSR-356 @ServerEndpoint的@OnMessage中访问ServletContext和HttpSession

30
我需要在@ServerEndpoint内部获取ServletContext,以便找到Spring的ApplicationContext并查找一个Bean。
目前,我最好的方法是将该Bean绑定在JNDI命名上下文中,并在Endpoint中查找它。欢迎任何更好的解决方案。
我还在寻找一种合理的方法来同步Servlet的HttpSession和websocket的Session

我也在尝试从websocket端点内部访问ServletContext。正在尝试在Tomcat中完成此操作。 - egerardus
请参见https://dev59.com/jX7aa4cB1Zd3GeqPpmed#23025059。 - anttix
你必须查找bean吗?为什么不能注入它?如果在你的@ServerEndpoint注释中使用configurator = org.springframework.web.socket.server.endpoint.SpringConfigurator.class,它应该是可注入的。 - Peter G
4个回答

45

在JSR-356中,servlet HttpSession 可以通过HandshakeRequest#getHttpSession()获得,当在一个@ServerEndpoint之前进行握手请求时,在@OnOpen之前。

ServletContext可以通过HttpSession#getServletContext()获得。这样就一举两得了。

为了捕获握手请求,实现一个ServerEndpointConfig.Configurator,并覆盖modifyHandshake()方法。在此方法中,可以将HandshakeRequest作为方法参数。可以将HttpSession放入EndpointConfig#getUserProperties()中。而EndpointConfig可在@OnOpen中作为方法参数。

下面是一个ServerEndpointConfig.Configurator实现的启动示例:

public class ServletAwareConfig extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
        HttpSession httpSession = (HttpSession) request.getHttpSession();
        config.getUserProperties().put("httpSession", httpSession);
    }

}

以下是如何使用它的方法,请注意 @ServerEndpointconfigurator 属性:

@ServerEndpoint(value="/your_socket", configurator=ServletAwareConfig.class)
public class YourSocket {

    private EndpointConfig config;

    @OnOpen
    public void onOpen(Session websocketSession, EndpointConfig config) {
        this.config = config;
    }

    @OnMessage
    public void onMessage(String message) {
        HttpSession httpSession = (HttpSession) config.getUserProperties().get("httpSession");
        ServletContext servletContext = httpSession.getServletContext();
        // ...
    }

}

作为设计提示,最好让您的@ServerEndpoint完全不依赖于servlet API。在modifyHandshake()实现中,您最好立即从servlet会话或上下文中提取出您需要的信息(通常是可变的Javabean)并将其放入用户属性映射中。如果您不这样做,那么您应该记住websocket会话的生命周期可能比HTTP会话更长。因此,如果您仍然将HttpSession带到端点中,则在尝试访问它时可能会遇到IllegalStateException,因为它正在过期。

如果您手头有CDI(也许还有JSF),则可以从Omnifaces <o:socket>的源代码中获取灵感(链接位于展示的最底部)。

另请参见:


感谢这个解释,尤其是提醒要让端点保持不依赖于Servlet API。 - hfmanson
不要注入 Servlet 上下文作为用户属性,而是插入 spring 上下文;虽然它不像自动装配 bean 那样方便,但它可以解决我的问题。 - Sombriks
对于 WebSocket,似乎 Omniface 示例中的代码使用了 HTTP 请求来增加计数器。我在这里错过了什么,这不是违背了它的初衷吗? - Ced
@Ced:它只是触发了推送。 - BalusC
如果有人在获取HttpSession时得到null,这里是解决方案:https://dev59.com/C2Ij5IYBdhLWcg3weE4J - Kashyap Kansara
在某些情况下,强制为每个请求启用会话可能是不可接受的(例如禁用了cookies,用户明确拒绝任何数据存储)。我已经向API提交了一个PR,以在HandshakeRequest中添加getServletContext()方法:请给它点赞 :) - morgwai

5

更新 BalusC 的答案的代码,需要在 onOpen 方法上加上 @OnOpen 注解。然后就不再需要扩展 Endpoint 类了:

@ServerEndpoint(value="/your_socket", configurator=ServletAwareConfig.class)
public class YourSocket {

    private EndpointConfig config;

    @OnOpen
    public void onOpen(Session websocketSession, EndpointConfig config) {
        this.config = config;
    }

    @OnMessage
    public void onMessage(String message) {
        HttpSession httpSession = (HttpSession) config.getUserProperties().get("httpSession");
        ServletContext servletContext = httpSession.getServletContext();
        // ...
    }

}

1
我在Tomcat(版本7.0.56和8.0.14)上尝试了BalusC的答案。 在两个容器上,modifyHandshake的请求参数不包含HttpSession(因此没有servletContext)。 由于我只需要访问“全局”变量(即Web应用程序全局变量),因此我只需将这些变量存储在持有者类的普通静态字段中。 这很不优雅,但它起作用了。 这似乎是特定Tomcat版本中的错误 - 有没有人也看到过这种情况?

2
不,这并不是一个错误。它表示httpSession根本没有创建。您可以事先自己创建它。有关更多信息,请参见https://dev59.com/C2Ij5IYBdhLWcg3weE4J。 - Kalle
谢谢 - 这是一个我没有想到的解决方案(我以为Session总是由基础设施创建)。不过,由于我只想将其用于应用程序全局数据,所以我仍然会坚持手工制作的解决方案,因为它现在可以工作。 - Wolfgang Liebich
1
在Jetty中的行为是相同的。正如@Kalle所指出的那样,只需使用HttpFilterServletRequestListener并调用request.getSession(true)即可。遗憾的是,在HandshakeRequest.getHttpSession()上没有类似的重载。 - earcam

0
有时候我们无法通过BalusC的ServletAwareConfig获取到session,这是因为session还没有被创建。由于我们不是在寻找session而是servletContext,在Tomcat中我们可以按照以下方式操作:
public static class ServletAwareConfig extends ServerEndpointConfig.Configurator {

    @Override
    public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
        try {
            Field reqfld = request.getClass().getDeclaredField("request");
            reqfld.setAccessible(true);
            HttpServletRequest req = (HttpServletRequest) reqfld.get(request);
            ServletContext ctxt = req.getServletContext();
            Map<String, Object> up = config.getUserProperties();
            up.put("servletContext", ctxt);
        } catch (NoSuchFieldException e) {
        } catch (SecurityException e) {
        } catch (IllegalArgumentException e) {
        } catch (IllegalAccessException e) {
        }
    }

}

如果我们想要立即初始化会话,我们可以调用request.getSession()。
参考: Websocket - httpSession returns null

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接