Jakarta ee 在数据库中修改某些内容时,仅通过WebSocket通知特定用户
为了通过WebSocket通知所有用户,当在选定的JPA实体中修改某些内容时,我使用以下基本方法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
@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");
}