Atualizações em tempo real do database usando JSF / Java EE

Eu tenho um aplicativo em execução no seguinte ambiente.

  • GlassFish Server 4.0
  • JSF 2.2.8-02
  • PrimeFaces 5.1 final
  • Extensão PrimeFaces 2.1.0
  • OmniFaces 1.8.1
  • EclipseLink 2.5.2 tendo o JPA 2.1
  • MySQL 5.6.11
  • JDK-7u11

Existem várias páginas públicas que são carregadas com preguiça a partir do database. Alguns menus CSS são exibidos no header da página do modelo, como exibir categoria / subcategoria, destaque, produtos novos, etc.

Os menus CSS são preenchidos dinamicamente a partir do database com base em várias categorias de produtos no database.

Esses menus são preenchidos em cada carregamento de página, o que é completamente desnecessário. Alguns desses menus exigem consultas de critérios JPA complexas / caras.

Atualmente, os beans gerenciados pelo JSF que preenchem esses menus são vistos no escopo. Todos devem ter escopo de aplicativo, ser carregados apenas uma vez no início do aplicativo e ser atualizados apenas quando algo nas tabelas de database correspondentes (categoria / subcategoria / produto, etc.) for atualizado / alterado.

Eu fiz algumas tentativas para entender WebSokets (nunca tentei antes, completamente novo para WebSokets) como isto e isto . Eles funcionaram bem no GlassFish 4.0, mas não envolvem bancos de dados. Ainda não consigo entender corretamente como o WebSokets funciona. Especialmente quando o database está envolvido.

Neste cenário, como notificar os clientes associados e atualizar os menus CSS mencionados acima com os valores mais recentes do database, quando algo é atualizado / excluído / adicionado às tabelas de database correspondentes?

Um exemplo simples seria ótimo.

   

Prefácio

Nesta resposta, assumirei o seguinte:

  • Você não está interessado em usar (deixarei o motivo exato no meio, você está, pelo menos, interessado em usar a nova API WebSocket Java EE 7 / JSR356).
  • Você quer um push de escopo de aplicativo (ou seja, todos os usuários recebem a mesma mensagem de envio de uma só vez; portanto, você não está interessado em uma session nem visualiza o escopo definido).
  • Você deseja chamar push diretamente do lado do database (MySQL) (portanto, não está interessado em chamar o push do lado da JPA usando um listener de entidade). Edit : vou cobrir os dois passos de qualquer maneira. A etapa 3a descreve o gatilho do BD e a etapa 3b descreve o gatilho do JPA. Use-os ou, não ambos!

1. Criar um terminal WebSocket

Primeiro crie uma class @ServerEndpoint que basicamente coleta todas as sessões websocket em um conjunto de aplicativos. Note que neste exemplo em particular só pode ser static já que toda session websocket basicamente obtém sua própria instância @ServerEndpoint (eles são diferentes de servlets, portanto, sem 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); } } } } } 

O exemplo acima tem um método adicional sendAll() que envia a mensagem dada para todas as sessões websocket abertas (ou seja, push com escopo de aplicativo). Note que esta mensagem também pode ser uma string JSON.

Se você pretende armazená-los explicitamente no escopo do aplicativo (ou no escopo da session (HTTP)), use o exemplo ServletAwareConfig nessa resposta para isso. Você sabe, os atributos do ServletContext mapeiam para ExternalContext#getApplicationMap() no JSF (e os atributos HttpSession mapeiam para ExternalContext#getSessionMap() ).

2. Abra o WebSocket no lado do cliente e ouça nele

Use este pedaço de JavaScript para abrir um websocket e ouvi-lo:

 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 agora, ele registra apenas o texto enviado. Gostaríamos de usar este texto como uma instrução para atualizar o componente do menu. Para isso, precisaríamos de um .

    

Imagine que você está enviando um nome de function JS como texto por Push.sendAll("updateMenu") , então você poderia interpretá-lo e acioná-lo da seguinte maneira:

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

Novamente, ao usar uma string JSON como mensagem (que você poderia analisar por $.parseJSON(event.data) ), mais dinâmicas são possíveis.

3a. Ou acionar o envio de WebSocket do lado do database

Agora precisamos triggersr o comando Push.sendAll("updateMenu") do lado do database. Uma das maneiras mais simples de permitir que o DB envie uma solicitação HTTP em um serviço da web. Um servlet simples é mais que suficiente para agir como um serviço da web:

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

É claro que você tem a oportunidade de parametrizar a mensagem push com base em parâmetros de solicitação ou informações de caminho, se necessário. Não se esqueça de executar verificações de segurança se o chamador tiver permissão para invocar este servlet, caso contrário, qualquer outra pessoa no mundo que não seja o próprio DB poderá invocá-lo. Você pode verificar o endereço IP do chamador, por exemplo, o que é útil se o servidor de database e o servidor da Web forem executados na mesma máquina.

Para permitir que o DB dispare uma solicitação HTTP nesse servlet, é necessário criar um procedimento armazenado reutilizável que basicamente chame o comando específico do sistema operacional para executar uma solicitação HTTP GET, por exemplo, curl . O MySQL não suporta nativamente a execução de um comando específico do sistema operacional, então você precisa instalar uma function definida pelo usuário (UDF) para isso primeiro. No mysqludf.org você pode encontrar um monte de SYS que é do nosso interesse. Ele contém a function sys_exec() que precisamos. Uma vez instalado, crie o seguinte procedimento armazenado no MySQL:

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

Agora você pode criar gatilhos insert / update / delete que irão invocá-lo (assumindo que table name é nomeado 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 acionar o envio de WebSocket pelo lado da JPA

Se sua necessidade / situação permitir escutar somente events de mudança de entidade JPA e, portanto, não for necessário cobrir alterações externas no BD, você poderá, em vez dos acionadores de BD, conforme descrito na etapa 3a, usar apenas um listener de alteração de entidade JPA. Você pode registrá-lo via anotação @EntityListeners na class @Entity :

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

Se acontecer de você usar um único projeto de perfil da web em que tudo (EJB / JPA / JSF) é lançado no mesmo projeto, você pode simplesmente invocar Push.sendAll("updateMenu") lá.

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

No entanto, em projetos “empresariais”, o código da camada de serviço (EJB / JPA / etc) é geralmente separado no projeto EJB enquanto o código da camada da Web (JSF / Servlets / WebSocket / etc) é mantido no projeto da Web. O projeto EJB não deve ter uma dependência única no projeto da web. Nesse caso, é melhor você triggersr um Event CDI em vez do qual o projeto da Web poderia @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)); } } 

(observe os outcomments; injetar um Event CDI é quebrado no GlassFish e no WildFly nas versões atuais (4.1 / 8.2); a solução alternativa triggers o evento via BeanManager ; se isso ainda não funcionar, a alternativa do 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; } } 

E então no projeto web:

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

Atualização : em 1 de abril de 2016 (meio ano após a resposta acima), o OmniFaces introduziu com a versão 2.3 o que deve tornar tudo menos circular. O próximo JSF 2.3 é largamente baseado em . Veja também Como o servidor pode enviar mudanças assíncronas para uma página HTML criada pelo JSF?

Como você está usando o Primefaces e o Java EE 7, ele deve ser fácil de implementar:

use Primefaces Push (exemplo aqui http://www.primefaces.org/showcase/push/notify.xhtml )

  • Criar uma visualização que ouça um nó de extremidade do Websocket
  • Criar um ouvinte do database que produza um evento CDI na alteração do database
    • A carga útil do evento pode ser o delta dos dados mais recentes ou apenas atualizar informações
  • Propagar o evento CDI via Websocket para todos os clientes
  • Clientes atualizando os dados

Espero que isso ajude Se você precisar de mais alguns detalhes é só pedir

Saudações

PrimeFaces tem resources de pesquisa para atualizar o componente automaticamente. No exemplo a seguir, será atualizado automaticamente a cada 3 segundos por .

Como notificar os clientes associados e atualizar os menus CSS mencionados acima com os valores mais recentes do database?

Crie um método de ouvinte como process() para selecionar seus dados de menu. atualizará automaticamente seu componente de menu.

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