概述
RichFaces Push 允许您通过服务器端事件触发的实时客户端更新。我们集成了 Atmosphere 框架,它根据具体浏览器的支持提供各种传输机制(Comet、HTML5 WebSocket)。在服务器端,事件通过集成 Java 消息服务 (JMS) 进行管理。这提供了从企业级消息集成到浏览器的完整解决方案!
在这篇博客中,我将介绍我们创建的 irc-client 示例应用程序,该应用程序旨在介绍 RichFaces push 功能。它提供以下功能
- 连接到 freenode.org IRC 服务器
- 以所选昵称加入频道
- 接收所有频道消息(昵称更改、加入、离开...)
- 向频道发送消息
- 实时观察加入用户列表
在这篇博客中,我只会回顾重要的配置和应用程序中的代码片段。完整源代码可以在 SVN 中查看,您可以亲自尝试。构建和部署应用程序的说明在 readme.txt 中。
依赖项
为了节省应用程序设置时间,我使用 richfaces-archetype-simpleapp 架构 创建了一个入门应用程序。接下来,我们需要更新应用程序 pom.xml 以添加 Atmosphere 框架 依赖项。客户端的 a4j:push 使用 jquery-atmosphere.js 插件(由组件渲染器隐式添加)。
注意:该特定示例针对 JBoss 6 应用服务器。因此,我们将使用开箱即用的 JBoss HornetQ。在 Tomcat 或 Jetty 下使用 RichFaces Push 需要 JMS 依赖项和配置代码。我们将在未来的版本中处理 maven 配置文件和额外的可选类。目前,审查该配置的唯一地方是我们的开发者演示,该演示针对 Tomcat 配置,使用外部 HornetQ 依赖项和配置类进行初始化。
我们需要将 atmosphere-runtime 添加到 pom.xml 中。只有在您使用推送组件时才需要此操作。
<dependency> <groupId>org.atmosphere</groupId> <artifactId>atmosphere-runtime</artifactId> </dependency>
此外,如演示所示,我们提供了一个简单的 IRC 客户端,因此添加了一个额外的依赖项
<dependency> <groupId>pircbot</groupId> <artifactId>pircbot</artifactId> <version>1.4.2</version> </dependency>
PircBot 是一个用于编写 IRC 机器人的简单框架。
配置 JMS
首先,让我们讨论一下为什么我们需要 JMS?为什么不用例如推送上下文对象?答案很简单。后端使用 JMS 提供了与 EE 容器和高级消息服务的出色集成,同时也让您免于在业务层管理各种实体。如果您已经在应用程序中使用了 JMS 进行消息传递,您只需继续发送相同的消息,并只需在应监听这些主题的视图中声明 a4j:push 即可。
我们假设您已经熟悉 JMS,因此在这里不会提供详细信息。要了解更多信息,请访问 JMS 文档。
现在,我们需要为我们的示例配置 JMS。我们将使用 JBoss 6 AS 部署应用程序,如依赖项部分所述。因此,我们将使用 与 JBoss AS 打包的 JMS 服务器。我们需要创建将被 Push 用于检查消息的主题。让我们启动 JBoss 6 AS 并打开 http://localhost:8080/admin-console。我们将使用默认的 admin/admin 凭据登录。然后点击导航菜单中的 JMS 主题,并使用以下设置创建新的 JMS 主题
名称: chat
JNDI 名称: /topic/chat 其他所有设置:默认。
在相同的表单中,我们需要为此主题添加角色。对于此演示应用程序,我们将创建一个具有以下参数的单个角色
名称: guest
发送: 是
消费: 是
创建订阅者: 是
删除订阅者: 是
创建持久订阅者:是
删除持久订阅者:是
重要: 最后两个选项对于推送功能至关重要,因为我们使用 持久订阅 以能够接收所有事件,包括推送未连接到服务器时发送的事件。因此,如果它们设置为 false,则推送将无法注册订阅者。
因此,在完成 JMS 设置后,您应该看到以下内容
这就完了。现在我们已经完成了 JMS 配置。
注意:如果您不熟悉使用 admin-console 进行 JBoss 配置,请访问 JBoss AS 管理控制台用户指南。
Web 应用程序配置
现在,让我们添加 RichFaces Push 需要的应用程序设置。我们需要添加一个额外的过滤器和一些 context-param。以下是我们在该演示应用程序中使用的设置
<context-param> <param-name>org.richfaces.push.jms.connectionUsername</param-name> <param-value>guest</param-value> </context-param> <context-param> <param-name>org.richfaces.push.jms.connectionPassword</param-name> <param-value>guest</param-value> </context-param> <filter> <filter-name>PushFilter</filter-name> <filter-class>org.richfaces.webapp.PushFilter</filter-class> <async-supported>true</async-supported> </filter> <filter-mapping> <filter-name>PushFilter</filter-name> <servlet-name>Faces Servlet</servlet-name> </filter-mapping>
让我们详细审查这些参数。首先,我们需要安装 PushFilter 并将其映射到 Faces Servlet。它将处理推送请求并使用 Atmosphere runtime 提供事件。此外,connectionUserName 和 connectionPassword 上下文参数需要用于定义用于连接和订阅主题的凭据。
以下是可用于您应用程序的所有参数的表格
context-param 名称 | 默认值 | 描述 |
---|---|---|
org.richfaces.push.jms.connectionFactory | /ConnectionFactory | JMS 连接工厂 JNDI 名称 |
org.richfaces.push.jms.topicsNamespace | /topic | 用作解析所有主题的根名称:例如,默认情况下 'chat' 主题将通过 /topics/chatJNDI 名称进行查找。 |
org.richfaces.push.jms.connectionUsername | - | 用于连接和主题创建/监听的用户名 |
org.richfaces.push.jms.connectionPassword | - | 用于连接和主题创建/监听的密码 |
注意:还有可用的 JNDI 凭据字符串引用
- java:comp/env/org.richfaces.push.jms.connectionUsername
- java:comp/env/org.richfaces.push.jms.connectionPassword
使用web.xml定义的替代方案是,使用属性文件org/richfaces/push.properties提供所有参数。
TopicsContext 初始化
现在我们需要用我们将要监听的JMS主题来初始化org.richfaces.application.push.TopicsContext。为此,我们创建了一个JSF 2 系统事件监听器,该监听器处理PostConstructApplicationEvent
public class TopicsInitializer implements SystemEventListener { public void processEvent(SystemEvent event) throws AbortProcessingException { TopicsContext topicsContext = TopicsContext.lookup(); Topic topic = topicsContext.getOrCreateTopic(new TopicKey("chat")); topic.setMessageDataSerializer(DefaultMessageDataSerializer.instance()); topic.addTopicListener(new SessionTopicListener() {...}); } public boolean isListenerForSource(Object source) { return true; } }
并将其注册到faces-config
<application> <system-event-listener> <system-event-listener-class>org.ircclient.listeners.TopicsInitializer</system-event-listener-class> <system-event-class>javax.faces.event.PostConstructApplicationEvent</system-event-class> </system-event-listener> </application>
这样,我们在TopicContext中创建了一个名为chat的新主题(与我们在配置JMS时定义的相同)。
实际上,MessageDataSerializer和TopicListener的创建是可选的,所以让我们简要描述一下这些对象的主要思想。
- 消息序列化器用于将消息序列化为传递给客户端的预期格式。默认情况下,内置序列化器将消息数据序列化为JavaScript。
- 会话主题监听器用于处理订阅(预处理和后处理)和取消订阅事件。这可以用于在连接到某些主题之前检查用户权限,并在某些推送组件成功附加到主题后执行一些额外的后处理。在示例中仅使用了日志记录,因此代码被省略。
应用程序代码
我们已经完成了设置和推送初始化相关的内容,现在是时候实际查看示例代码,看看我们如何管理IRC连接,以及如何根据服务器端事件使用推送进行页面更新。
记录到IRC
本节与推送无直接关系,但描述了用于连接到IRC的代码。下面是使用的简单页面
<rich:panel header="Connect to IRC"> <h:form> <rich:messages style=”color:red”/> <h:panelGrid columns="2"> <h:outputText value="Your nickname:" /> <h:inputText required="true" id="name" value="#{chatBean.userName}" /> <h:outputText value="Channel:" /> <h:outputText value="RichFaces" style="font-weight:bold"/> <h:outputText value="Server:" /> <h:outputText value="irc.freenode.org" style="font-weight:bold"/> </h:panelGrid> <h:commandButton value="Connect" action="#{chatBean.connect}" /> </h:form> </rich:panel>
它被渲染为
请注意,出于简单起见,频道和服务器名称使用常量。在未来,我们计划扩展示例以支持打开多个频道等。
以下是使用的ChatBean.java代码
@ManagedBean @SessionScoped public class ChatBean extends PircBot implements Serializable { private static final String SERVER_URL = "irc.freenode.org"; private static final int SERVER_PORT = 6667; private static final String CHANNEL_PREFIX = "#"; private static final String SUBTOPIC_SEPARATOR = "_"; private static final String DEFAULT_CHANNEL = "richfaces"; private String channelName; private String message; public String connect() { try { this.connect(SERVER_URL, SERVER_PORT); this.joinChannel(CHANNEL_PREFIX + DEFAULT_CHANNEL); channelName = DEFAULT_CHANNEL; } catch (NickAlreadyInUseException e) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, this.getName() + " nick already in use", this.getName() + " nick already in use")); return null; } catch (IOException e) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Sorry, server unresponsive. Try again later.", "Sorry, server unresponsible. Try again later.")); return null; } catch (IrcException e) { FacesContext.getCurrentInstance().addMessage(null, new FacesMessage(FacesMessage.SEVERITY_ERROR, "Sorry, we encountered IRC services problems. Try again later.", "Sorry, we encountered IRC services problems. Try again later.")); return null; } return "chat"; } ... }
如您所见,我们的Bean扩展了PirBot抽象类。所以我们只是使用PircBot API来连接到服务器,并将重定向到对我们最有兴趣的chat.xhtml页面。
客户端主页面
下面的截图显示了我们在该页面上想要实现的结果
以下是chat.xhtml页面的完整列表。我们将在下面详细审查
<script> function getMessageString(data){ return data.author + " - " +data.timestamp+ ": " + data.text; } </script> <rich:panel header="Welcome to #{chatBean.channelName} channel at #{chatBean.serverName}" id="chatpanel"> <rich:panel styleClass="chatOutput" bodyClass="#{chatBean.channelName}Output" /> <rich:panel styleClass="chatList"> <rich:list value="#{chatBean.users}" var="user" id="users" type="unordered"> #{user.nick} </rich:list> </rich:panel> <br clear="all" /> <hr /> <h:form> <a4j:push address="#{chatBean.listSubtopic}@chat" onerror="alert(event.rf.data)"> <a4j:ajax event="dataavailable" render="users" execute="@none" /> </a4j:push> <a4j:push address="#{chatBean.messagesSubtopic}@chat" onerror="alert(event.rf.data)" ondataavailable="jQuery('<div/>'). prependTo('.#{chatBean.channelName}Output').text( getMessageString(event.rf.data))" /> <h:inputTextarea value="#{chatBean.message}" rows="3" style="width:80%" id="nm" /> <a4j:commandButton value="Send" action="#{chatBean.send}" render="@none" execute="@form" /> </h:form> <hr /> <h:form> <rich:panel header="Change nickname:"> <h:inputText valueChangeListener="#{chatBean.changeNick}" id="cn" /> <a4j:commandButton value="Change" execute="@form" render="@none"/> </rich:panel> <h:commandButton value="Disconnect" action="#{chatBean.leave}"/> </h:form> </rich:panel>
我将向您展示两种执行推送更新的选项。
- 使用JavaScript处理器ondataavailable直接从客户端获取JMS事件的数据。
- 使用a4j:ajax行为触发一个Ajax请求,当推送通知我们服务器事件时执行局部更新。
所以第一个面板
<rich:panel styleClass="chatOutput" bodyClass="#{chatBean.channelName}Output" />
只是一个容器,其中包含将通过JavaScript更新的聊天文本。
第二个面板
<rich:panel styleClass="chatList"> <rich:list value="#{chatBean.users}" var="user" id="users" type="unordered"> #{user.nick} </rich:list> </rich:panel>
包含一个rich:list组件,显示连接到频道的用户。它将在服务器端填充有关列表更改的事件后通过Ajax请求进行更新。
在服务器端事件上使用JavaScript进行客户端更新
让我们更详细地审查第一个a4j:push组件
<a4j:push address="#{chatBean.messagesSubtopic}@chat" onerror="alert(event.rf.data)" ondataavailable="jQuery('<div/>').prependTo('.#{chatBean.channelName}Output'). text(getMessageString(event.rf.data))" />
它用于将消息获取到主聊天窗口中。以下是TopicContext声明和ChatBean的main方法,它生成这些推送事件
import org.richfaces.application.push.TopicKey; import org.richfaces.application.push.TopicsContext; ... private transient TopicsContext topicsContext; public String getMessagesSubtopic() { return this.getUserName() + SUBTOPIC_SEPARATOR + channelName; } private TopicsContext getTopicsContext() { if (topicsContext == null) { topicsContext = TopicsContext.lookup(); } return topicsContext; } @Override protected void onMessage(String channel, String sender, String login, String hostname, String message) { try { Message messageObject = new Message(message, sender, DateFormat.getInstance().format( new Date())); getTopicsContext().publish(new TopicKey("chat", getMessagesSubtopic()), messageObject); } catch (MessageException e) { LOGGER.error(e.getMessage(), e); } } public void send() { this.sendMessage(CHANNEL_PREFIX + channelName, message); try { Message messageObject = new Message(message, this.getName(), DateFormat.getInstance().format(new Date())); getTopicsContext().publish(new TopicKey("chat", getMessagesSubtopic()), messageObject); } catch (MessageException e) { LOGGER.error(e.getMessage(), e); } }
onMessage方法 - 重写了PircBot方法,在来自其他聊天用户的消息到来时被调用。当发送消息时调用send方法。这是发送按钮的标准JSF操作。
如您所见,为了为a4j:push引发事件,我们只需将其发布到TopicContext。 publish方法接受两个参数
- TopicKey key - 地址由主题和子主题名称组成。
- 对象数据 - 实际传递给客户端事件的参数。
由页面上的 a4j:push 组件使用的 getMessagesSubtopic() 方法生成的消息子主题,对于每个用户和频道都是唯一的,因此只有当前用户会接收到包含该对象的事件。
作为数据传递的 Message 对象非常简单,是一个具有三个字符串属性(作者、文本和时间戳)的 POJO。它后来在客户端的 push oncomplete 中通过 getMessageString(data) JavaScript 方法创建格式化的字符串,并将其添加到聊天 div 中。
<script> function getMessageString(data){ return data.author + " - " +data.timestamp+ ": " + data.text; } </script>
重要:如前所述,发布事件的另一种方法是通过直接发送到 JMS 总线。所以在这种情况下,您无需使用我们的 TopicContext。我们只是为了简单示例使用了这种方法,因为我们没有其他组件,只有简单的 JSF 实体。请参阅“附加详情”部分获取有关通过 JMS 总线发布的更多信息。
使用 a4j:ajax 在服务器端事件上执行 Ajax 更新。
现在让我们回顾一下根据接收到的 a4j:push 事件执行更新的第二种方法。
<a4j:push address="#{chatBean.listSubtopic}@chat" onerror="alert(event.rf.data)"> <a4j:ajax event="dataavailable" render="users" execute="@none" /> </a4j:push>
它用于更新显示当前频道用户的 rich:list。以下是 ChatBean 的主要方法,这些方法生成用于该推送的事件。
public String getListSubtopic() { return this.getUserName() + SUBTOPIC_SEPARATOR + channelName + "List"; } @Override protected void onUserList(String channel, User[] users) { try { getTopicsContext().publish(new TopicKey("chat", getListSubtopic()), null); } catch (MessageException e) { LOGGER.error(e.getMessage(), e); } } @Override protected void onJoin(String channel, String sender, String login, String hostname) { try { getTopicsContext().publish(new TopicKey("chat", getListSubtopic()), null); Message messageObject = new Message("joined channel", sender, DateFormat.getInstance().format(new Date())); getTopicsContext().publish(new TopicKey("chat", getMessagesSubtopic()), messageObject); } catch (MessageException e) { LOGGER.error(e.getMessage(), e); } }
我们在服务器端使用相同的通过 TopicContext 发布方法,只是为该 a4j:push 定义了不同的子主题。并且传递 null 作为数据,因为我们不打算在客户端处理任何内容。这就是
<a4j:ajax event="dataavailable" render="users" execute="@none" />
发挥作用的地方。当推送接收到事件时,一个 Ajax 行为发送一个请求,仅执行 rich:list 的渲染,并使用新值。很简单,不是吗?
附加详情
在那里我想提醒您注意文章中未涉及的一些细节。
单个连接
如您在此示例中看到的,我们在一个页面上使用了两个 a4j:push 组件来演示处理推送事件的不同方法。重要的是要知道,这并不意味着将打开两个连接。所有推送组件的实例都使用单个连接,只有订阅了该连接的实例在接收到某些事件后会根据其地址接收事件以进行处理。您可以使用 Firebug for FireFox 等工具来验证这一点。
通过 JMS 总线发布
推送支持的 JMS 消息类型包括:
- ObjectMessage
- TextMessage
从 getObject 或 getText 传递给 DataSerializer 的对应数据,并将其序列化到客户端数据(使用默认序列化器传递给 JavaScript)。
待续...
最后,我想宣布将在下一篇关于 Push 的相关文章中涉及的主题。
与 CDI 事件的集成
CDI 在 JEE6 生态系统中的地位很高,因此在 RichFaces 开发过程中受到了很多关注。a4j:push 对 CDI 事件系统的支持也是即将单独发表的博客文章的主题!
还有更多...
这个组件有很多可能性,太多无法用一篇文章描述。所以请关注我们的博客或 RichFaces Twitter 以及与该组件相关的任何未来示例和 文档 更新。