Подтвердить что ты не робот

Обновления в реальном времени из базы данных с использованием JSF/Java EE

У меня есть одно приложение, работающее в следующей среде.

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

Существует несколько общедоступных страниц, которые лениво загружаются из базы данных. Несколько CSS-меню отображаются в заголовке страницы шаблона, например, отображаются категории/подкатегории, популярные продавцы, новые продукты прибытия и т.д.

Меню CSS динамически заполняется из базы данных на основе различных категорий продуктов в базе данных.

Эти меню заполняются при каждой загрузке страницы, что совершенно не нужно. Некоторые из этих меню требуют сложных/дорогостоящих запросов по критериям JPA.

В настоящее время управляемый JSF beans, который заполняет эти меню, представлен в виде области. Все они должны быть областью применения, загружаться только один раз при запуске приложения и обновляться только тогда, когда обновляется/изменяется что-то в соответствующих таблицах базы данных (категория/подкатегория/продукт и т.д.).

Я сделал некоторые попытки понять WebSokets (никогда не пробовал раньше, совершенно новый для WebSokets), например this и this. Они отлично работали на GlassFish 4.0, но они не связаны с базами данных. Я все еще не могу правильно понять, как работают WebSokets. Особенно, когда используется база данных.

В этом случае, как уведомлять связанные клиенты и обновлять вышеупомянутые меню CSS с последними значениями из базы данных, когда что-то обновляется/удаляется/добавляется в соответствующие таблицы базы данных?

Простой пример /s был бы большим.

4b9b3361

Ответ 1

Введение

В этом ответе я буду предполагать следующее:

  • Вы не заинтересованы в использовании <p:push> (я оставлю точную причину посередине, вы, по крайней мере, заинтересованы в использовании нового API Java EE 7/JSR356 WebSocket).
  • Вы хотите, чтобы приложение охватывало push (т.е. все пользователи получают одно и то же push-сообщение одновременно, поэтому вы не заинтересованы в сеансе или не просматриваете скользящий push).
  • Вы хотите вызвать push непосредственно из (DB) БД (таким образом, вам не интересно вызывать push из стороны JPA с помощью прослушивателя сущностей). Изменить. Я все равно покрою оба шага. Шаг 3a описывает триггер DB, а шаг 3b описывает триггер JPA. Используйте их либо - или, не оба!


1. Создание конечной точки WebSocket

Сначала создайте класс @ServerEndpoint, который в основном собирает все сеансы websocket в набор приложений. Обратите внимание, что в этом конкретном примере это может быть только static, поскольку каждый сеанс websocket в основном получает свой собственный экземпляр @ServerEndpoint (они в отличие от сервлетов, таким образом, без гражданства).

@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)), то вы можете использовать пример ServletAwareConfig в этом ответе для этого, Знаете, атрибуты ServletContext сопоставляются с ExternalContext#getApplicationMap() в JSF (и HttpSession атрибуты отображаются на ExternalContext#getSessionMap()).


2. Откройте WebSocket на стороне клиента и прослушайте его

Используйте этот фрагмент JavaScript, чтобы открыть веб-узел и прослушать его:

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>

Представьте, что вы отправляете имя функции JS в виде текста Push.sendAll("updateMenu"), тогда вы можете интерпретировать и запускать его следующим образом:

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

Опять же, при использовании строки JSON в качестве сообщения (которое вы могли бы проанализировать с помощью $.parseJSON(event.data)), возможно увеличение динамики.


3a. Любой из них вызывает запуск WebSocket с стороны БД

Теперь нам нужно вызвать команду Push.sendAll("updateMenu") со стороны БД. Один из самых простых способов, позволяющий БД запускать HTTP-запрос на веб-службу. Обычный ванильный сервлет более чем достаточен, чтобы действовать как веб-сервис:

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

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

}

У вас, конечно, есть возможность параметризовать push-сообщение на основе параметров запроса или информации о пути, если это необходимо. Не забудьте выполнить проверки безопасности, если вызывающему абоненту разрешено вызывать этот сервлет, иначе кто-либо другой в мире, кроме самого БД, сможет его вызвать. Например, вы можете проверить IP-адрес вызывающего абонента, что удобно, если оба сервера БД и веб-сервер работают на одном компьютере.

Чтобы позволить БД запускать HTTP-запрос на этот сервлет, вам необходимо создать многоразовую хранимую процедуру, которая в основном вызывает конкретную команду операционной системы для выполнения запроса HTTP GET, например. curl. MySQL не поддерживает поддержку конкретной команды ОС, поэтому вам нужно будет сначала установить определенную пользователем функцию (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 ;

Теперь вы можете создавать триггеры insert/update/delete, которые будут вызывать его (если имя таблицы называется 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. Или запускать WebSocket нажимать со стороны JPA

Если ваше требование/ситуация позволяет прослушивать только события изменения сущности JPA, и поэтому внешние изменения в БД должны быть покрыты не, тогда вы можете вместо Триггеры DB, как описано в шаге 3a, также просто используют прослушиватель изменений сущности JPA. Вы можете зарегистрировать его через @EntityListeners аннотацию в классе @Entity:

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

Если вы используете один проект веб-профиля, в котором все (EJB/JPA/JSF) сбрасывается вместе в одном проекте, вы можете просто напрямую вызвать Push.sendAll("updateMenu") там.

public class MenuChangeListener {

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

}

Однако в проектах "enterprise" код уровня обслуживания (EJB/JPA/etc) обычно разделяется в проекте EJB, в то время как код веб-слоя (JSF/Servlets/WebSocket/etc) хранится в веб-проекте. Проект EJB должен иметь никакой отдельной зависимости от веб-проекта. В этом случае вам лучше запустить CDI Event, вместо которого веб-проект может @Observes.

public class MenuChangeListener {

    // Outcommented because it 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 broken in current GF/WF versions.
        // event.fire(new MenuChangeEvent(menu));

        beanManager.fireEvent(new MenuChangeEvent(menu));
    }

}

(обратите внимание на то, что в текущих версиях (4.1/8.2) нарушены ограничения, введя CDI Event в 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;
    }

}

И затем в веб-проекте:

@ApplicationScoped
public class Application {

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

}

Обновление: в 1 апреля 2016 года (полгода после ответа выше) OmniFaces, представленный с версия 2.3 <o:socket>, которая должна сделать все это менее крутым. Предстоящий JSF 2.3 <f:websocket> в значительной степени основан на <o:socket>. См. Также Как сервер может асинхронно изменять на HTML-страницу, созданную JSF?

Ответ 2

Поскольку вы используете Primefaces и Java EE 7, это должно быть легко реализовать:

use Primefaces Push (пример здесь http://www.primefaces.org/showcase/push/notify.xhtml)

  • Создайте представление, которое прослушивает конечную точку Websocket
  • Создайте прослушиватель базы данных, который создает событие CDI при изменении базы данных
    • Полезная нагрузка события может быть либо дельтами последних данных, либо просто и обновлять информацию.
  • Распространение события CDI через Websocket всем клиентам
  • Клиенты, обновляющие данные

Надеюсь, что это поможет Если вам нужна дополнительная информация, просто спросите

Привет

Ответ 3

PrimeFaces имеет функции опроса для автоматического обновления компонента. В следующем примере <h:outputText> будет автоматически обновляться каждые 3 секунды <p:poll>.

Как уведомить связанные клиенты и обновить вышеупомянутые меню 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.
    }
}