在数据库中修改某些内容时,仅通过WebSockets通知特定用户

时间:2015-09-06 18:07:27

标签: java-ee websocket java-ee-7 real-time-updates

为了通过WebSockets通知所有用户,当在选定的JPA实体中修改某些内容时,我使用以下基本方法。

@ServerEndpoint("/Push")
public class Push {

    private static final Set<Session> sessions = new LinkedHashSet<Session>();

    @OnOpen
    public void onOpen(Session session) {
        sessions.add(session);
    }

    @OnClose
    public void onClose(Session session) {
        sessions.remove(session);
    }

    private static JsonObject createJsonMessage(String message) {
        return JsonProvider.provider().createObjectBuilder().add("jsonMessage", message).build();
    }

    public static void sendAll(String text) {
        synchronized (sessions) {
            String message = createJsonMessage(text).toString();

            for (Session session : sessions) {
                if (session.isOpen()) {
                    session.getAsyncRemote().sendText(message);
                }
            }
        }
    }
}

当修改选定的JPA实体时,会引发适当的CDI事件,该事件将由以下CDI观察者观察。

@Typed
public final class EventObserver {

    private EventObserver() {}

    public void onEntityChange(@Observes EntityChangeEvent event) {
        Push.sendAll("updateModel");
    }
}

观察者/使用者调用WebSockets端点中定义的静态方法Push#sendAll(),该方法将JSON消息作为通知发送给所有关联的用户/连接。

当只有选定的用户被通知时,需要以某种方式修改sendAll()方法中的逻辑。

  • 仅通知负责修改相关实体的用户(可能是管理员用户或只有在成功登录后才能修改内容的注册用户)。
  • 仅通知特定用户(不是全部)。 “具体”是指,例如,当在本网站上投票时,只通知帖子所有者(帖子可能由具有足够权限的任何其他用户投票)。

当建立初始握手时,HttpSession可以按照this回答中的说明进行访问,但仍然不足以完成上述两个子弹的任务。由于它在第一次握手请求时可用,因此之后设置为该会话的任何属性在服务器端点中都不可用,即换句话说,在建立握手之后设置的任何会话属性都将不可用。

如上所述,仅通知所选用户的最可接受/规范方式是什么?需要sendAll()方法中的某些条件语句或其他某些条件语句。似乎除了用户的HttpSession之外,它必须做一些事情。

我使用GlassFish Server 4.1 / Java EE 7。

1 个答案:

答案 0 :(得分:24)

会话?

  

由于它在第一次握手请求时可用,因此之后设置为该会话的任何属性在服务器端点中都不可用,即换句话说,在建立握手后设置的任何会话属性都不会可用

似乎你被'#34; session&#34;这个词的含糊不清所困扰。会话的生命周期取决于上下文和客户端。 websocket(WS)会话与HTTP会话的生命周期不同。就像EJB会话与HTTP会话的生命周期不同。就像传统的Hibernate会话与HTTP会话的生命周期不同。等等。您可能已经理解的HTTP会话在此处解释How do servlets work? Instantiation, sessions, shared variables and multithreading。这里讨论了EJB会话JSF request scoped bean keeps recreating new Stateful session beans on every request?

WebSocket生命周期

WS会话与HTML文档表示的上下文相关联。客户端基本上是JavaScript代码。当JavaScript执行new WebSocket(url)时,WS会话开始。当JavaScript明确调用close()实例上的WebSocket函数时,或者当关联的HTML文档因页面导航(单击链接/书签或在浏览器中修改URL)而被卸载时,WS会话将停止; s地址栏),或页面刷新,或浏览器选项卡/窗口关闭。请注意,您可以在同一个DOM中创建多个WebSocket实例,通常每个实例都有不同的URL路径或查询字符串参数。

每次WS会话开始时(即每当JavaScript执行var ws = new WebSocket(url);时),这将触发一个握手请求,您可以通过下面的Configurator类访问相关的HTTP会话正如你已经发现的那样:

public class ServletAwareConfigurator extends Configurator {

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

}

因此,每个HTTP会话或HTML文档只会调用一次,就像您期望的那样。每次创建new WebSocket(url)时都会调用此方法。

然后将创建@ServerEndpoint带注释的类的全新实例,并将调用其@OnOpen带注释的方法。如果您熟悉JSF / CDI托管bean,只需将该类视为@ViewScoped,并将该方法视为@PostConstruct

@ServerEndpoint(value="/push", configurator=ServletAwareConfigurator.class)
public class PushEndpoint {

    private Session session;
    private EndpointConfig config;

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

    @OnMessage
    public void onMessage(String message) {
        // ...
    }

    @OnError
    public void onError(Throwable exception) {
        // ...
    }

    @OnClose
    public void onClose(CloseReason reason) {
        // ...
    }

}

请注意,此课程不同于一个servlet而不是应用程序作用域。它基本上是WS会话作用域。所以每个新的WS会话都有自己的实例。这就是为什么您可以安全地将SessionEndpointConfig指定为实例变量的原因。根据类设计(例如抽象模板等),您可以根据需要将Session添加回所有其他onXxx方法的第一个参数。这也得到了支持。

当JavaScript执行webSocket.send("some message")时,将调用@OnMessage带注释的方法。当WS会话关闭时,将调用@OnClose带注释的方法。如有必要,可以通过CloseReason.CloseCodes enum提供的密切原因代码确定完全接近的原因。抛出异常时将调用@OnError带注释的方法,通常是WS连接上的IO错误(断开管道,连接重置等)。

按登录用户

收集WS会话

回到仅通知特定用户的具体功能要求,您应该在上述说明之后理解您可以安全地依赖modifyHandshake()从关联的HTTP会话中提取登录用户,只要在用户登录后创建了

new WebSocket(url)

在带有public class UserAwareConfigurator extends Configurator { @Override public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) { HttpSession httpSession = (HttpSession) request.getHttpSession(); User user = (User) httpSession.getAttribute("user"); config.getUserProperties().put("user", user); } } 的WS端点类中,您可以使用@ServerEndpoint(configurator=UserAwareConfigurator.class)注释方法获取它,如下所示:

@OnOpen

您应该在应用范围内收集它们。您可以在端点类的@OnOpen public void onOpen(Session session, EndpointConfig config) { User user = (User) config.getUserProperties().get("user"); // ... } 字段中收集它们。或者,更好的是,如果您的环境中没有破坏WS端点中的CDI支持(在WildFly中工作,而不是在Tomcat + Weld中,不确定GlassFish),那么只需在应用程序范围内的CDI托管bean中收集它们{{ 1}}在端点类中。

static实例不是@Inject时(即用户登录时),请记住用户可以拥有多个WS会话。因此,您基本上需要在User结构中收集它们,或者可能是更细粒度的映射,它们通过用户ID或组/角色来映射它们,这毕竟允许更容易地找到特定用户。这一切都取决于最终的要求。这里至少是使用应用程序作用域CDI托管bean的启动示例:

null
Map<User, Set<Session>>

最后,您可以在@ApplicationScoped public class PushContext { private Map<User, Set<Session>> sessions; @PostConstruct public void init() { sessions = new ConcurrentHashMap<>(); } void add(Session session, User user) { sessions.computeIfAbsent(user, v -> ConcurrentHashMap.newKeySet()).add(session); } void remove(Session session) { sessions.values().forEach(v -> v.removeIf(e -> e.equals(session))); } }

中向特定用户发送消息,如下所示
@ServerEndpoint(value="/push", configurator=UserAwareConfigurator.class)
public class PushEndpoint {

    @Inject
    private PushContext pushContext;

    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        User user = (User) config.getUserProperties().get("user");
        pushContext.add(session, user);
    }

    @OnClose
    public void onClose(Session session) {
        pushContext.remove(session);
    }

}

作为CDI托管bean的PushContext具有额外的优势,可以在任何其他CDI托管bean中进行注入,从而可以更轻松地进行集成。

与关联用户一起开火CDI事件

public void send(Set<User> users, String message) { Set<Session> userSessions; synchronized(sessions) { userSessions = sessions.entrySet().stream() .filter(e -> users.contains(e.getKey())) .flatMap(e -> e.getValue().stream()) .collect(Collectors.toSet()); } for (Session userSession : userSessions) { if (userSession.isOpen()) { userSession.getAsyncRemote().sendText(message); } } } ,您最有可能按照上一个相关问题Real time updates from database using JSF/Java EE触发CDI事件,您已经拥有了已更改的实体,因此您应该能够找到与之关联的用户通过它们在模型中的关系来实现它。

  

仅通知负责修改相关实体的用户(可能是管理员用户或只有在成功登录后才能修改内容的注册用户)

PushContext
  

仅通知特定用户(不是全部)。 &#34;具体&#34;例如,当在本网站上投票时,只通知帖子所有者(该帖子可能由具有足够权限的任何其他用户投票)。

EntityListener

然后在CDI事件观察员中,将其传递出去:

@PostUpdate
public void onChange(Entity entity) {
    Set<User> editors = entity.getEditors();
    beanManager.fireEvent(new EntityChangeEvent(editors));
}

另见: