使用JSF / Java EE从数据库进行实时更新 [英] Real time updates from database using JSF/Java EE

查看:131
本文介绍了使用JSF / Java EE从数据库进行实时更新的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have one application running in the following environment.


  • 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

  • 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 having JPA 2.1
  • MySQL 5.6.11
  • JDK-7u11

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

There are several public pages which are lazily loaded from the database. A few CSS menus are displayed on the header of the template page like displaying category/subcategory-wise featured, top seller, new arrival etc products.

CSS菜单已填充基于数据库中各种类别的产品从数据库动态生成。

The CSS menus are populated dynamically from the database based on various categories of products in the database.

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

These menus are populated on every page load which is completely unnecessary. Some of these menus require complex/expensive JPA criteria queries.

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

Currently the JSF managed beans that populate these menus are view scoped. They all should be application scoped, be loaded only once on application start up and be updated only when something in the corresponding database tables (category/subcategory/product etc) is updated/changed.

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

I made some attempts to understand WebSokets (never tried before, completely new to WebSokets) like this and this. They worked fine on GlassFish 4.0 but they don't involve databases. I'm still not able to understand properly how WebSokets work. Especially when database is involved.

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

In this scenario, how to notify the associated clients and update the above-mentioned CSS menus with the latest values from the database, when something is updated/deleted/added to the corresponding database tables?

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

推荐答案

前言



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

Preface

In this answer, I'll assume the following:


  • 你对使用< p:push> 不感兴趣(我会留下确切的理由在中间,你至少对使用新的Java EE 7 / JSR356 WebSocket API感兴趣。

  • 你想要一个应用程序作用域推送(即所有用户获得相同的推送消息)一次;因此你对会话不感兴趣,也不会查看范围推送。)

  • 你想直接从(MySQL)数据库端调用push(因此你对调用push不感兴趣从JPA方面使用实体监听器)。 修改:无论如何,我将完成这两个步骤。步骤3a描述了DB触发,步骤3b描述了JPA触发。使用它们 - 或者,不要两者都使用它们!

  • You're not interested in using <p:push> (I'll leave the exact reason in the middle, you're at least interested in using the new Java EE 7 / JSR356 WebSocket API).
  • You want an application scoped push (i.e. all users gets the same push message at once; thus you're not interested in a session nor view scoped push).
  • You want to invoke push directly from (MySQL) DB side (thus you're not interested in invoking push from JPA side using an entity listener). Edit: I'll cover both steps anyway. Step 3a describes DB trigger and step 3b describes JPA trigger. Use them either-or, not both!


首先创建一个 @ServerEndpoint 类,它基本上将所有websocket会话收集到应用程序范围集中。请注意,在此特定示例中,这只能是 static ,因为每个websocket会话基本上都有自己的 @ServerEndpoint 实例(它们与servlet不同,因此无状态)。

First create a @ServerEndpoint class which basically collects all websocket sessions into an application wide set. Note that this can in this particular example only be static as every websocket session basically gets its own @ServerEndpoint instance (they are unlike servlets thus stateless).

@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字符串。

The example above has an additional method sendAll() which sends the given message to all open websocket sessions (i.e. application scoped push). Note that this message can also quite good be a JSON string.

如果您打算将它们显式存储在应用程序范围(或(HTTP)会话范围内),那么您可以使用 ServletAwareConfig 示例-jsr-356-serverendpo / 23405830#23405830>这个答案。你知道, ServletContext 属性映射到JSF中的 ExternalContext#getApplicationMap()(以及 HttpSession 属性映射到 ExternalContext#getSessionMap())。

If you intend to explicitly store them in application scope (or (HTTP) session scope), then you can use the ServletAwareConfig example in this answer for that. You know, ServletContext attributes map to ExternalContext#getApplicationMap() in JSF (and HttpSession attributes map to ExternalContext#getSessionMap()).


使用这段JavaScript打开websocket并听取它:

Use this piece of JavaScript to open a websocket and listen on it:

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>

As of now it merely logs the pushed text. We'd like to use this text as an instruction to update the menu component. For that, we'd need an additional <p:remoteCommand>.

<h:form>
    <p:remoteCommand name="updateMenu" update=":menu" />
</h:form>

想象一下,您通过 Push发送JS函数名作为文本。 sendAll(updateMenu),然后您可以按如下方式解释并触发它:

Imagine that you're sending a JS function name as text by Push.sendAll("updateMenu"), then you could interpret and trigger it as follows:

    ws.onmessage = function(event) {
        var functionName = event.data;
        if (window[functionName]) {
            window[functionName]();
        }
    };

再次使用JSON字符串作为消息(可以通过解析) $ .parseJSON(event.data)),可以提供更多动态。

Again, when using a JSON string as message (which you could parse by $.parseJSON(event.data)), more dynamics is possible.


现在我们需要触发命令 Push.sendAll(updateMenu)来自数据库端的。允许DB在Web服务上触发HTTP请求的最简单方法之一。一个普通的vanilla servlet足以像Web服务一样:

Now we need to trigger the command Push.sendAll("updateMenu") from the DB side. One of simplest ways it letting the DB to fire a HTTP request on a web service. A plain vanilla servlet is more than sufficient to act like a web service:

@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服务器都在同一台计算机上运行,​​这很方便。

You've of course the opportunity to parameterize the push message based on request parameters or path info, if necessary. Don't forget to perform security checks if the caller is allowed to invoke this servlet, otherwise anyone else in the world other then the DB itself would be able to invoke it. You could check the caller's IP address, for example, which is handy if both DB server and web server run at the same machine.

为了让数据库触发在该servlet上的HTTP请求,您需要创建一个可重用的存储过程,它基本上调用操作系统特定的命令来执行HTTP GET请求,例如卷曲。 MySQL本身不支持执行特定于操作系统的命令,因此您需要首先安装用户定义的函数(UDF)。在 mysqludf.org ,您可以找到一堆 SYS 是我们感兴趣的。它包含我们需要的 sys_exec()函数。安装后,在MySQL中创建以下存储过程:

In order to let the DB fire a HTTP request on that servlet, you need to create a reusable stored procedure which basically invokes the operating system specific command to execute a HTTP GET request, e.g. curl. MySQL doesn't natively support executing an OS specific command, so you'd need to install an user defined function (UDF) for that first. At mysqludf.org you can find a bunch of which SYS is of our interest. It contains the sys_exec() function which we need. Once installed it, create the following stored procedure in MySQL:

DELIMITER //
CREATE PROCEDURE menu_push()
BEGIN 
SET @result = sys_exec('curl http://example.com/contextname/push-update-menu'); 
END //
DELIMITER ;

现在你可以创建插入/更新/删除触发器来调用它(假设表名被命名为 menu ):

Now you can create insert/update/delete triggers which will invoke it (assuming table name is named 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实体更改事件,从而外部更改DB需要覆盖,然后您可以而不是 DB触发器,如步骤3a中所述,也只使用JPA实体更改侦听器。您可以在 @Entity 类的 @EntityListeners 注释中注册它:

If your requirement/situation allows to listen on JPA entity change events only, and thus external changes to the DB does not need to be covered, then you can instead of DB triggers as described in step 3a also just use a JPA entity change listener. You can register it via @EntityListeners annotation on the @Entity class:

@Entity
@EntityListeners(MenuChangeListener.class)
public class Menu {
    // ...
}

如果您碰巧使用单个Web配置文件项目,其中所有内容(EJB / JPA / JSF)被抛在一起在同一个项目中,你可以直接在那里调用 Push.sendAll(updateMenu)

If you happen to use a single web profile project wherein everything (EJB/JPA/JSF) is thrown together in the same project, then you can just directly invoke Push.sendAll("updateMenu") in there.

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项目应该对web项目有没有单一的依赖关系。在这种情况下,您最好启动CDI 事件,而不是Web项目 @Observes

However, in "enterprise" projects, service layer code (EJB/JPA/etc) is usually separated in EJB project while web layer code (JSF/Servlets/WebSocket/etc) is kept in Web project. The EJB project should have no single dependency on web project. In that case, you'd better fire a CDI Event instead which the Web project could @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 事件;解决方法是通过 BeanManager 而不是;如果这仍然不起作用,CDI 1.1替代方案是 CDI.current()。getBeanManager()。fireEvent(new MenuChangeEvent(menu))

(note the outcomments; injecting a CDI Event is broken in both GlassFish and WildFly in current versions (4.1 / 8.2); the workaround fires the event via BeanManager instead; if this still doesn't work, the CDI 1.1 alternative is 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> 。另请参见服务器如何将异步更改推送到JSF创建的HTML页面?


Update: at 1 april 2016 (half a year after above answer), OmniFaces introduced with version 2.3 the <o:socket> which should make this all less circuitous. The upcoming JSF 2.3 <f:websocket> is largely based on <o:socket>. See also How can server push asynchronous changes to a HTML page created by JSF?

这篇关于使用JSF / Java EE从数据库进行实时更新的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆