前言
在本篇答案中,我将假设以下情况:
- 您不想使用
<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
示例。您知道,在JSF中,
ServletContext
属性映射到
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 {
}
目前它仅记录推送的文本。我们希望将此文本用作更新菜单组件的指令。为此,我们需要另一个
<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. 或者从数据库端触发WebSocket推送
现在我们需要从数据库端触发命令Push.sendAll("updateMenu")
。其中最简单的一种方式是让数据库向Web服务发送HTTP请求。一个普通的Vanilla 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,否则世界上除了DB本身之外的任何人都能够调用它。例如,您可以检查调用者的IP地址,这在DB服务器和Web服务器运行在同一台机器上时非常方便。
为了让DB在该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实体更改事件,并且因此不需要覆盖对DB的外部更改,则可以替代步骤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等)通常分离在EJB项目中,而Web层代码(JSF/Servlets/WebSocket等)则保留在Web项目中。EJB项目不应该对Web项目产生
无单一依赖。在这种情况下,最好触发CDI
Event
,Web项目可以
@Observes
。
public class MenuChangeListener {
@Inject
private BeanManager beanManager;
@PostPersist
@PostUpdate
@PostRemove
public void onChange(Menu menu) {
beanManager.fireEvent(new MenuChangeEvent(menu));
}
}
请注意注释;在当前版本的GlassFish和WildFly(4.1 / 8.2)中,注入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;
}
}
然后在网络项目中:
@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>
。另请参见如何将异步变化推送到JSF创建的HTML页面中的服务器?