Mises à jour en temps réel de la base de données utilisant 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 plusieurs pages publiques qui sont paresseusement chargé à 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 de catégorie/Sous-catégorie-wise featured, top seller, new arrival etc produits.

les menus CSS sont remplis dynamiquement à partir de la base de données basée sur diverses catégories de produits dans la base de données.

Ces menus sont remplis à chaque chargement de page qui est complètement inutile. Certains de ces menus demandes de critères JPA complexes / coûteuses.

actuellement, le JSF géré haricots qui peuplent ces menus sont Vue scoped. Ils doivent tous être des applications scoped, être chargés seulement une fois sur le démarrage de l'application et être mis à jour seulement quand quelque chose dans les tableaux de base de données correspondants (catégorie/Sous-catégorie/Produit etc) est mis à jour/changé.

j'ai fait quelques tentatives pour comprendre WebSokets (jamais essayé avant, complètement nouveau à WebSokets) comme ce et ce . Ils ont bien travaillé sur GlassFish 4.0, mais pas sur les bases de données. Je ne suis toujours pas capable de comprendre comment fonctionnent les WebSokets. Surtout quand 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, quand quelque chose est mis à jour/supprimé/ajouté aux tables de base de données correspondantes?

un exemple simple serait grand.

22
demandé sur Tiny 2014-09-20 14:28:59

3 réponses

Préface

dans cette réponse, je supposerai ce qui suit:

  • vous n'êtes pas intéressé à utiliser <p:push> (je vais laisser la raison exacte au milieu, vous êtes au moins intéressé à utiliser la nouvelle API WebSocket Java EE 7 / JSR356).
  • vous voulez une application scoped push (i.e. tous les utilisateurs reçoivent le même message push à la fois; donc vous n'êtes pas intéressé par une session ni voir scoped push).
  • vous voulez invoquer push directement du côté (MySQL) DB (donc vous n'êtes pas intéressé à invoquer push du côté JPA en utilisant un écouteur d'entité). Edit : je vais couvrir les deux étapes de toute façon. L'étape 3a décrit le déclencheur de L'AD et l'étape 3b décrit le déclencheur de L'app. Utilisez-les soit-ou, pas les deux!



1. Créer un terminal WebSocket

créer D'abord une classe @ServerEndpoint qui recueille essentiellement toutes les sessions websocket dans un ensemble d'application. Notez que dans cet exemple particulier, cela ne peut être que static car chaque session websocket reçoit fondamentalement sa propre instance @ServerEndpoint (elles sont donc différentes des servlets apatrides).

@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é à tous les WebSocket ouverts sessions (c.-à-d. application scoped push). Notez que ce message peut aussi très bien être une chaîne JSON.

si vous avez l'intention de les stocker explicitement dans application scope (ou (HTTP) session scope), vous pouvez utiliser l'exemple ServletAwareConfig dans cette réponse pour cela. Vous savez, ServletContext attribue la carte à ExternalContext#getApplicationMap() dans JSF (et HttpSession attribue la carte à ExternalContext#getSessionMap() ).



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

utilisez ce morceau de JavaScript pour ouvrir un 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.
}

A partir de maintenant il ne fait que Logger le texte poussé. Nous aimerions utiliser ce texte comme instruction pour mettre à jour le composant 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 comme texte par Push.sendAll("updateMenu") , alors vous pouvez interpréter et 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 pousser à partir de DB côté

maintenant nous devons déclencher la commande Push.sendAll("updateMenu") du côté de la base de données. L'un des plus simple il permet à la DB de lancer une requête HTTP sur un service web. Un simple service à la vanille 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 la requête ou des informations de chemin, si nécessaire. N'oubliez pas d'effectuer des contrôles de sécurité si l'appelant est autorisé à invoquer ce servlet, sinon n'importe qui d'autre dans le monde autre que le DB lui-même serait en mesure de l'invoquer. Vous pouvez le vérifier l'adresse IP de l'appelant, par exemple, ce qui est pratique si le serveur DB et le serveur web fonctionnent sur la même machine.

pour permettre à la DB de lancer une requête HTTP sur ce 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 exemple curl . MySQL ne prend pas nativement en charge l'exécution d'une commande spécifique au système D'exploitation, vous devez donc installer une fonction définie par l'utilisateur (UDF) pour la première fois. À mysqludf.org vous pouvez trouver un tas dont 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 ;

maintenant vous pouvez créer insert / update / delete triggers 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 pousser de JPA côté

si votre exigence/situation permet d'écouter sur les événements de changement d'entité JPA seulement, et donc les changements externes à la base de données ne pas doivent être couverts, alors vous pouvez au lieu de déclencheurs de la base de données comme décrit à l'étape 3a aussi juste utiliser un auditeur de changement d'entité JPA. Vous pouvez l'enregistrer via @EntityListeners annotation sur la classe @Entity :

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

si vous utilisez un seul projet de profil web où tout (EJB/JPA/JSF) est regroupé dans le même projet, alors vous pouvez simplement invoquer directement Push.sendAll("updateMenu") .

public class MenuChangeListener {

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

}

cependant, dans les projets" enterprise", le code de couche 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. La EJB projet devrait avoir pas de dépendance unique projet web. Dans ce cas, vous feriez mieux de lancer un CDI Event plutôt 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 cassée à la fois dans GlassFish et WildFly dans les versions actuelles (4.1 / 8.2); la solution de contournement tire l'événement via BeanManager à la place; si cela ne fonctionne toujours pas, le CDI 1.1 alternative 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> qui devrait rendre tout cela moins difficile. Le prochain JSF 2.3 <f:websocket> est largement basé sur <o:socket> . Voir aussi comment le serveur peut-il pousser des changements asynchrones à une page HTML créée par JSF?

44
répondu BalusC 2017-05-23 12:34:51

puisque vous utilisez Primefaces et Java EE 7, Il devrait être facile à mettre en œuvre:

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

  • Créer une vue qui sont à l'écoute à un point de terminaison Websocket
  • créer un écouteur de base de données qui produit un événement CDI sur le changement de base de données
    • la charge utile de l'événement pourrait être delta de la dernière de données ou de juste et de mise à jour de l'information
  • Propager le CDI de l'événement via Websocket pour tous les clients
  • Clients mettant à jour les données

Espérons que cette aide Si vous avez besoin de plus de détails il suffit de demander

concerne

4
répondu urbiwanus 2015-02-19 03:24:58

PrimeFaces a des fonctionnalités poll pour mettre à jour le composant automatiquement. Dans l'exemple suivant, <h:outputText> sera mis à jour automatiquement 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> sera auto-mettre à jour votre menu composant.

<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.
    }
}   
0
répondu CycDemo 2015-02-22 19:22:34