我在以下环境中运行了一个应用程序。
有几个公共页面从数据库中延迟加载。一些CSS菜单显示在模板页面的标题上,如显示类别/子类别特色,最畅销,新到货等产品。
CSS菜单是根据数据库中各种类别的产品从数据库动态填充的。
这些菜单填充在每个页面加载上,这是完全没有必要的。其中一些菜单需要复杂/昂贵的JPA标准查询。
目前填充这些菜单的JSF托管bean是视图范围的。它们都应该是应用程序作用域,在应用程序启动时只加载一次,并且只有在更新/更改相应数据库表(类别/子类别/产品等)中的某些内容时才更新。
我尝试了解WebSokets(以前从未尝试过,对WebSokets来说是全新的),例如this和this。他们在GlassFish 4.0上运行良好,但他们并不涉及数据库。我仍然无法正确理解WebSokets的工作方式。特别是涉及数据库时。
在这种情况下,当更新/删除/添加到相应的数据库表时,如何通知关联的客户端并使用数据库中的最新值更新上述CSS菜单?
一个简单的例子/ s会很棒。
答案 0 :(得分:46)
在这个答案中,我将假设以下内容:
<p:push>
不感兴趣(我在中间留下确切的理由,您至少对使用新的Java EE 7 / JSR356 WebSocket API感兴趣)。
首先创建一个@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()
)。
使用这段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)
解析)时,可能会有更多动态。
现在我们需要从数据库端触发命令Push.sendAll("updateMenu")
。允许DB在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地址,如果数据库服务器和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();
如果您的要求/情况仅允许监听JPA实体更改事件,因此需要覆盖对不的外部更改,那么您可以而不是如步骤3a中所述的DB触发器也仅使用JPA实体更改侦听器。您可以通过@EntityListeners
类上的@Entity
注释注册它:
@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");
}
}
然而,在&#34;企业&#34;项目,服务层代码(EJB / JPA / etc)通常在EJB项目中分离,而Web层代码(JSF / Servlets / WebSocket / etc)保存在Web项目中。 EJB项目应该对Web项目具有no single依赖性。在这种情况下,您最好启动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;
}
}
然后在网络项目中:
@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?
答案 1 :(得分:5)
由于您使用的是Primefaces和Java EE 7,因此应该很容易实现:
使用Primefaces Push(例如http://www.primefaces.org/showcase/push/notify.xhtml)
希望这会有所帮助 如果您需要更多细节,请询问
此致
答案 2 :(得分:1)
PrimeFaces具有轮询功能,可自动更新组件。在以下示例中,<h:outputText>
将每隔3秒自动更新<p:poll>
。
如何通知关联的客户端并使用数据库中的最新值更新上述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.
}
}