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

Jakarta ee 在数据库中修改某些内容时,仅通过WebSocket通知特定用户,jakarta-ee,websocket,java-ee-7,real-time-updates,Jakarta Ee,Websocket,Java Ee 7,Real Time Updates,为了通过WebSocket通知所有用户,当在选定的JPA实体中修改某些内容时,我使用以下基本方法 @ServerEndpoint("/Push") public class Push { private static final Set<Session> sessions = new LinkedHashSet<Session>(); @OnOpen public void onOpen(Session session) { s

为了通过WebSocket通知所有用户,当在选定的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);
                }
            }
        }
    }
}
观察者/使用者调用WebSockets端点中定义的静态方法
Push#sendAll()
,该方法将JSON消息作为通知发送给所有关联用户/连接

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

  • 仅通知负责修改相关实体的用户(可能是管理员用户或注册用户,只有在成功登录后才能修改某些内容)
  • 仅通知特定用户(不是全部用户)。“特定”是指,例如,当一篇文章在本网站上被投票时,只通知文章所有者(该文章可以由任何其他具有足够权限的用户投票)
当建立初始握手时,
HttpSession
可以按照回答中所述进行访问,但仍不足以通过两个项目完成上述任务。由于它在发出第一次握手请求时可用,因此在服务器端点中,随后为该会话设置的任何属性都将不可用,即,在建立握手后设置的任何会话属性都将不可用

如上所述,仅通知选定用户的最可接受/规范的方式是什么?
sendAll()
方法中的某些条件语句或其他地方是必需的。它似乎必须执行一些操作,而不仅仅是用户的
HttpSession

我使用GlassFish服务器4.1/JavaEE7。

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

你似乎被“会话”这个词的模糊性所困扰。会话的生存期取决于上下文和客户端。websocket(WS)会话与HTTP会话的生存期不同。就像EJB会话与HTTP会话没有相同的生存期一样。与此类似,传统Hibernate会话与HTTP会话的生存期不同。等等。这里将解释您可能已经理解的HTTP会话。这里将解释EJB会话

WebSocket生命周期 WS-session绑定到HTML文档表示的上下文。客户端基本上就是JavaScript代码。当JavaScript执行
新建WebSocket(url)
时,WS会话开始。当JavaScript显式调用
WebSocket
实例上的
close()
函数时,或者当页面导航(单击链接/书签或修改浏览器地址栏中的URL)或页面刷新或浏览器选项卡/窗口关闭导致卸载关联的HTML文档时,WS会话停止。请注意,您可以在同一个DOM中创建多个
WebSocket
实例,通常每个实例具有不同的URL路径或查询字符串参数

每次WS-session启动时(即每次JavaScript执行
var WS=new WebSocket(url);
),都会触发握手请求,这样您就可以通过下面的类访问相关的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文档只调用一次。每次创建
新WebSocket(url)
时都会调用此函数

然后将创建带注释类的全新实例,并调用其带注释的方法。如果您熟悉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-session作用域。因此,每个新的WS-session都有自己的实例。这就是为什么您可以安全地将
会话
端点配置
指定为实例变量。根据类设计(例如抽象模板等),如有必要,可以将
Session
添加回所有其他
onXxx
方法的第一个参数。这也得到了支持

当JavaScript执行
webSocket.send(“某些消息”)
时,将调用带注释的方法。WS会话关闭时将调用带注释的方法。如有必要,可通过enum提供的关闭原因代码确定确切的关闭原因。当抛出异常时,通常作为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);
    }

}
在具有
@ServerEndpoint(configurator=UserAwareConfigurator.class)
的WS-endpoint类中,您可以通过
@OnOpen
注释方法获得它,如下所示:

@OnOpen
public void onOpen(Session session, EndpointConfig config) {
    User user = (User) config.getUserProperties().get("user");
    // ...
}
您应该在应用程序范围内收集它们。您可以在endpoint类的
静态
字段中收集它们。或者,更好的方法是,如果WS-endpoint中的CDI支持在您的环境中没有被破坏(在WildFly中有效,在Tomcat+Weld中无效,对GlassFish不确定),那么只需将它们收集到应用程序范围内的CDI托管bean中,然后将其注入endpoint类中

User
实例不是
null
(即当用户登录时),则
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
    User user = (User) config.getUserProperties().get("user");
    // ...
}
@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);
    }

}
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);
        }
    }
}
@PostUpdate
public void onChange(Entity entity) {
    Set<User> editors = entity.getEditors();
    beanManager.fireEvent(new EntityChangeEvent(editors));
}
@PostUpdate
public void onChange(Entity entity) {
    User owner = entity.getOwner();
    beanManager.fireEvent(new EntityChangeEvent(Collections.singleton(owner)));
}
public void onEntityChange(@Observes EntityChangeEvent event) {
    pushContext.send(event.getUsers(), "message");
}