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

Tin*_*iny 29 jsf java-ee websocket primefaces java-ee-7

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

  • GlassFish Server 4.0
  • JSF 2.2.8-02
  • PrimeFaces 5.1决赛
  • PrimeFaces Extension 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来说是全新的),就像这样这个.他们在GlassFish 4.0上运行良好,但它们不涉及数据库.我仍然无法正确理解WebSokets的工作方式.特别是涉及数据库时.

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

一个简单的例子很棒.

Bal*_*usC 47

前言

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

  • 你对使用不感兴趣<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);
                }
            }
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

上面的示例有一个额外的方法sendAll(),它将给定的消息发送到所有打开的websocket会话(即应用程序作用域推送).请注意,此消息也可以是一个JSON字符串.

如果您打算将它们显式存储在应用程序范围(或(HTTP)会话范围)中,那么您可以使用此答案中ServletAwareConfig示例.你知道,属性映射到JSF(和属性映射到).ServletContextExternalContext#getApplicationMap()HttpSessionExternalContext#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.
}
Run Code Online (Sandbox Code Playgroud)

截至目前,它只记录推送的文本.我们想将此文本用作更新菜单组件的指令.为此,我们需要额外的<p:remoteCommand>.

<h:form>
    <p:remoteCommand name="updateMenu" update=":menu" />
</h:form>
Run Code Online (Sandbox Code Playgroud)

想象一下,您将JS函数名称作为文本发送Push.sendAll("updateMenu"),然后您可以解释并触发它,如下所示:

    ws.onmessage = function(event) {
        var functionName = event.data;
        if (window[functionName]) {
            window[functionName]();
        }
    };
Run Code Online (Sandbox Code Playgroud)

同样,当使用JSON字符串作为消息(您可以解析它$.parseJSON(event.data))时,可以实现更多动态.


3A.无论是从DB侧触发的WebSocket推

现在我们需要Push.sendAll("updateMenu")从DB端触发命令.允许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");
    }

}
Run Code Online (Sandbox Code Playgroud)

您当然有机会根据请求参数或路径信息参数化推送消息,如有必要.如果允许调用者调用此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 ;
Run Code Online (Sandbox Code Playgroud)

现在您可以创建将调用它的插入/更新/删除触发器(假设表名已命名menu):

CREATE TRIGGER after_menu_insert
AFTER INSERT ON menu
FOR EACH ROW CALL menu_push();
Run Code Online (Sandbox Code Playgroud)
CREATE TRIGGER after_menu_update
AFTER UPDATE ON menu
FOR EACH ROW CALL menu_push();
Run Code Online (Sandbox Code Playgroud)
CREATE TRIGGER after_menu_delete
AFTER DELETE ON menu
FOR EACH ROW CALL menu_push();
Run Code Online (Sandbox Code Playgroud)


3B.或者从JPA端触发WebSocket推送

如果您的要求/情况允许监听JPA实体变化的事件而已,从而给DB外部变化并没有需要覆盖,那么你可以,而不是如在步骤3a中描述的也只是使用JPA实体更改侦听数据库触发器.您可以通过类中的@EntityListeners注释注册它@Entity:

@Entity
@EntityListeners(MenuChangeListener.class)
public class Menu {
    // ...
}
Run Code Online (Sandbox Code Playgroud)

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

public class MenuChangeListener {

    @PostPersist
    @PostUpdate
    @PostRemove
    public void onChange(Menu menu) {
        Push.sendAll("updateMenu");
    }

}
Run Code Online (Sandbox Code Playgroud)

但是,在"企业"项目中,服务层代码(EJB/JPA/etc)通常在EJB项目中分离,而Web层代码(JSF/Servlets/WebSocket/etc)保存在Web项目中.EJB项目应该没有对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));
    }

}
Run Code Online (Sandbox Code Playgroud)

(注意注释; Event在当前版本(4.1/8.2)中,GlassFish和WildFly都注册了CDI ;解决方法是通过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;
    }

}
Run Code Online (Sandbox Code Playgroud)

然后在Web项目中:

@ApplicationScoped
public class Application {

    public void onMenuChange(@Observes MenuChangeEvent event) {
        Push.sendAll("updateMenu");
    }

}
Run Code Online (Sandbox Code Playgroud)

更新:2016年4月1日(上述答案后半年),OmniFaces推出了版本2.3,<o:socket>这应该使这一切不那么迂回.即将推出的JSF 2.3 <f:websocket>主要基于<o:socket>.另请参见服务器如何将异步更改推送到JSF创建的HTML页面?

  • 我扩展了步骤3b的答案,以防用户只对JPA实体监听器感兴趣. (3认同)

urb*_*nus 6

由于您使用的是Primefaces和Java EE 7,因此应该易于实现:

使用Primefaces Push(此处示例http://www.primefaces.org/showcase/push/notify.xhtml

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

希望这对您有帮助如果您需要更多详细信息,请询问

问候