web-dev-qa-db-ja.com

JSF / Java EEを使用したデータベースからのリアルタイム更新

次の環境で1つのアプリケーションを実行しています。

  • GlassFish Server 4.0
  • JSF 2.2.8-02
  • PrimeFaces 5.1ファイナル
  • 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は、ビュースコープです。これらはすべてアプリケーションスコープであり、アプリケーションの起動時に一度だけ読み込まれ、対応するデータベーステーブル(カテゴリ/サブカテゴリ/製品など)の何かが更新/変更された場合にのみ更新されます。

thisthis のように、WebSoketを理解しようと試みました(これまでに試したことはなく、WebSoketにはまったく新しいものです)。 GlassFish 4.0では正常に機能しましたが、データベースは関係しません。 WebSoketがどのように機能するのか、まだ適切に理解できません。特にデータベースが関係している場合。

このシナリオでは、対応するデータベーステーブルに何かが更新/削除/追加されたときに、関連付けられたクライアントに通知し、データベースの最新の値で上記のCSSメニューを更新する方法は?

簡単な例は素晴らしいでしょう。

27
Tiny

序文

この回答では、次のことを想定します。

  • _<p:Push>_の使用には興味がありません(正確な理由は途中で説明しますが、少なくとも新しいJava EE 7/JSR356 WebSocket APIの使用に興味があります。 )。
  • アプリケーションスコーププッシュ(つまり、すべてのユーザーが一度に同じプッシュメッセージを取得するため、セッションに関心がなく、スコーププッシュを表示しません)。
  • (MySQL)DB側からプッシュを直接呼び出します(したがって、エンティティリスナーを使用してJPA側からプッシュを呼び出すことに興味はありません)。 Edit:とにかく両方のステップをカバーします。ステップ3aはDBトリガーを説明し、ステップ3bはJPAトリガーを説明します。両方ではなく、どちらかを使用してください!


1. WebSocketエンドポイントを作成する

最初に _@ServerEndpoint_ クラスを作成します。このクラスは、基本的にすべてのwebsocketセッションをアプリケーション全体のセットに収集します。この特定の例では、すべてのwebsocketセッションが基本的に独自の_@ServerEndpoint_インスタンスを取得するため、これはstaticにしかならないことに注意してください(サーブレットとは異なり、ステートレスです)。

_@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 answerServletAwareConfigの例を使用できます。ご存知のように、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。 いずれかDB側からWebSocketプッシュをトリガー

ここで、DB側からPush.sendAll("updateMenu")コマンドをトリガーする必要があります。 DBがWebサービスでHTTPリクエストを実行できるようにする最も簡単な方法の1つ。単純なVanillaサーブレットは、Webサービスのように振る舞うには十分です:

_@WebServlet("/Push-update-menu")
public class PushUpdateMenu extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Push.sendAll("updateMenu");
    }

}
_

もちろん、必要に応じて、要求パラメーターまたはパス情報に基づいてプッシュメッセージをパラメーター化する機会があります。呼び出し元がこのサーブレットの呼び出しを許可されている場合は、セキュリティチェックを実行することを忘れないでください。そうしないと、DB以外の世界の誰かがそれを呼び出すことができます。たとえば、呼び出し元のIPアドレスを確認できます。これは、DBサーバーとWebサーバーの両方が同じマシンで実行されている場合に便利です。

DBがそのサーブレットでHTTPリクエストを起動できるようにするには、基本的にオペレーティングシステム固有のコマンドを呼び出してHTTP GETリクエストを実行する再利用可能なストアドプロシージャを作成する必要があります。 curl。 MySQLはOS固有のコマンドの実行をネイティブでサポートしていないため、そのためにまずユーザー定義関数(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 Pushをトリガーする

要件/状況でJPAエンティティ変更イベントのみをリッスンできる場合、DBへの外部変更はnotをカバーする必要があります。 の代わりに、ステップ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");
    }

}
_

ただし、「エンタープライズ」プロジェクトでは、通常、サービスレイヤーコード(EJB/JPA/etc)はEJBプロジェクトで分離され、Webレイヤーコード(JSF/Servlets/WebSocket/etc)はWebプロジェクトに保持されます。 EJBプロジェクトには、Webプロジェクトへの 単一なし 依存関係が必要です。その場合、代わりにWebプロジェクトが_@Observes_できるCDI Eventを実行する方がよいでしょう。

_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))

_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");
    }

}
_

Update:2016年4月1日(上記の回答から半年後)、 OmniFaces バージョン2.3で導入された _<o:socket>_ これにより、すべての回路がより少なくなります。今後のJSF 2.3 _<f:websocket>_は、主に_<o:socket>_に基づいています。 JSFによって作成されたHTMLページに非同期の変更をサーバーでプッシュする方法 も参照してください。

47
BalusC

PrimefacesとJava EE 7を使用しているので、簡単に実装できるはずです。

primefaces Pushを使用します(例はこちら http://www.primefaces.org/showcase/Push/notify.xhtml

  • Websocketエンドポイントをリッスンするビューを作成する
  • データベースの変更時にCDIイベントを生成するデータベースリスナーを作成します
    • イベントのペイロードは、最新のデータのデルタ、または単に更新情報のいずれかです
  • Websocketを介してすべてのクライアントにCDIイベントを伝播する
  • データを更新するクライアント

これが役立つことを願っています

よろしく

5
urbiwanus

PrimeFacesには、コンポーネントを自動的に更新するポーリング機能があります。次の例では、_<h:outputText>_は_<p:poll>_によって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.
    }
}   
_
1
Zaw Than oo