web-dev-qa-db-fra.com

Mises à jour en temps réel de la base de données à l'aide de JSF / Java EE

J'ai une application en cours d'exécution dans l'environnement suivant.

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

Il existe plusieurs pages publiques qui sont paresseusement chargées à partir de la base de données. Quelques menus CSS sont affichés sur l'en-tête de la page de modèle, comme l'affichage des produits présentés par catégorie/sous-catégorie, les meilleurs vendeurs, les nouveaux arrivages, etc.

Les menus CSS sont remplis dynamiquement à partir de la base de données en fonction de différentes catégories de produits dans la base de données.

Ces menus sont remplis à chaque chargement de page, ce qui est totalement inutile. Certains de ces menus nécessitent des requêtes de critères JPA complexes/coûteuses.

Actuellement, les beans gérés JSF qui remplissent ces menus ont une portée limitée. Ils doivent tous avoir une portée d'application, être chargés une seule fois au démarrage de l'application et être mis à jour uniquement lorsque quelque chose dans les tables de base de données correspondantes (catégorie/sous-catégorie/produit, etc.) est mis à jour/modifié.

J'ai fait quelques tentatives pour comprendre les WebSokets (jamais essayés auparavant, complètement nouveaux pour les WebSokets) comme this et this . Ils ont bien fonctionné sur GlassFish 4.0 mais ils n'impliquent pas de bases de données. Je ne parviens toujours pas à comprendre correctement le fonctionnement des WebSokets. Surtout lorsque la base de données est impliquée.

Dans ce scénario, comment notifier les clients associés et mettre à jour les menus CSS mentionnés ci-dessus avec les dernières valeurs de la base de données, lorsqu'un élément est mis à jour/supprimé/ajouté aux tables de base de données correspondantes?

Un exemple simple/s serait formidable.

27
Tiny

Préface

Dans cette réponse, je suppose que:

  • Vous n'êtes pas intéressé à utiliser <p:Push> (Je vais laisser la raison exacte au milieu, vous êtes au moins intéressé à utiliser le nouveau Java EE 7/JSR356 WebSocket API).
  • Vous voulez une application à portée étendue (c'est-à-dire que tous les utilisateurs reçoivent le même message Push à la fois; vous n'êtes donc pas intéressé par une session ni à afficher la portée étendue).
  • Vous voulez invoquer Push directement du côté de la base de données (MySQL) (vous n'êtes donc pas intéressé à invoquer Push du côté JPA à l'aide d'un écouteur d'entité). Edit : Je couvrirai les deux étapes de toute façon. L'étape 3a décrit le déclencheur DB et l'étape 3b décrit le déclencheur JPA. Utilisez-les, ou pas les deux!


1. Créez un point de terminaison WebSocket

Créez d'abord une classe @ServerEndpoint qui recueille essentiellement toutes les sessions websocket dans un ensemble étendu d'application. Notez que cela ne peut être dans cet exemple particulier que static car chaque session websocket obtient fondamentalement sa propre instance @ServerEndpoint (Elles sont différentes des servlets donc sans état).

@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);
                }
            }
        }
    }

}

L'exemple ci-dessus a une méthode supplémentaire sendAll() qui envoie le message donné à toutes les sessions websocket ouvertes (c'est-à-dire à portée d'application Push). Notez que ce message peut également être une chaîne JSON.

Si vous avez l'intention de les stocker explicitement dans la portée d'application (ou la portée de session (HTTP)), vous pouvez utiliser l'exemple ServletAwareConfig dans cette réponse pour cela. Vous savez, les attributs ServletContext sont mappés à ExternalContext#getApplicationMap() dans JSF (et les attributs HttpSession sont mappés à ExternalContext#getSessionMap()).


2. Ouvrez le WebSocket côté client et écoutez-le

Utilisez ce morceau de JavaScript pour ouvrir une websocket et écouter dessus:

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.
}

Pour l'instant, il enregistre simplement le texte poussé. Nous aimerions utiliser ce texte comme une instruction pour mettre à jour le composant de menu. Pour cela, nous aurions besoin d'un <p:remoteCommand> Supplémentaire.

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

Imaginez que vous envoyez un nom de fonction JS sous forme de texte par Push.sendAll("updateMenu"), vous pouvez alors l'interpréter et le déclencher comme suit:

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

Encore une fois, lorsque vous utilisez une chaîne JSON comme message (que vous pouvez analyser par $.parseJSON(event.data)), plus de dynamique est possible.


3a. Soit déclencheur WebSocket Push du côté DB

Maintenant, nous devons déclencher la commande Push.sendAll("updateMenu") du côté DB. L'un des moyens les plus simples permettant à la base de données de déclencher une requête HTTP sur un service Web. Un simple servlet Vanilla est plus que suffisant pour agir comme un service Web:

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

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

}

Vous avez bien sûr la possibilité de paramétrer le message Push en fonction des paramètres de demande ou des informations de chemin, si nécessaire. N'oubliez pas d'effectuer des vérifications de sécurité si l'appelant est autorisé à invoquer ce servlet, sinon toute autre personne dans le monde autre que la base de données elle-même pourrait l'invoquer. Vous pouvez vérifier l'adresse IP de l'appelant, par exemple, ce qui est pratique si le serveur DB et le serveur Web s'exécutent sur la même machine.

Afin de permettre à la base de données de lancer une requête HTTP sur cette servlet, vous devez créer une procédure stockée réutilisable qui invoque essentiellement la commande spécifique au système d'exploitation pour exécuter une requête HTTP GET, par ex. curl. MySQL ne prend pas en charge nativement l'exécution d'une commande spécifique au système d'exploitation, vous devez donc d'abord installer une fonction définie par l'utilisateur (UDF). Sur mysqludf.org vous pouvez trouver un tas de ce qui SYS est de notre intérêt. Il contient la fonction sys_exec() dont nous avons besoin. Une fois installé, créez la procédure stockée suivante dans MySQL:

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

Vous pouvez maintenant créer des déclencheurs d'insertion/mise à jour/suppression qui l'invoqueront (en supposant que le nom de la table est nommé 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. Ou déclencheur WebSocket Push du côté JPA

Si votre exigence/situation permet d'écouter uniquement les événements de changement d'entité JPA, et donc les modifications externes à la base de données n'ont pas besoin d'être couvertes, alors vous pouvez au lieu de les déclencheurs DB comme décrit à l'étape 3a utilisent également simplement un écouteur de changement d'entité JPA. Vous pouvez l'enregistrer via l'annotation @EntityListeners Sur la classe @Entity:

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

S'il vous arrive d'utiliser un seul projet de profil Web dans lequel tout (EJB/JPA/JSF) est regroupé dans le même projet, vous pouvez simplement invoquer directement Push.sendAll("updateMenu") dedans.

public class MenuChangeListener {

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

}

Cependant, dans les projets "entreprise", le code de couche de service (EJB/JPA/etc) est généralement séparé dans le projet EJB tandis que le code de couche Web (JSF/Servlets/WebSocket/etc) est conservé dans le projet Web. Le projet EJB devrait avoir pas un seul dépendance au projet web. Dans ce cas, vous feriez mieux de lancer un CDI Event à la place que le projet Web pourrait @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));
    }

}

(notez les résultats; l'injection d'un CDI Event est rompue dans GlassFish et WildFly dans les versions actuelles (4.1/8.2); la solution de contournement déclenche l'événement via BeanManager à la place; si cela ne fonctionne toujours pas, l'alternative CDI 1.1 est 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;
    }

}

Et puis dans le projet web:

@ApplicationScoped
public class Application {

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

}

Mise à jour : au 1er avril 2016 (six mois après la réponse ci-dessus), OmniFaces introduit avec la version 2.3 le <o:socket> ce qui devrait rendre tout cela moins détourné. Le prochain JSF 2.3 <f:websocket> Est largement basé sur <o:socket>. Voir aussi Comment le serveur peut-il pousser des modifications asynchrones sur une page HTML créée par JSF?

47
BalusC

Puisque vous utilisez Primefaces et Java EE 7, il devrait être facile à implémenter:

utilisez Primefaces Push (exemple ici http://www.primefaces.org/showcase/Push/notify.xhtml )

  • Créer une vue qui écoute un point de terminaison Websocket
  • Créer un écouteur de base de données qui produit un événement CDI lors d'un changement de base de données
    • La charge utile de l'événement pourrait être le delta des dernières données ou simplement et mettre à jour les informations
  • Propager l'événement CDI via Websocket à tous les clients
  • Clients mettant à jour les données

J'espère que cela vous aide Si vous avez besoin de plus de détails, demandez

Cordialement

5
urbiwanus

PrimeFaces possède des fonctions d'interrogation pour mettre à jour le composant automatiquement. Dans l'exemple suivant, <h:outputText> Sera automatiquement mis à jour toutes les 3 secondes par <p:poll>.

Comment notifier les clients associés et mettre à jour les menus CSS mentionnés ci-dessus avec les dernières valeurs de la base de données?

Créez une méthode d'écoute comme process() pour sélectionner vos données de menu. <p:poll> Mettra automatiquement à jour votre composant de menu.

<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