前言
在这个答案中,我将假设以下内容:
- 您对使用
<p:push> 不感兴趣(我将在中间留下确切的原因,您至少对使用新的 Java EE 7 / JSR356 WebSocket API 感兴趣)。
- 您想要一个应用程序范围的推送(即所有用户一次收到相同的推送消息;因此您对会话既不感兴趣也不查看范围推送)。
- 您想直接从 (MySQL) DB 端调用推送(因此您对使用实体侦听器从 JPA 端调用推送不感兴趣)。 编辑:无论如何我都会介绍这两个步骤。步骤 3a 描述 DB 触发器,步骤 3b 描述 JPA 触发器。要么使用它们,要么使用它们,而不是两者都使用!
1。创建 WebSocket 端点
首先创建一个@ServerEndpoint 类,它基本上将所有 websocket 会话收集到一个应用程序范围的集合中。请注意,在这个特定示例中,这只能是 static,因为每个 websocket 会话基本上都有自己的 @ServerEndpoint 实例(它们与 servlet 不同,因此是无状态的)。
@ServerEndpoint("/push")
public class Push {
private static final Set<Session> SESSIONS = ConcurrentHashMap.newKeySet();
@OnOpen
public void onOpen(Session session) {
SESSIONS.add(session);
}
@OnClose
public void onClose(Session session) {
SESSIONS.remove(session);
}
public static void sendAll(String text) {
synchronized (SESSIONS) {
for (Session session : SESSIONS) {
if (session.isOpen()) {
session.getAsyncRemote().sendText(text);
}
}
}
}
}
上面的示例有一个附加方法sendAll(),它将给定的消息发送到所有打开的 websocket 会话(即应用程序范围的推送)。请注意,此消息也可以是 JSON 字符串。
如果您打算将它们显式存储在应用程序范围(或(HTTP)会话范围)中,那么您可以使用this answer 中的ServletAwareConfig 示例。你知道,ServletContext 属性映射到 JSF 中的 ExternalContext#getApplicationMap()(而 HttpSession 属性映射到 ExternalContext#getSessionMap())。
2。在客户端打开 WebSocket 并监听它
使用这段 JavaScript 打开一个 websocket 并监听它:
if (window.WebSocket) {
var ws = new WebSocket("ws://example.com/contextname/push");
ws.onmessage = function(event) {
var text = event.data;
console.log(text);
};
}
else {
// Bad luck. Browser doesn't support it. Consider falling back to long polling.
// See http://caniuse.com/websockets for an overview of supported browsers.
// There exist jQuery WebSocket plugins with transparent fallback.
}
到目前为止,它只记录推送的文本。我们希望将此文本用作更新菜单组件的说明。为此,我们需要一个额外的<p:remoteCommand>。
<h:form>
<p:remoteCommand name="updateMenu" update=":menu" />
</h:form>
假设您正在通过Push.sendAll("updateMenu") 将 JS 函数名称作为文本发送,那么您可以按如下方式解释和触发它:
ws.onmessage = function(event) {
var functionName = event.data;
if (window[functionName]) {
window[functionName]();
}
};
同样,当使用 JSON 字符串作为消息(您可以通过 $.parseJSON(event.data) 解析)时,更多动态是可能的。
3a。 要么 从 DB 端触发 WebSocket 推送
现在我们需要从数据库端触发命令Push.sendAll("updateMenu")。让数据库在 Web 服务上触发 HTTP 请求的最简单方法之一。一个普通的 servlet 足以像 Web 服务一样工作:
@WebServlet("/push-update-menu")
public class PushUpdateMenu extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Push.sendAll("updateMenu");
}
}
如有必要,您当然可以根据请求参数或路径信息参数化推送消息。如果允许调用者调用此 servlet,请不要忘记执行安全检查,否则世界上除数据库本身之外的任何其他人都可以调用它。例如,您可以检查调用者的 IP 地址,如果 DB 服务器和 Web 服务器都运行在同一台机器上,这将很方便。
为了让数据库在该 servlet 上触发 HTTP 请求,您需要创建一个可重用的存储过程,它基本上调用操作系统特定的命令来执行 HTTP GET 请求,例如curl。 MySQL 本身不支持执行特定于操作系统的命令,因此您需要首先为此安装用户定义函数 (UDF)。在mysqludf.org,您可以找到我们感兴趣的SYS。它包含我们需要的sys_exec() 函数。安装后,在 MySQL 中创建以下存储过程:
DELIMITER //
CREATE PROCEDURE menu_push()
BEGIN
SET @result = sys_exec('curl http://example.com/contextname/push-update-menu');
END //
DELIMITER ;
现在您可以创建将调用它的插入/更新/删除触发器(假设表名为 menu):
CREATE TRIGGER after_menu_insert
AFTER INSERT ON menu
FOR EACH ROW CALL menu_push();
CREATE TRIGGER after_menu_update
AFTER UPDATE ON menu
FOR EACH ROW CALL menu_push();
CREATE TRIGGER after_menu_delete
AFTER DELETE ON menu
FOR EACH ROW CALL menu_push();
3b。 或者从 JPA 端触发 WebSocket 推送
如果您的要求/情况只允许监听 JPA 实体更改事件,因此对数据库的外部更改确实不需要需要被覆盖,那么您可以而不是 步骤 3a 中描述的 DB 触发器也只使用 JPA 实体更改侦听器。你可以通过@Entity类上的@EntityListeners注解注册它:
@Entity
@EntityListeners(MenuChangeListener.class)
public class Menu {
// ...
}
如果您碰巧使用单个 Web 配置文件项目,其中所有内容 (EJB/JPA/JSF) 都放在同一个项目中,那么您可以直接在其中调用 Push.sendAll("updateMenu")。
public class MenuChangeListener {
@PostPersist
@PostUpdate
@PostRemove
public void onChange(Menu menu) {
Push.sendAll("updateMenu");
}
}
然而,在“企业”项目中,服务层代码(EJB/JPA/etc)通常在 EJB 项目中分离,而 Web 层代码(JSF/Servlets/WebSocket/etc)则保留在 Web 项目中。 EJB 项目应该有no single 对Web 项目的依赖。在这种情况下,您最好使用 CDI Event 而不是 Web 项目可以使用 @Observes。
public class MenuChangeListener {
// Outcommented because it's broken in current GF/WF versions.
// @Inject
// private Event<MenuChangeEvent> event;
@Inject
private BeanManager beanManager;
@PostPersist
@PostUpdate
@PostRemove
public void onChange(Menu menu) {
// Outcommented because it's broken in current GF/WF versions.
// event.fire(new MenuChangeEvent(menu));
beanManager.fireEvent(new MenuChangeEvent(menu));
}
}
(注意结果;在当前版本(4.1 / 8.2)中,GlassFish 和 WildFly 中注入 CDI Event 被破坏;解决方法改为通过 BeanManager 触发事件;如果这仍然没有工作,CDI 1.1 替代方案是CDI.current().getBeanManager().fireEvent(new MenuChangeEvent(menu)))
public class MenuChangeEvent {
private Menu menu;
public MenuChangeEvent(Menu menu) {
this.menu = menu;
}
public Menu getMenu() {
return menu;
}
}
然后在web项目中:
@ApplicationScoped
public class Application {
public void onMenuChange(@Observes MenuChangeEvent event) {
Push.sendAll("updateMenu");
}
}
更新:在 2016 年 4 月 1 日(上述答案后半年),OmniFaces 在 2.3 版中引入了 <o:socket>,这将使这一切变得不那么迂回。即将推出的 JSF 2.3 <f:websocket> 主要基于 <o:socket>。另见How can server push asynchronous changes to a HTML page created by JSF?