使用JSF / Java EE从数据库进行实时更新

时间:2014-09-20 10:29:00

标签: jsf java-ee primefaces websocket java-ee-7

我在以下环境中运行了一个应用程序。

  • GlassFish Server 4.0
  • JSF 2.2.8-02
  • PrimeFaces 5.1 final
  • PrimeFaces Extension 2.1.0
  • OmniFaces 1.8.1
  • 具有JPA 2.1的EclipseLink 2.5.2
  • MySQL 5.6.11
  • JDK-7u11

有几个公共页面从数据库中延迟加载。一些CSS菜单显示在模板页面的标题上,如显示类别/子类别特色,最畅销,新到货等产品。

CSS菜单是根据数据库中各种类别的产品从数据库动态填充的。

这些菜单填充在每个页面加载上,这是完全没有必要的。其中一些菜单需要复杂/昂贵的JPA标准查询。

目前填充这些菜单的JSF托管bean是视图范围的。它们都应该是应用程序作用域,在应用程序启动时只加载一次,并且只有在更新/更改相应数据库表(类别/子类别/产品等)中的某些内容时才更新。

我尝试了解WebSokets(以前从未尝试过,对WebSokets来说是全新的),例如thisthis。他们在GlassFish 4.0上运行良好,但他们并不涉及数据库。我仍然无法正确理解WebSokets的工作方式。特别是涉及数据库时。

在这种情况下,当更新/删除/添加到相应的数据库表时,如何通知关联的客户端并使用数据库中的最新值更新上述CSS菜单?

一个简单的例子/ s会很棒。

3 个答案:

答案 0 :(得分:46)

前言

在这个答案中,我将假设以下内容:

  • 您对使用<p:push>不感兴趣(我在中间留下确切的理由,您至少对使用新的Java EE 7 / JSR356 WebSocket API感兴趣)。
  • 您想要一个应用程序作用域推送(即所有用户同时获得相同的推送消息;因此您对会话不感兴趣,也不会查看范围推送)。
  • 您想直接从(MySQL)数据库端调用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)会话范围内),那么您可以使用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。 从数据库端触发WebSocket推送

现在我们需要从数据库端触发命令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();


3B。 从JPA端触发WebSocket推送

如果您的要求/情况仅允许监听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

  • 创建一个侦听Websocket端点的视图
  • 创建一个数据库侦听器,在数据库更改时生成CDI事件
    • 事件的有效负载可以是最新数据的增量,也可以只是更新信息
  • 通过Websocket将CDI事件传播到所有客户端
  • 客户更新数据

希望这会有所帮助 如果您需要更多细节,请询问

此致

答案 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.
    }
}