我有一个应用程序在以下环境中运行。
- GlassFish Server 4.0 JSF 2.2.8-02
- PrimeFaces 5.1 final
- PrimeFaces Extension 2.1.0
- OmniFaces 1.8.1
- EclipseLink 2.5.2有JPA 2.1 MySQL 5.6.11
- JDK-7u11
有几个公共页面是从数据库惰性加载的。一些CSS菜单显示在模板页面的页眉,如显示类别/子类别明智的特色,最畅销,新到货等产品。
CSS菜单是根据数据库中产品的不同类别从数据库动态填充的。
这些菜单在每次页面加载时都被填充,这是完全不必要的。其中一些菜单需要复杂/昂贵的JPA标准查询。
目前,填充这些菜单的JSF托管bean是视图范围的。它们都应该在应用程序范围内,只在应用程序启动时加载一次,并且只有在相应的数据库表(类别/子类别/产品等)更新/更改时才更新。
我做了一些尝试来理解websoket(以前从未尝试过,对websoket完全陌生),像这样和这样。他们在GlassFish 4.0上工作得很好,但他们不涉及数据库。我仍然不能正确地理解websockets是如何工作的。特别是当涉及到数据库时。
在这种情况下,如何通知相关的客户端,并更新上述CSS菜单与数据库中的最新值,当更新/删除/添加到相应的数据库表?
一个简单的例子就可以了。
引言
在这个答案中,我将假设如下:
- 你对使用
<p:push>
不感兴趣(我将在中间留下确切的原因,你至少对使用新的Java EE 7/JSR356 WebSocket API感兴趣)。 - 你想要一个应用范围内的推送(即所有用户一次得到相同的推送消息;因此,您对会话或视图作用域的推送不感兴趣)。
- 您希望直接从(MySQL) DB端调用push(因此您对使用实体侦听器从JPA端调用push不感兴趣)。编辑无论如何,我将介绍这两个步骤。步骤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)会话范围)中,那么您可以在这个答案中使用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 {
// 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)
解析)时,可以实现更多动态。
3。从DB端触发WebSocket推送
现在我们需要从DB端触发命令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,不要忘记执行安全检查,否则除了DB本身之外的任何人都可以调用它。例如,如果DB服务器和web服务器在同一台机器上运行,您可以检查调用者的IP地址,这很方便。
为了让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();
3 b。或从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");
}
}
然而,在"enterprise"项目中,服务层代码(EJB/JPA/等)通常在EJB项目中分离,而web层代码(JSF/servlet/WebSocket/等)则保留在web项目中。EJB项目不应该单独依赖于web项目。在这种情况下,您最好触发CDIEvent
,而不是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)中,注入CDIEvent
在GlassFish和WildFly中都被打破;变通方法通过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>
。另请参阅服务器如何将异步更改推送到JSF创建的HTML页面?
由于您使用的是Primefaces和Java EE 7,因此应该很容易实现:
使用Primefaces Push(示例http://www.primefaces.org/showcase/push/notify.xhtml)
- 创建一个监听Websocket端点的视图
- 创建数据库侦听器,在数据库更改时产生CDI事件
- 事件的有效载荷可以是最新数据的增量,也可以只是和更新信息
- 通过Websocket向所有客户端传播CDI事件
- 客户端更新数据
希望这有帮助如果你需要更多的细节就问
对
PrimeFaces具有自动更新组件的投票功能。在下面的示例中,<h:outputText>
将由<p:poll>
每3秒自动更新一次。
如何通知关联的客户端并用数据库中的最新值更新上述CSS菜单?
创建像process()
这样的侦听器方法来选择菜单数据。<p:poll>
将自动更新您的菜单组件。
<h:form>
<h:outputText id="count"
value="#{AutoCountBean.count}"/> <!-- Replace your menu component-->
<p:poll interval="3" listener="#{AutoCountBean.process}" update="count" />
</h:form>
@ManagedBean
@ViewScoped
public class AutoCountBean implements Serializable {
private int count;
public int getCount() {
return count;
}
public void process() {
number++; //Replace your select data from db.
}
}