使用JSF / Java EE从数据库实时更新
我有一个应用程序在以下环境中运行。
- GlassFish Server 4.0
- JSF 2.2.8-02
- PrimeFaces 5.1 final
- PrimeFaces扩展2.1.0
- OmniFaces 1.8.1
- EclipseLink 2.5.2具有JPA 2.1
- MySQL 5.6.11
- JDK-7u11
有几个从数据库中延迟加载的公共页面。 一些CSS菜单显示在模板页面的标题上,如显示类别/子类别特色,畅销品,新到货等产品。
CSS菜单根据数据库中各种类别的产品从数据库dynamic填充。
这些菜单是填充在每个页面加载完全不必要的。 其中一些菜单需要复杂/昂贵的JPA标准查询。
目前,填充这些菜单的JSF托pipe的bean是视图作用域。 它们都应该是应用程序的范围,只能在应用程序启动时加载一次,并且只有在相应数据库表(类别/子类别/产品等)中的某些内容被更新/更改时才会被更新。
我做了一些尝试来理解WebSokets(从来没有尝试过,对WebSokets是全新的),就像这样和这个 。 他们在GlassFish 4.0上工作得很好,但他们不涉及数据库。 我仍然无法正确理解WebSokets的工作方式。 特别是涉及数据库的时候。
在这种情况下,当更新/删除/添加到相应的数据库表时,如何通知关联的客户端并使用数据库中的最新值更新上述CSS菜单?
一个简单的例子会很棒。
前言
在这个答案中,我将假设如下:
- 您对使用
<p:push>
不感兴趣(我将在中间留下确切的原因,您至less对使用新的Java EE 7 / JSR356 WebSocket API感兴趣)。 - 你想要一个应用程序范围的推送(即所有的用户一次获得相同的推送消息,因此你不感兴趣的一个会议或查看范围推送)。
- 你想从(MySQL)数据库端直接调用push(因此你不希望使用实体监听器从JPA端调用push)。 编辑 :我会涵盖两个步骤。 步骤3a描述了DB触发器,步骤3b描述了JPA触发器。 使用它们,或者,而不是两个!
1.创build一个WebSocket端点
首先创build一个@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会话(即应用程序范围的推送)。 请注意,这个消息也可以是一个JSONstring。
如果你打算明确地将它们存储在应用程序范围(或(HTTP)会话范围),那么你可以在这个答案中使用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. }
截至目前,它只是logging推送的文字。 我们想用这个文本作为更新菜单组件的指令。 为此,我们需要额外的<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](); } };
再次,当使用JSONstring作为消息(您可以通过$.parseJSON(event.data)
parsing)时,更多的dynamic是可能的。
3A。 从数据库端触发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"); } }
如有必要,您当然有机会根据请求参数或path信息参数化推送消息。 如果允许调用者调用这个servlet,不要忘记执行安全检查,否则世界上除了DB之外的其他任何人都可以调用它。 您可以检查来电者的IP地址,例如,如果数据库服务器和networking服务器在同一台计算机上运行,这很方便。
为了让DB在这个servlet上激发一个HTTP请求,你需要创build一个可重用的存储过程,它主要调用特定于操作系统的命令来执行HTTP GET请求,例如curl
。 MySQL本身不支持执行特定于操作系统的命令,因此您需要首先安装用户定义的函数(UDF)。 在mysqludf.org你可以find一堆SYS是我们感兴趣的。 它包含我们需要的sys_exec()
函数。 安装完成后,在MySQL中创build以下存储过程:
DELIMITER // CREATE PROCEDURE menu_push() BEGIN SET @result = sys_exec('curl http://example.com/contextname/push-update-menu'); END // DELIMITER ;
现在你可以创buildinsert / update / delete触发器来调用它(假设表名被命名为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实体更改事件,并且因此不需要覆盖对数据库的外部更改,则可以使用JPA实体更改侦听器来代替步骤3a中所述的数据库触发器。 你可以在@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项目中的服务层代码(EJB / JPA / etc)通常是分开的,而Web层代码(JSF / Servlets / WebSocket / etc)则保存在Web项目中。 EJB项目不应该单独依赖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>
。 另请参见服务器如何将asynchronous更改推送到由JSF创build的HTML页面?
由于您使用的是Primefaces和Java EE 7,因此应该很容易实现:
使用Primefaces Push(例如http://www.primefaces.org/showcase/push/notify.xhtml )
- 创build一个监听Websocket端点的视图
- 创build一个数据库监听器,它会在数据库更改时生成一个CDI事件
- 事件的有效载荷可以是最新数据的增量,也可以是更新信息
- 通过Websocket将CDI活动传播给所有客户
- 客户更新数据
希望这有助于如果你需要一些更多的细节只是问
问候
PrimeFaces具有轮询function来自动更新组件。 在以下示例中, <h:outputText>
将由<p:poll>
每3秒自动更新一次。
如何通知关联客户端,并用数据库中的最新值更新上述CSS菜单?
创build一个像process()
这样的监听器方法来select你的菜单数据。 <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. } }