websocket 使用JSF/Java EE从数据库进行真实的更新

rkttyhzu  于 2022-12-29  发布在  Java
关注(0)|答案(3)|浏览(137)

我有一个应用程序在下面的环境中运行。

  • 服务器4.0
  • JSF 2.2.8-02标准
  • PrimeFaces 5.1最终版
  • PrimeFaces扩展模块2.1.0
  • 全方位工作面1.8.1
  • 包含JPA 2.1的Eclipse链接2.5.2
  • MySQL 5.6.11
  • JDK-7 u11语言

有几个公共页面是从数据库懒洋洋地加载的。一些CSS菜单显示在模板页面的标题上,如显示类别/子类别的特色,畅销,新到达等产品。
基于数据库中的各种产品类别从数据库动态填充CSS菜单。
这些菜单在每次页面加载时填充,这是完全不必要的。其中一些菜单需要复杂/昂贵的JPA条件查询。
目前,填充这些菜单的JSF托管bean都是视图范围的,它们都应该是应用程序范围的,只在应用程序启动时加载一次,并且只在相应的数据库表(类别/子类别/产品等)中的某些内容更新/更改时才更新。
我做了一些尝试来理解WebSokets(以前从未尝试过,对WebSokets来说是全新的),比如thisthis。它们在GlassFish 4.0上运行良好,但它们不涉及数据库。我仍然无法正确理解WebSokets是如何工作的。尤其是涉及数据库的时候。
在这种情况下,如何通知相关联的客户端,并更新上述CSS菜单与最新的值从数据库,当一些更新/删除/添加到相应的数据库表?
一个简单的例子/s会很棒。

pgpifvop

pgpifvop1#

前言

在回答这个问题时,我会假设以下几点:

  • 您对使用<p:push>不感兴趣(我将把确切的原因放在中间,您至少对使用新的Java EE 7 /JSR 356 WebSocket API感兴趣)。
  • 您需要一个应用程序范围的推送(即所有用户同时获得相同的推送消息;因此您对会话不感兴趣,也不查看范围化推送)。
  • 您希望直接从(MySQL)DB端调用push(因此您对使用实体监听器从JPA端调用push不感兴趣)。Edit:无论如何,我将介绍这两个步骤。步骤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)会话范围)中,那么您可以使用此答案中的ServletAwareConfig示例。您知道,ServletContext属性Map到JSF中的ExternalContext#getApplicationMap()(而HttpSession属性Map到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")。最简单的方法之一是让数据库在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,请不要忘记执行安全检查,否则除了DB本身之外,世界上的任何其他人都可以调用它。您可以检查调用者的IP地址,例如,如果DB服务器和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实体更改事件,因此需要覆盖DB的外部更改,那么您可以而不是步骤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/Servlet/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;
    }

}

然后在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?

bq9c1y66

bq9c1y662#

由于您使用的是Primefaces和Java EE 7,因此应该很容易实现:
使用质数推进(此处示例为http://www.primefaces.org/showcase/push/notify.xhtml

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

希望这能有所帮助,如果你需要更多的细节,请询问
问候

fnvucqvd

fnvucqvd3#

PrimeFaces具有自动更新组件的轮询功能。在以下示例中,<h:outputText>将每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.
    }
}

相关问题