Actualizaciones en tiempo real desde la base de datos usando JSF / Java EE

Tengo una aplicación ejecutándose en el siguiente entorno.

  • GlassFish Server 4.0
  • JSF 2.2.8-02
  • PrimeFaces 5.1 final
  • Extensión PrimeFaces 2.1.0
  • OmniFaces 1.8.1
  • EclipseLink 2.5.2 que tiene JPA 2.1
  • MySQL 5.6.11
  • JDK-7u11

Hay varias páginas públicas que se cargan de forma lenta desde la base de datos. Algunos menús de CSS se muestran en el encabezado de la página de la plantilla, como mostrar los productos destacados de categoría / subcategoría, más vendidos, nuevos productos de llegada, etc.

Los menús CSS se rellenan dinámicamente de la base de datos en función de varias categorías de productos en la base de datos.

Estos menús se llenan en cada carga de página que es completamente innecesaria. Algunos de estos menús requieren costosas / complejas consultas de criterios de JPA.

Actualmente, los beans gestionados de JSF que pueblan estos menús tienen una vista de ámbito. Todos deben tener un ámbito de aplicación, ser cargados solo una vez en el inicio de la aplicación y actualizarse solo cuando se actualice / cambie algo en las tablas de la base de datos (categoría / subcategoría / producto, etc.) correspondientes.

Hice algunos bashs para comprender WebSokets (nunca antes probado, completamente nuevo en WebSokets) como este y este . Trabajaron bien en GlassFish 4.0 pero no involucran bases de datos. Todavía no puedo entender correctamente cómo funciona WebSokets. Especialmente cuando la base de datos está involucrada.

En este escenario, ¿cómo notificar a los clientes asociados y actualizar los menús CSS mencionados anteriormente con los últimos valores de la base de datos, cuando algo se actualiza / elimina / agrega a las tablas de la base de datos correspondientes?

Un ejemplo / s simple sería genial.

Prefacio

En esta respuesta, asumiré lo siguiente:

  • No está interesado en usar (dejaré el motivo exacto en el medio, al menos está interesado en usar la nueva API Java EE 7 / JSR356 WebSocket).
  • Desea un empuje de ámbito de la aplicación (es decir, todos los usuarios reciben el mismo mensaje de inserción a la vez, por lo tanto, no está interesado en una sesión ni en la inserción de scope).
  • Desea invocar push directamente desde el lado de la base (MySQL) (por lo tanto, no está interesado en invocar la inserción desde el lado JPA utilizando un detector de entidad). Editar : cubriré ambos pasos de todos modos. El paso 3a describe el disparador DB y el paso 3b describe el activador JPA. Úsalos-o, ¡no ambos!

1. Crear un punto final WebSocket

Primero crea una clase @ServerEndpoint que básicamente recostack todas las sesiones de websocket en un conjunto amplio de aplicaciones. Tenga en cuenta que esto en este ejemplo particular solo puede ser static ya que cada sesión de websocket básicamente obtiene su propia instancia de @ServerEndpoint (son a diferencia de los servlets sin estado).

 @ServerEndpoint("/push") public class Push { private static final Set 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); } } } } } 

El ejemplo anterior tiene un método adicional sendAll() que envía el mensaje dado a todas las sesiones abiertas de websocket (es decir, aplicación con scope push). Tenga en cuenta que este mensaje también puede ser una cadena JSON.

Si tiene la intención de almacenarlos explícitamente en el scope de la aplicación (o scope de la sesión (HTTP)), entonces puede usar el ejemplo de ServletAwareConfig en esta respuesta para eso. Ya sabes, ServletContext atribuye el mapa a ExternalContext#getApplicationMap() en JSF (y los atributos HttpSession asignan a ExternalContext#getSessionMap() ).

2. Abra el WebSocket en el lado del cliente y escúchelo

Utilice este fragmento de JavaScript para abrir un websocket y escucharlo:

 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 ahora, simplemente registra el texto enviado. Nos gustaría utilizar este texto como una instrucción para actualizar el componente del menú. Para eso, necesitaríamos un .

    

Imagine que está enviando un nombre de función JS como texto mediante Push.sendAll("updateMenu") , luego podría interpretarlo y activarlo de la siguiente manera:

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

Nuevamente, cuando se utiliza una cadena JSON como mensaje (que se puede analizar por $.parseJSON(event.data) ), es posible más dinámica.

3a. O active la inserción de WebSocket desde el lado de DB

Ahora necesitamos activar el comando Push.sendAll("updateMenu") desde el lado DB. Una de las formas más sencillas es permitir que el DB active una solicitud HTTP en un servicio web. Un servlet simple de vainilla es más que suficiente para actuar como un servicio web:

 @WebServlet("/push-update-menu") public class PushUpdateMenu extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Push.sendAll("updateMenu"); } } 

Por supuesto, tiene la oportunidad de parametrizar el mensaje push según los parámetros de solicitud o la información de ruta, si es necesario. No olvide realizar comprobaciones de seguridad si la persona que llama puede invocar este servlet, de lo contrario, cualquier persona en el mundo que no sea el propio DB podría invocarlo. Puede verificar la dirección IP de la persona que llama, por ejemplo, que es útil si el servidor de base de datos y el servidor web se ejecutan en la misma máquina.

Para que la base de datos active una solicitud HTTP en ese servlet, debe crear un procedimiento almacenado reutilizable que invoque básicamente el comando específico del sistema operativo para ejecutar una solicitud HTTP GET, por ejemplo, curl . MySQL no admite de forma nativa la ejecución de un comando específico del sistema operativo, por lo que deberá instalar una función definida por el usuario (UDF) para ese primero. En mysqludf.org puede encontrar muchos de los cuales SYS es de nuestro interés. Contiene la función sys_exec() que necesitamos. Una vez que lo haya instalado, cree el siguiente procedimiento almacenado en MySQL:

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

Ahora puede crear activadores de inserción / actualización / eliminación que lo invocarán (suponiendo que el nombre de la tabla se denomine 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. O desencadenar empuje WebSocket desde el lado JPA

Si su requerimiento / situación permite escuchar eventos de cambio de entidad JPA solamente, y por lo tanto no es necesario cubrir los cambios externos a la DB, entonces puede en lugar de activadores DB como se describe en el paso 3a usar simplemente un oyente de cambio de entidad JPA. Puede registrarlo a través de la anotación @EntityListeners en la clase @Entity :

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

Si usa un único proyecto de perfil web en el que todo (EJB / JPA / JSF) se junta en el mismo proyecto, puede invocar directamente Push.sendAll("updateMenu") allí.

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

Sin embargo, en los proyectos “empresariales”, el código de capa de servicio (EJB / JPA / etc) suele estar separado en el proyecto EJB mientras que el código de capa web (JSF / Servlets / WebSocket / etc) se mantiene en el proyecto web. El proyecto EJB no debería tener una sola dependencia en el proyecto web. En ese caso, será mejor que @Observes un Event CDI que el proyecto web podría @Observes .

 public class MenuChangeListener { // Outcommented because it's broken in current GF/WF versions. // @Inject // private Event 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)); } } 

(tenga en cuenta las observaciones: la inserción de un Event CDI está rota tanto en GlassFish como en WildFly en las versiones actuales (4.1 / 8.2); la solución BeanManager el evento a través de BeanManager ; si esto aún no funciona, la alternativa de CDI 1.1 es 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; } } 

Y luego en el proyecto web:

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

Actualización : el 1 de abril de 2016 (medio año después de la respuesta anterior), OmniFaces presentó con la versión 2.3 el que debería hacer que todo esto sea menos tortuoso. El próximo JSF 2.3 se basa en gran medida en . Consulte también ¿Cómo puede el servidor enviar cambios asíncronos a una página HTML creada por JSF?

Dado que está utilizando Primefaces y Java EE 7, debería ser fácil de implementar:

use Primefaces Push (ejemplo aquí http://www.primefaces.org/showcase/push/notify.xhtml )

  • Crear una vista que escuche un punto final de Websocket
  • Crear un oyente de base de datos que produce un evento CDI en el cambio de base de datos
    • La carga útil del evento podría ser el delta de los datos más recientes o simplemente actualizar la información
  • Propague el evento CDI a través de Websocket a todos los clientes
  • Clientes actualizando los datos

Espero que esto ayude Si necesita más detalles solo pregunte

Saludos

PrimeFaces tiene funciones de sondeo para actualizar el componente automáticamente. En el siguiente ejemplo, se actualizará automáticamente cada 3 segundos por .

¿Cómo notificar a los clientes asociados y actualizar los menús CSS mencionados anteriormente con los últimos valores de la base de datos?

Cree un método de escucha como process() para seleccionar los datos de su menú. se actualizará automáticamente su componente de menú.

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