Tin*_*iny 29 jsf java-ee websocket primefaces java-ee-7
我有一个应用程序在以下环境中运行.
有几个公共页面从数据库中延迟加载.一些CSS菜单显示在模板页面的标题上,如显示类别/子类别特征,最畅销,新到达等产品.
CSS菜单根据数据库中的各种产品类别从数据库动态填充.
这些菜单填充在每个页面加载上,这是完全没有必要的.其中一些菜单需要复杂/昂贵的JPA标准查询.
目前,填充这些菜单的JSF托管bean是视图范围的.它们都应该是应用程序作用域,在应用程序启动时只加载一次,并且只有在更新/更改相应数据库表(类别/子类别/产品等)中的某些内容时才更新.
我做了一些尝试来理解WebSokets(从未尝试过,对WebSokets来说是全新的),就像这样和这个.他们在GlassFish 4.0上运行良好,但它们不涉及数据库.我仍然无法正确理解WebSokets的工作方式.特别是涉及数据库时.
在这种情况下,当更新/删除/添加到相应的数据库表时,如何通知关联的客户端并使用数据库中的最新值更新上述CSS菜单?
一个简单的例子很棒.
Bal*_*usC 47
在这个答案中,我将假设以下内容:
<p:push>
(我会在中间留下确切的理由,你至少对使用新的Java EE 7/JSR356 WebSocket API感兴趣).首先创建一个@ServerEndpoint
类,它基本上将所有websocket会话收集到应用程序范围集中.请注意,在这个特定的示例中,这只能是static
因为每个websocket会话基本上都有自己的@ServerEndpoint
实例(它们与Servlet不同,因此无状态).
@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);
}
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
上面的示例有一个额外的方法sendAll()
,它将给定的消息发送到所有打开的websocket会话(即应用程序作用域推送).请注意,此消息也可以是一个JSON字符串.
如果您打算将它们显式存储在应用程序范围(或(HTTP)会话范围)中,那么您可以使用此答案中的ServletAwareConfig
示例.你知道,属性映射到JSF(和属性映射到).ServletContext
ExternalContext#getApplicationMap()
HttpSession
ExternalContext#getSessionMap()
使用这段JavaScript打开websocket并听取它:
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.
}
Run Code Online (Sandbox Code Playgroud)
截至目前,它只记录推送的文本.我们想将此文本用作更新菜单组件的指令.为此,我们需要额外的<p:remoteCommand>
.
<h:form>
<p:remoteCommand name="updateMenu" update=":menu" />
</h:form>
Run Code Online (Sandbox Code Playgroud)
想象一下,您将JS函数名称作为文本发送Push.sendAll("updateMenu")
,然后您可以解释并触发它,如下所示:
ws.onmessage = function(event) {
var functionName = event.data;
if (window[functionName]) {
window[functionName]();
}
};
Run Code Online (Sandbox Code Playgroud)
同样,当使用JSON字符串作为消息(您可以解析它$.parseJSON(event.data)
)时,可以实现更多动态.
现在我们需要Push.sendAll("updateMenu")
从DB端触发命令.允许DB在Web服务上触发HTTP请求的最简单方法之一.一个简单的vanilla servlet足以像Web服务一样行事:
@WebServlet("/push-update-menu")
public class PushUpdateMenu extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Push.sendAll("updateMenu");
}
}
Run Code Online (Sandbox Code Playgroud)
您当然有机会根据请求参数或路径信息参数化推送消息,如有必要.如果允许调用者调用此servlet,请不要忘记执行安全检查,否则世界上除了DB本身之外的任何其他人都可以调用它.例如,您可以检查呼叫者的IP地址,如果数据库服务器和Web服务器都在同一台计算机上运行,这将非常方便.
为了让DB在该servlet上触发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 ;
Run Code Online (Sandbox Code Playgroud)
现在您可以创建将调用它的插入/更新/删除触发器(假设表名已命名menu
):
CREATE TRIGGER after_menu_insert
AFTER INSERT ON menu
FOR EACH ROW CALL menu_push();
Run Code Online (Sandbox Code Playgroud)
CREATE TRIGGER after_menu_update
AFTER UPDATE ON menu
FOR EACH ROW CALL menu_push();
Run Code Online (Sandbox Code Playgroud)
CREATE TRIGGER after_menu_delete
AFTER DELETE ON menu
FOR EACH ROW CALL menu_push();
Run Code Online (Sandbox Code Playgroud)
如果您的要求/情况允许监听JPA实体变化的事件而已,从而给DB外部变化并没有需要覆盖,那么你可以,而不是如在步骤3a中描述的也只是使用JPA实体更改侦听数据库触发器.您可以通过类中的@EntityListeners
注释注册它@Entity
:
@Entity
@EntityListeners(MenuChangeListener.class)
public class Menu {
// ...
}
Run Code Online (Sandbox Code Playgroud)
如果您碰巧使用单个Web配置文件项目,其中所有内容(EJB/JPA/JSF)在同一项目中被抛出,那么您可以直接Push.sendAll("updateMenu")
在那里调用.
public class MenuChangeListener {
@PostPersist
@PostUpdate
@PostRemove
public void onChange(Menu menu) {
Push.sendAll("updateMenu");
}
}
Run Code Online (Sandbox Code Playgroud)
但是,在"企业"项目中,服务层代码(EJB/JPA/etc)通常在EJB项目中分离,而Web层代码(JSF/Servlets/WebSocket/etc)保存在Web项目中.EJB项目应该没有对Web项目的单一依赖.在这种情况下,你最好开发一个CDI,Event
而不是Web项目@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));
}
}
Run Code Online (Sandbox Code Playgroud)
(注意注释; Event
在当前版本(4.1/8.2)中,GlassFish和WildFly都注册了CDI ;解决方法是通过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;
}
}
Run Code Online (Sandbox Code Playgroud)
然后在Web项目中:
@ApplicationScoped
public class Application {
public void onMenuChange(@Observes MenuChangeEvent event) {
Push.sendAll("updateMenu");
}
}
Run Code Online (Sandbox Code Playgroud)
更新:2016年4月1日(上述答案后半年),OmniFaces推出了版本2.3,<o:socket>
这应该使这一切不那么迂回.即将推出的JSF 2.3 <f:websocket>
主要基于<o:socket>
.另请参见服务器如何将异步更改推送到JSF创建的HTML页面?
由于您使用的是Primefaces和Java EE 7,因此应该易于实现:
使用Primefaces Push(此处示例http://www.primefaces.org/showcase/push/notify.xhtml)
希望这对您有帮助如果您需要更多详细信息,请询问
问候
归档时间: |
|
查看次数: |
21411 次 |
最近记录: |