
所有的计算都表明它不工作,唯一的做法是:使其工作。
--Pierre-Georges Latécoère 早期法国航空企业家
1综述
1.1 前言
Openfire是免费的、开源的、基于可拓展通讯和表示协议(XMPP)、采用Java编程语言开发的IM服务器,Openfire的组件分为内部基础组件和外部扩展组件两种,我们在实际的业务应用时,为了满足不断变化的复杂业务,Openfire本身的基础功能已经很难满足我们实际的业务需求,这就需要我们使用其外部扩展功能(插件),来快速实现各种复杂业务,实现插件化方式功能扩展。
1.2 编写目的
本文档详细描述了Openfire插件化开发所涉及到的主要相关内容,能够很好的指导开发者对Openfire进行插件化的开发。
如下图为预期目标,将相互独立的业务模块作为一个单独的插件,采用Maven进行开发,可实现不同业务插件,进行独立的上线下线等操作,而不会造成相互影响。

1.3 参考资料
该文档的整理主要参考以下资料,大家在需要的时候可以进行翻阅,增加对相关内容和知识的理解。
Openfire官网(英文)https://www.igniterealtime.org/projects/openfire/index.jsp
Openfire文档(英文)http://download.igniterealtime.org/openfire/docs/latest/
Openfire源码 https://github.com/igniterealtime
以及一些其他的开源博客,实际工作中,可以看博客文章入门,但是需要阅读官方文档和源码进行深入的理解和应用。
2 插件化开发
2.1 Openfire插件分类
在我们实际应用中,openfire的插件化功能通常分为如下三大类:
1、消息等内部插件
这类插件主要用于对 openfire 内处理的XMPP消息(Iq、Prence、Message),进行一定功能的扩展。例如:客服消息、离线消息推送处理、多端同步等等。
2、WebUI 插件
我们知道Openfire提供了控制台(通过admin插件实现),方便我们通过控制台来进行一些功能的查看和操作,同时通过插件化也可以扩展自己所需要的web功能页面(需要通过Jsp进行实现)。
3、Http接口插件
这类插件主要用于对 openfire 后台接口扩展,例如我们对外暴露关于群的操作以及用户相关接口。
以上插件的是从功能上来划分的,在我们实际的开发中,在一个业务插件中可能同时涵盖这三种类型的插件,这个需要根据我们实际的业务进行选择实现,一个openfire插件的实现通常分为如下几步,1)、实现 Plugin 类。2)、添加 plugin.xml 配置插件启动类。3)、实现对应业务功能(以上三个类别)。4)、添加 changelog.html, logo_*.png/gif, readme.html 等说明文件及logo,然后发布上线。
2.2 Openfire插件工程结构
基本的目录结构如下图:

1、其中 changelog.html, logo_*.png/gif, readme.html 说明文件,对应在插件安装后对应的信息会展示在插件列表中,对应如下:

其中的logo大小中,small为16X16,large为32X32的大小
2、pom.xml maven进行包管理的文件。
3、lib 存放该插件开发时依赖的一些特殊外部jar包,也可以通过pom.xml配置管理。
4、src 为核心代码文件夹,其中 i18n 为国际化是需要配置的内容-其命名一定是规范化的[插件名称]_i18n[.国际化].properties,database 为该插件所需要的脚本,后面会专门描述。Java目录下为对应的源代码逻辑。
5、web 目录为对应的openfire管理后台添加的jsp页面。
2.3 核心配置文件说明
plugin.xml文件说明
通常 plugin.xml 是导入 PluginManager 中的默认配置文件,其核心配置如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<plugin>
<!-- 插件的入口HelloPlugin需要实现Plugin接口 -->
<class>com.demo.hello.HelloPlugin</class>
<!-- 插件的名称,在安装插件后会展示在插件列表中 -->
<name>hello</name>
<!-- 插件的描述,展示在插件列表中 -->
<description>First Openfire Custom Plugin.</description>
<!-- 插件开发者 -->
<author>ladd</author>
<!-- 插件的版本 -->
<version>1.0.0</version>
<!-- 插件发布时间The date must be in the form MM/dd/yyyy, such as 07/01/2006 -->
<date>11/07/2018</date>
<!-- 最小支持的openfire 版本 -->
<minServerVersion>4.2.0</minServerVersion>
<!-- 控制台上追加界面 -->
<adminconsole>
<!-- tab位置 -->
<tab id="tab-server">
<!-- sidebar位置 -->
<sidebar id="sidebar-server-manager">
<!-- item位置,使用 name 进行 i18n 语言配置,url连接地址配置 -->
<item id="hello-setting" name="${hello.title}" url="hello.jsp"
description="Quickly change the HELLO configurations." />
</sidebar>
</tab>
</adminconsole>
</plugin>上面是一些常见的配置,下面介绍2个其他的关键配置项
- parentPlugin-父插件
我们在插件开发时,也会遇到一些插件存在一定的关联关系,例如我们开发的插件exampleplugin需要在search插件加载后,才进行加载,则需要在插件exampleplugin的plugin.xml中添加如下配置:
<parentPlugin>search</parentPlugin>其中search为插件解压后的文件名称。


2、databaseKey 和 databaseVersion 的配置
如果插件拥有它自己的数据库表,就需要设置databaseKey它的名称(格式:<databaseKey>pushnotification</databaseKey>)通常和插件的名称一致,对应脚本名称必须${ databaseKey }_mysql.sql 和 ${ databaseKey }_oracle.sql 且需要放到 database 目录结构下,不同的数据库配置不同的脚本。且在初始化时我们需要在库中插入一条数据
INSERT INTO ofVersion (name, version) VALUES ('foo', 0);
后续如果有更新的话通常我们需要设置databaseVersion(格式:<databaseVersion>1</databaseVersion>),且需要创建目录database/uprade/${ databaseVersion } 以及对应版本目录,将脚本${ databaseKey }_mysql.sql放置到该目录下,
例如:对于设置了在plugin.xml中设置了,如下参数的

则对应的database下的目录应该如下:

且database下的sql 脚本应该是涵盖了所有版本的完整数据,upgrade下的是只含有特定版本的脚本。
web-custom.xml文件说明
[WEB-INF] 目录下 web-custom.xml 则是对 servlet 等动态注册的配置页面,类似 Tomcat 中的 web.xml 配置,详细参见 Web 接口开发,如下为配置的servlet项。

2.4 消息等内部插件
主要在插件主入口处拿到 XMPPServer 对象,并插入相关的回调函数,进行相应的处理即可。但要确保自己初始化的内容,能够在插件销毁时进行回收。常见格式如下:
public class HelloPlugin implements Plugin
{
private XMPPServer mXMPPServer;
public void initializePlugin(PluginManager manager, File pluginDirectory)
{
// 获取 XMPPServer 服务
mXMPPServer = XMPPServer.getInstance();
// 添加 IQHandler --> mHandler
mXMPPServer.getIQRouter().addHandler(mHandler);
// 添加拦截器
InterceptorManager.getInstance().addInterceptor(interceptor);
// 添加离线消息监听
mXMPPServer.getOfflineMessageStrategy().addListener(this);
// TODO 更多
}
public void destroyPlugin()
{
// 删除 IQHandler --> mHandler
mXMPPServer.getIQRouter().removeHandler(mHandler);
// 删除离线消息监听
mXMPPServer.getOfflineMessageStrategy().removeListener(this);
}
}Openfire本身在系统内部提供了很多的回调函数,我们可以根据自己的需要来进行实现,如下为我列举的一些常见处理,
- 实现PropertyEventListener接口,可以实现监听动态参数的变更,通过 PropertyEventDispatcher.addListener(propertyEventListener)方法, 添加到openfire的回调函数。
- (最常用)实现PacketInterceptor接口,实现消息的拦截,通过InterceptorManager.getInstance().addInterceptor(interceptor),添加到Openfire的回调函数。
- 实现IQHandler接口,可针对实际需要定制IQ的消息。通过如下方法XMPPServer.getInstance().getIQRouter().addHandler( push0IQHandler ),添加到Openfire的回调钩子中。
- 实现UserEventListener接口,可以监听到用户状态的变更,进行特定的操作。通过UserEventDispatcher.addListener( this )添加到openfire内部。
- 实现OfflineMessageListener,可以监听消息离线的丢弃或则存储的逻辑,并根据需要实现自己的业务。通过如下方式注入到openfire内部OfflineMessageStrategy.addListener( listener)
以上列举的仅仅时很小的一部分,Openfire内部有很多的功能回调钩子子,我们根据自己的实际业务场景,多做分析,选择最合适的来进行实现。
2.5 WebUI 开发
如果所有UI都是界面,则可以通过 jsp 进行,具体进行如下几步即可:
- 配置入口
在 plugin.xml 中的 <adminconsole> 标签中添加相关的 item 即可:
<!-- 控制台上追加界面 -->
<adminconsole>
<!-- tab位置 -->
<tab id="tab-server">
<!-- sidebar位置 -->
<sidebar id="sidebar-server-manager">
<!-- item位置,使用 name 进行 i18n 语言配置,url连接地址配置 -->
<item id="hello-setting" name="${hello.title}"
url="hello.jsp"
description="Quickly change the HELLO configurations." />
</sidebar>
</tab>
</adminconsole>

注意:这里 tab 和 sidebar 中设置的 id 代表 显示的位置, 具体可以查询 openfire_src/xmppserver/src/main/resources/admin-sidebar.xml 中的id,从而确认相关 tab 和 sidebar 的 id 名称。
- 编辑 jsp 文件
注意这里需要导入如下几个标签,不然 i18n 可能没法使用...
<%@ page import="org.jivesoftware.util.JiveProperties" errorPage="error.jsp"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>具体如下:
<%@ page import="org.jivesoftware.openfire.container.PluginManager, org.jivesoftware.util.JiveProperties" errorPage="error.jsp"%>
<%@ page import="java.util.Map"%>
<%@ page import="java.util.HashMap"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<jsp:useBean id="webManager" class="org.jivesoftware.util.WebManager" />
<%
webManager.init(request, response, session, application, out);
%>
<%
// Get parameters
boolean update = request.getParameter("update") != null;
Map<String, String> errors = new HashMap<String, String>();
final JiveProperties mJiveProperties = JiveProperties.getInstance();
if (update) {
// TODO update params
}
%>
<html>
<head>
<title><fmt:message key="hello.title" /></title>
<meta name="pageID" content="jpush-setting" />
</head>
<body>
<p>
<fmt:message key="hello.info" />
</p>
</body>
</html>2.6 Http接口插件开发
在 [WEB-INF] 目录下 web-custom.xml 则是对 servlet 等动态注册的配置页面,类似 Tomcat 中的 web.xml 配置, 如下两种方式进行配置:
1.Servlet 注册
比如 Fastpath 插件下的 servlet 配置如下:
<?xml version='1.0' encoding='ISO-8859-1'?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- Servlets -->
<servlet>
<servlet-name>ImageServlet</servlet-name>
<servlet-class>org.jivesoftware.openfire.fastpath.ImageServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>SoundServlet</servlet-name>
<servlet-class>org.jivesoftware.openfire.fastpath.SoundServlet</servlet-class>
</servlet>
<!-- Servlet mappings -->
<servlet-mapping>
<servlet-name>ImageServlet</servlet-name>
<url-pattern>/getimage</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>SoundServlet</servlet-name>
<url-pattern>/getsound</url-pattern>
</servlet-mapping>
</web-app>2.继承HttpServlet实现其方法
package org.jivesoftware.openfire.fastpath;
/**
* A servlet that displays images.
*/
public class ImageServlet extends HttpServlet {
private static final String CONTENT_TYPE = "image/jpeg";
private ChatSettingsManager chatSettingsManager;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
chatSettingsManager = ChatSettingsManager.getInstance();
}
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
String imageName = request.getParameter("imageName");
String workgroupName = (String)request.getSession().getAttribute("workgroup");
if (workgroupName == null) {
workgroupName = request.getParameter("workgroup");
}
byte[] bytes = getImage(imageName, workgroupName);
if (bytes != null) {
writeBytesToStream(bytes, response);
}
}
}另外可以采用框架jersey(Jersey是一个是 webservice框架,可以结合servelt实现类似SpringMvc的RestFul编程风格),这里不详细说明,可以自行百度或则参考openfire 开源插件 openfire-restAPI-plugin(https://github.com/igniterealtime/openfire-restAPI-plugin)。
3其他事项
3.1本地开发调试
我们目前是openfire的插件是独立开发的,如果需要在本地调试需要结合源码进行操作,这里以IDEA为例子进行说明:
- 本地启动openfire,下载github源码到本地,导入IDEA如下图。并执行mvn packege命令,如果出现如下build success 则表示成功。

- 配置Openfire启动debug参数

a).Main class: org.jivesoftware.openfire.starter.ServerStarter
b) VM options:在这里需要设置如下几个主要参数
-DopenfireHome: 指定编译后Openfire的目录(Openfire编译后在openfire_src/ distributiontargetdistribution-base).
-Dlog4j.configurationFile:主要指定配置的log日志。
-Dopenfire.lib.dir:依赖的所有Jar包
-server:是以服务方式启动
-DopenfireHome="D:projectsOpenfire-4.5.1distributiontargetdistribution-base"
-Xverify:none
-server
-Dlog4j.configurationFile="D:projectsOpenfire-4.5.1distributiontargetdistribution-baseliblog4j2.xml"
-Dopenfire.lib.dir="D:projectsOpenfire-4.5.1distributiontargetdistribution-baselib"
-Dfile.encoding=UTF-8
- 启动Openfire

启动Openfire后,根据提供的访问地址,可以访问Openfire的控制台进行配置,这里不在详述。
- 调试Openfire插件
将插件和Openfire的源码导入到同一个工作空间,

编译后将编译后的压缩包放入到:
D:projectsOpenfire-4.5.1distributiontargetdistribution-baseplugins目录下。重新已debug模式启动就可以了。