Ответ 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?