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

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

问题描述

我有一个应用程序在以下环境中运行.

I have one application running in the following environment.

  • 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 菜单显示在模板页面的标题上,例如显示类别/子类别的特色、最畅销、新到货等产品.

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 完全陌生),例如 this这个.它们在 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?

一个简单的例子会很棒.

推荐答案

前言

在这个答案中,我将假设如下:

Preface

In this answer, I'll assume the following:

  • 您对使用 <p:push> 不感兴趣(我会在中间留下确切的原因,您至少对使用新的 Java EE 7/JSR356 感兴趣WebSocket API).
  • 您想要一个应用范围的推送(即所有用户同时获得相同的推送消息;因此您对会话或查看范围的推送不感兴趣).
  • 您想直接从 (MySQL) 数据库端调用推送(因此您对使用实体侦听器从 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) 会话范围)中,那么您可以使用 这个答案.您知道,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.
}

截至目前,它仅记录推送的文本.我们想使用此文本作为更新菜单组件的说明.为此,我们需要一个额外的 .

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.sendAll("updateMenu")以文本形式发送JS函数名称,那么您可以按如下方式解释和触发它:

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.


现在我们需要从DB端触发命令Push.sendAll("updateMenu").让数据库在 Web 服务上触发 HTTP 请求的最简单方法之一.一个普通的 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 地址,如果数据库服务器和网络服务器在同一台机器上运行,这会很方便.

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.

为了让 DB 在该 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 实体更改事件,因此对数据库的外部更改不需要需要涵盖,那么您可以代替 步骤 3a 中描述的 DB 触发器也仅使用 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 Event 而不是 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));
    }

}

(注意结果;注入 CDI Event 在当前版本 (4.1/8.2) 中的 GlassFish 和 WildFly 中均已损坏;解决方法通过 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 版的 应该可以做到这一点一切都不那么迂回.即将推出的 JSF 2.3 主要基于 .另请参阅服务器如何将异步更改推送到由 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天全站免登陆