使用 JSF/Java EE 从数据库实时更新
Posted
技术标签:
【中文标题】使用 JSF/Java EE 从数据库实时更新【英文标题】:Real time updates from database using JSF/Java EE 【发布时间】:2014-11-14 20:48:57 【问题描述】:我有一个应用程序在以下环境中运行。
GlassFish 服务器 4.0 JSF 2.2.8-02 PrimeFaces 5.1 最终版 PrimeFaces 扩展 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 是视图范围的。它们都应该是应用程序范围的,仅在应用程序启动时加载一次,并且仅在相应数据库表(类别/子类别/产品等)中的某些内容更新/更改时更新。
我做了一些尝试来了解 WebSokets(以前从未尝试过,对 WebSokets 来说是全新的),例如 this 和 this。他们在 GlassFish 4.0 上运行良好,但不涉及数据库。我仍然无法正确理解 WebSokets 是如何工作的。尤其是涉及到数据库的时候。
在这种情况下,当更新/删除/添加到相应的数据库表时,如何通知关联的客户端并使用数据库中的最新值更新上述 CSS 菜单?
一个简单的例子就好了。
【问题讨论】:
要清楚,您已经在 JSF 端(使用 websockets)完成了推送部分,您只是在询问如何在实体更改事件期间从 JPA 端触发它?所以不需要考虑 JPA 无法控制的 DB 中的外部变化? 无论哪种方式,如果(关联的)数据库表中的某些内容发生更改,则应反映并通知关联的客户端。 (我自己缺乏它背后的实际概念——如何正确地做到这一点。我以前从未做过这样的事情)。 我阅读了this Oracle 文档,包括问题中的这两个实践练习,但我仍然不明白它是如何工作的。我在问题中写的只是我的主要想法。它不一定以那种方式发生。 上一条评论的链接坏了。 This 是 Oracle 教程的链接。 A good example 开头(确实它使用 GlassFish 和 NetBeans,但不用说它可以在任何等效环境中验证)。 【参考方案1】:前言
在这个答案中,我将假设以下内容:
您对使用<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?
【讨论】:
这可以通过使用 JPA 回调(或其他东西)来完成,例如@PrePersist
、@PostPersist
、@PreUpdate
...? (我不擅长数据库方面的 PL/SQL 尽管稍后我会以某种方式使用它,不用担心。很好的答案:) )。
我在对您的问题的第一条评论中问您是否需要考虑对数据库的外部更改。您回答“无论哪种方式”,所以我知道您也想涵盖这一点。 JPA 实体侦听器不足以涵盖这一点,因为当您从应用程序外部操作数据库时(例如,通过某些数据库管理工具或什至共享同一数据库的另一个应用程序),它们不会收到通知。
如果用户只对 JPA 实体侦听器感兴趣,我在步骤 3b 中扩展了答案。
在大多数情况下,我依赖于 JPA 端和 CDI 事件,很高兴知道它不仅限于这一端,UDF 在某些情况下似乎很有希望,因为在一个数据库上可能有多个应用程序!这应该是一篇博文 :)
你提到他可能对 p:push 不感兴趣,你会在中间留下确切的原因吗?您可能认为 p:push 不适合的原因是什么?【参考方案2】:
由于您使用的是 Primefaces 和 Java EE 7,它应该很容易实现:
使用 Primefaces 推送(此处为 http://www.primefaces.org/showcase/push/notify.xhtml 示例)
创建一个监听 Websocket 端点的视图 创建一个数据库监听器,它在数据库更改时产生一个 CDI 事件 事件的负载可以是最新数据的增量,也可以是更新信息 通过 Websocket 将 CDI 事件传播到所有客户端 客户端更新数据希望这会有所帮助 如果您需要更多详细信息,请询问
问候
【讨论】:
【参考方案3】: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.
【讨论】:
<p:poll>
对于触发在指定时间点发生的周期性操作很有用。在这种情况下,它不像定期发生的长轮询。这里的操作不依赖于定时器。它应该发生,只有当其他事情发生或永远不会发生时。以上是关于使用 JSF/Java EE 从数据库实时更新的主要内容,如果未能解决你的问题,请参考以下文章
Twitter Storm系列flume-ng+Kafka+Storm+HDFS 实时系统搭建