OpenFire源码学习之十八:IOS离线推送

IOS离线推送

场景:

如果您有IOS端的APP,在会话聊天的时候,用户登陆了但可能会退出了界面。这时候其他终端给目标端发送消息时候,消息可以发送到IOS的推送服务器。用过QQ的都知道,你会有哦一条消息在您的主屏上展示。这个就是利用了IOS的推送服务器呢。那么openfire只需要判断用户不在线的时候将消息推送给IOS端。

苹果服务器的消息推送都需要手机的唯一标志,也就是唯一的终端设备号。那么IOS端在登陆的时候需要将该手机的设备号传递给OF服务器。这个传递很简单,您可以自定义发送IQ消息。也可以在登陆后绑定资料的时候添加JID属性来绑定设备。(建议用绑定资源的形式,这样在服务端判断的时候可以很方便的根据JID的属性值来决定是都推送)

服务端实现ios消息推送所需2个证书(附件):测试推送证书.p12、正式推送正式.p12,密码都为123456.

2个证书的区别在于一个是用于开发测试的推送证书,一个是用于产品正式上线的推送证书。2个证书获取到的终端token是不一样的。这2个证书用于JAVA后台连接APNS的服务器地址也是不同的,测试推送证书对应服务器地址是:gateway.sandbox.push.apple.com , 正式推送证书对应的服务器地址是:gateway.push.apple.com .

具体怎么做呢:

1、安装IOS推送服务需要的证书到本地,这个在网上有很多中方法

2、IOS终端登陆发送设备消息给服务器,或者以绑定资源的形式。

3、OF服务端接收该设备ID,并保存起来。

4、当有消息推送时候,根据JID属性push消息。

接下来具体看看源码了。

源码

OfflinePushPlugin

public class OfflinePushPlugin implements Component, Plugin, PropertyEventListener, PacketInterceptor{

	private static final Logger Log = LoggerFactory.getLogger(OfflinePushPlugin.class);

	public static final String NAMESPACE_JABBER_IQ_TOKEN_BIND= "jabber:iq:token:bind";
	public static final String NAMESPACE_JABBER_IQ_TOKEN_UNBUND= "jabber:iq:token:unbund";

	public static final String SERVICENAME = "plugin.offlinepush.serviceName";
    public static final String SERVICEENABLED = "plugin.offlinepush.serviceEnabled";

    private ComponentManager componentManager;
    private PluginManager pluginManager;

    private String serviceName;
private boolean serviceEnabled;
//证书安装的目录
    private static String dcpath = System.getProperty("openfireHome") + "\\conf\\";
    private String dcName;
    private String dcPassword;
    private boolean enabled;

    private static Map<String, String> map = new ConcurrentHashMap<String, String>(20);
    private static Map<String, Integer> count = new ConcurrentHashMap<String, Integer>(20);

    private static AppleNotificationServer appleServer = null;
    private static List<PayloadPerDevice> list ;

    public String getDcName() {
		return dcName;
	}

	public void setDcName(String dcName) {
		JiveGlobals.setProperty("plugin.offlinepush.dcName", dcName);
		this.dcName = dcName;
	}

	public String getDcPassword() {
		return dcPassword;
	}

	public void setDcPassword(String dcPassword) {
		JiveGlobals.setProperty("plugin.offlinepush.password", dcPassword);
		this.dcPassword = dcPassword;
	}

	public boolean getEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
		JiveGlobals.setProperty("plugin.offlinepush.enabled",  enabled ? "true" : "false");
	}

	public OfflinePushPlugin () {
    	serviceName = JiveGlobals.getProperty(SERVICENAME, "offlinepush");
        serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true);
    }

	@Override
	public void xmlPropertySet(String property, Map<String, Object> params) {

	}

	@Override
	public void xmlPropertyDeleted(String property, Map<String, Object> params) {
	}

	@Override
	public void initializePlugin(PluginManager manager, File pluginDirectory) {

		dcName = JiveGlobals.getProperty("plugin.offlinepush.dcName", "");
        // If no secret key has been assigned to the user service yet, assign a random one.
        if (dcName.equals("")){
        	dcName = "delementtest.p12";
            setDcName(dcName);
        }
        dcpath += dcName;
		dcPassword = JiveGlobals.getProperty("plugin.offlinepush.password", "");
		if (dcPassword.equals("")){
			dcPassword = "123456";
            setDcPassword(dcPassword);
        }

		enabled = JiveGlobals.getBooleanProperty("plugin.offlinepush.enabled");
        setEnabled(enabled);

        Log.info("dcpath: " + dcpath);
        Log.info("dcPassword: " + dcPassword);
        Log.info("enabled: " + enabled);

		try {
			appleServer = new AppleNotificationServerBasicImpl(dcpath, dcPassword, enabled );
			if (list == null ) {
				list = new ArrayList<PayloadPerDevice>();
			}

		} catch (KeystoreException e1) {
			Log.error("KeystoreException: " + e1.getMessage());
		} 

		pluginManager = manager;

        componentManager = ComponentManagerFactory.getComponentManager();
        try {
            componentManager.addComponent(serviceName, this);
        }
        catch (ComponentException e) {
            Log.error(e.getMessage(), e);
        }

        InterceptorManager.getInstance().addInterceptor(this);
        PropertyEventDispatcher.addListener(this);

	}

	@Override
	public void destroyPlugin() {
		InterceptorManager.getInstance().removeInterceptor(this);
		PropertyEventDispatcher.removeListener(this);
        pluginManager = null;
        try {
            componentManager.removeComponent(serviceName);
            componentManager = null;
        }
        catch (Exception e) {
            if (componentManager != null) {
                Log.error(e.getMessage(), e);
            }
        }
        serviceName = null;
	}

	@Override
	public String getName() {
		return pluginManager.getName(this);
	}

	@Override
	public String getDescription() {
		return pluginManager.getDescription(this);
	}

	@Override
	public void processPacket(Packet p) {
		if (!(p instanceof IQ)) {
            return;
        }
        final IQ packet = (IQ) p;

        if (packet.getType().equals(IQ.Type.error)
                || packet.getType().equals(IQ.Type.result)) {
            return;
        }
        final IQ replyPacket = handleIQRequest(packet);

        try {
            componentManager.sendPacket(this, replyPacket);
        } catch (ComponentException e) {
            Log.error(e.getMessage(), e);
        }
	}

    private IQ handleIQRequest(IQ iq) {
        final IQ replyPacket; // 'final' to ensure that it is set.

        if (iq == null) {
            throw new IllegalArgumentException("Argument 'iq' cannot be null.");
        }

        final IQ.Type type = iq.getType();
        if (type != IQ.Type.get && type != IQ.Type.set) {
            throw new IllegalArgumentException(
                    "Argument 'iq' must be of type 'get' or 'set'");
        }

        final Element childElement = iq.getChildElement();
        if (childElement == null) {
            replyPacket = IQ.createResultIQ(iq);
            replyPacket
                    .setError(new PacketError(
                            Condition.bad_request,
                            org.xmpp.packet.PacketError.Type.modify,
                            "IQ stanzas of type 'get' and 'set' MUST contain one and only one child element (RFC 3920 section 9.2.3)."));
            return replyPacket;
        }

        final String namespace = childElement.getNamespaceURI();
        if (namespace == null) {
            replyPacket = IQ.createResultIQ(iq);
            replyPacket.setError(Condition.feature_not_implemented);
            return replyPacket;
        }

        if (namespace.equals(NAMESPACE_JABBER_IQ_TOKEN_BIND)) {
            replyPacket = processSetUUID(iq, true);
        }
        else if (namespace.equals(NAMESPACE_JABBER_IQ_TOKEN_UNBUND)) {
        	replyPacket = processSetUUID(iq, false);
        }
        else if (namespace.equals(IQDiscoInfoHandler.NAMESPACE_DISCO_INFO)) {
            replyPacket = handleDiscoInfo(iq);
        }
        else {
            // don't known what to do with this.
            replyPacket = IQ.createResultIQ(iq);
            replyPacket.setError(Condition.feature_not_implemented);
        }

        return replyPacket;
    }

    private static IQ handleDiscoInfo(IQ iq) {
        if (iq == null) {
            throw new IllegalArgumentException("Argument 'iq' cannot be null.");
        }

        if (!iq.getChildElement().getNamespaceURI().equals(
                IQDiscoInfoHandler.NAMESPACE_DISCO_INFO)
                || iq.getType() != Type.get) {
            throw new IllegalArgumentException(
                    "This is not a valid disco#info request.");
        }

        final IQ replyPacket = IQ.createResultIQ(iq);

        final Element responseElement = replyPacket.setChildElement("query",
                IQDiscoInfoHandler.NAMESPACE_DISCO_INFO);
        responseElement.addElement("identity").addAttribute("category",
                "directory").addAttribute("type", "user").addAttribute("name",
                "Offline Push");
        responseElement.addElement("feature").addAttribute("var",
                NAMESPACE_JABBER_IQ_TOKEN_BIND);
        responseElement.addElement("feature").addAttribute("var",
                IQDiscoInfoHandler.NAMESPACE_DISCO_INFO);
        responseElement.addElement("feature").addAttribute("var",
                ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT);

        return replyPacket;
    }

    private IQ processSetUUID(IQ packet, boolean isSet) {
    	Element rsmElement = null;
        if (!packet.getType().equals(IQ.Type.set)) {
            throw new IllegalArgumentException(
                    "This method only accepts 'set' typed IQ stanzas as an argument.");
        }

        final IQ resultIQ;

        final Element incomingForm = packet.getChildElement();

        rsmElement = incomingForm.element(QName.get("info",
        		NAMESPACE_JABBER_IQ_TOKEN_UNBUND));

        if(rsmElement == null) {
        	rsmElement = incomingForm.element(QName.get("info",
            		NAMESPACE_JABBER_IQ_TOKEN_BIND));
        }

        resultIQ = IQ.createResultIQ(packet);
        if (rsmElement != null) {
            String osElement = rsmElement.attributeValue("os");
            String jidElement = rsmElement.attributeValue("jid");

            String username = new JID(jidElement).getNode();

            if (osElement == null || jidElement == null) {
                resultIQ.setError(Condition.bad_request);
                return resultIQ;
            }
            if (isSet) {
            	String tokenElement = rsmElement.attributeValue("token");
            	map.put(username, tokenElement);
            	count.put(username, 0);
            	Log.info("set token,username:" + username + " ,token:" + tokenElement);
            }
            else {
            	map.remove(username);
            	count.remove(username);
            	Log.info("remove token,username:" + username );
            }
        }
        else{
            resultIQ.setError(Condition.bad_request);
        }

        return resultIQ;
    }

    public String getServiceName() {
        return serviceName;
    }

    public void setServiceName(String name) {
        JiveGlobals.setProperty(SERVICENAME, name);
    }

    public boolean getServiceEnabled() {
        return serviceEnabled;
    }

    public void setServiceEnabled(boolean enabled) {
        serviceEnabled = enabled;
        JiveGlobals.setProperty(SERVICEENABLED, enabled ? "true" : "false");
    }

    public void propertySet(String property, Map<String, Object> params) {
        if (property.equals(SERVICEENABLED)) {
            this.serviceEnabled = Boolean.parseBoolean((String)params.get("value"));
        }
        if (property.equals("plugin.offlinepush.dcName")) {
            this.dcName = (String)params.get("value");
        }
        else if (property.equals("plugin.offlinepush.enabled")) {
            this.enabled = Boolean.parseBoolean((String)params.get("value"));
        }
        else if (property.equals("plugin.offlinepush.password")) {
        	this.dcPassword = (String)params.get("value");
        }
    }

	/*
	 * (non-Javadoc)
	 *
	 * @see org.jivesoftware.util.PropertyEventListener#propertyDeleted(java.lang.String,
	 *      java.util.Map)
	 */
	public void propertyDeleted(String property, Map<String, Object> params) {
        if (property.equals(SERVICEENABLED)) {
            this.serviceEnabled = true;
        }
        if (property.equals("plugin.offlinepush.dcName")) {
            this.dcName = "delementtest.p12";
        }
        else if (property.equals("plugin.offlinepush.enabled")) {
            this.enabled = false;
        }
        else if (property.equals("plugin.offlinepush.password")) {
        	this.dcPassword = "123456";
        }
    }

	@Override
	public void initialize(JID jid, ComponentManager componentManager)
			throws ComponentException {
		// TODO Auto-generated method stub

	}

	@Override
	public void start() {
		// TODO Auto-generated method stub

	}

	@Override
	public void shutdown() {
		// TODO Auto-generated method stub

	}

	@Override
	public void interceptPacket(Packet packet, Session session,
			boolean incoming, boolean processed) throws PacketRejectedException {
		if (processed && incoming) {
			if (packet instanceof Message) {
				if (((Message) packet).getBody() == null) {
					return;
				}
				JID jid = packet.getTo();
                //获取用户的设备标志id
				String uuid = map.get(jid.getNode());
				if (uuid != null && !"".equals(uuid)) {
					User user = null;
					try {
						user = XMPPServer.getInstance().getUserManager().getUser(jid.getNode());
					} catch (UserNotFoundException e2) {
						e2.printStackTrace();
					}
					PresenceManager presenceManager = XMPPServer.getInstance().getPresenceManager();
					org.xmpp.packet.Presence presence = presenceManager.getPresence(user);
					if (presence == null) {

						String body = ((Message) packet).getBody();
						JSONObject jb = null;
						String msgType = "10015";
						try {

							jb = new JSONObject(body);
							msgType = jb.getString("msgType");
							if ("10012".equals(msgType) || "10001".equals(msgType) || "10002".equals(msgType)) {
								return;
							}
						} catch (JSONException e) {
							try {
                              //根据不同的消息类型,发送不通的提示语
								msgType = jb.getInt("msgType")+"";
								if ("10012".equals(msgType) || "10001".equals(msgType) || "10002".equals(msgType)) {
									return;
								}
							} catch (JSONException e1) {
								msgType = "10015";
							}
						}
						if (msgType != null) {
							//msgType = "offlinepush." + msgType;
							String pushCont = LocaleUtils.getLocalizedString("offlinepush.10015", "offlinepush");
							if (!"10000".equals(msgType)) {
								msgType = "offlinepush." + msgType;
								pushCont = LocaleUtils.getLocalizedString(msgType, "offlinepush");
							}
							else {
								pushCont = LocaleUtils.getLocalizedString("offlinepush.10000", "offlinepush");
								String cont = LocaleUtils.getLocalizedString("offlinepush.other", "offlinepush");;
								String mtype  = "";
								try {
									mtype = jb.getString("mtype");
								} catch (JSONException e) {
									try {
										mtype = jb.getInt("mtype") + "";
									} catch (JSONException e1) {
										msgType = "10015";
									}
								}
								if ("0".equals(mtype)) {
									try {
										cont = jb.getString("Cnt");
										if (cont.length() > 20) {
											cont = cont.substring(0, 20);
											cont += "...";
										}

									} catch (JSONException e) {
									}

								}
								else if ("1".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.image", "offlinepush");
								}
								else if ("2".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.audio", "offlinepush");
								}
								else if ("4".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.file", "offlinepush");
								}
								else if ("3".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.location", "offlinepush");
								}
								else if ("6".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.video", "offlinepush");
								}
								pushCont += cont;
							}
							pushOfflineMsg(uuid, pushCont, jid);
						}
					}
				}
			}
		}

	}

	private void pushOfflineMsg(String token, String pushCont, JID jid) {
			NotificationThreads work = null;
        try {

        	Integer size = count.get(jid.getNode()) + 1;
        	if (size <= 1000)
        		count.put(jid.getNode(), size);
        	List<PayloadPerDevice> list = new ArrayList<PayloadPerDevice>();
        	PushNotificationPayload payload = new PushNotificationPayload();
			payload.addAlert(pushCont);
			payload.addSound("default");
	        payload.addBadge(size);
	        payload.addCustomDictionary("jid", jid.toString());
	        PayloadPerDevice pay = new PayloadPerDevice(payload, token);
	        list.add(pay);
	        work = new NotificationThreads(appleServer,list,1);
	        work.setListener(DEBUGGING_PROGRESS_LISTENER);
	        work.start();
		} catch (JSONException e) {
			Log.error("JSONException:" + e.getMessage());
		} catch (InvalidDeviceTokenFormatException e) {
			Log.error("InvalidDeviceTokenFormatException:" + e.getMessage());
		}finally{
			work.destroy();
			Log.info("push to apple: username: " + jid.getNode() + " ,context" + pushCont);
		}
	}

	public Runnable createTask(final String token, final String msgType, final JID jid) {
		return new Runnable() {
			@Override
			public void run() {
				PushNotificationPayload payload = new PushNotificationPayload();
		        try {
		        	String pushCont = LocaleUtils.getLocalizedString(msgType, "offlinepush");
		        	List<PayloadPerDevice> list = new ArrayList<PayloadPerDevice>();
					payload.addAlert(pushCont);
					payload.addSound("default");
			        payload.addBadge(1);
			        payload.addCustomDictionary("jid", jid.toString());
			        PayloadPerDevice pay = new PayloadPerDevice(payload, token);
			        list.add(pay);
			        NotificationThreads work = new NotificationThreads(appleServer,list,1);
			        work.setListener(DEBUGGING_PROGRESS_LISTENER);
			        work.start();
				} catch (JSONException e) {
					Log.error("JSONException:" + e.getMessage());
				} catch (InvalidDeviceTokenFormatException e) {
					Log.error("InvalidDeviceTokenFormatException:" + e.getMessage());
				}
			}
		};

	}

	public static final NotificationProgressListener DEBUGGING_PROGRESS_LISTENER = new NotificationProgressListener() {
        public void eventThreadStarted(NotificationThread notificationThread) {
            System.out.println("   [EVENT]: thread #" + notificationThread.getThreadNumber() + " started with " + " devices beginning at message id #" + notificationThread.getFirstMessageIdentifier());
        }
        public void eventThreadFinished(NotificationThread thread) {
            System.out.println("   [EVENT]: thread #" + thread.getThreadNumber() + " finished: pushed messages #" + thread.getFirstMessageIdentifier() + " to " + thread.getLastMessageIdentifier() + " toward "+ " devices");
        }
        public void eventConnectionRestarted(NotificationThread thread) {
            System.out.println("   [EVENT]: connection restarted in thread #" + thread.getThreadNumber() + " because it reached " + thread.getMaxNotificationsPerConnection() + " notifications per connection");
        }
        public void eventAllThreadsStarted(NotificationThreads notificationThreads) {
            System.out.println("   [EVENT]: all threads started: " + notificationThreads.getThreads().size());
        }
        public void eventAllThreadsFinished(NotificationThreads notificationThreads) {
            System.out.println("   [EVENT]: all threads finished: " + notificationThreads.getThreads().size());
        }
        public void eventCriticalException(NotificationThread notificationThread, Exception exception) {
            System.out.println("   [EVENT]: critical exception occurred: " + exception);
        }
     };
}

Plugin.xml

<?xml version="1.0" encoding="UTF-8"?>

<plugin>
    <class>com.....offlinepush.plugin.OfflinePushPlugin</class>
    <name>offlinepush</name>
    <description>.......</description>
    <author>huwenfeng</author>
    <version>1.5.1</version>
    <date>1/2/2014</date>
    <minServerVersion>3.7.0</minServerVersion>
</plugin>

资源文件:offlinepush_i18n_zh_CN.properties

offlinepush.10000=\u65B0\u6D88\u606F\uFF1A
offlinepush.10001=\u7528\u6237\u64CD\u4F5C
offlinepush.image=[\u56FE\u7247]
offlinepush.audio=[\u8BED\u97F3]
offlinepush.file=[\u6587\u4EF6]
offlinepush.other=[\u5176\u4ED6]
offlinepush.location=[\u4F4D\u7F6E]
offlinepush.video=[\u89C6\u9891]
......

需要的jar包。

OK啦。

注意:IOS的推送服务器有两种模式都是免费,一种是测试的还一种是正式使用的。

所以这里最好将推送服务的使用模式在OF的管理台做配置。

本人在控制台配置了三个属性值:

时间: 2024-10-17 03:06:27

OpenFire源码学习之十八:IOS离线推送的相关文章

OpenFire源码学习之十九:在openfire中使用redis插件(上)

Redis插件 介绍 Redis是目前比较流行的NO-SQL,基于K,V的数据库系统.关于它的相关操作信息,本人这里就不做重复了,相关资料可以看这个网站http://www.redis.io/(官网).http://www.redis.cn/(中文站). 这里本人想说的是,拿Redis做openfire的缓存数据库.能够大大的提高openfire连接的吞吐量.Openfire自身在本地使用Map.Cache的方式缓存了Group.roster.MUC等信息.但是当系统用户过大的时候,需要缓存的数

(转)OpenFire源码学习之十:连接管理(上)

转:http://blog.csdn.net/huwenfeng_2011/article/details/43415827 关于连接管理分为上下两部分 连接管理 在大并发环境下,连接资源 需要随着用户并发访问量的增加而增加,所以可伸缩的连接资源就是支持大访问量的关键技术.openfire系统通过增加独立部署的连接管理器程序提高并发的能力,连接管理的前端是一台负载均衡设备,它负责把用户访问分配到指定连接管理器,多台连接管理器在访问服务器.使用连接管理器后,服务器的连接池是提供给连接管理器连 接,

OpenFire源码学习之十二:HttpBind&amp;Script Syntax

HttpSessionManager 该类管理所有通过httpbing连接到openfire的议定.它是一个同步http的双向流 http://www.xmpp.org/extensions/xep-0124.html 构造方法:HttpSessionManager() 配置一个汇集执行者对异步路由传进来的数据的默认大小配置默认为60秒 注意:在默认情况下,服务支持最大254个客户端.这时候BOSH 承载着非常大的负荷,那么这就需要额外的分配一些线程池容量以供客户端及时入站点. public H

OpenFire源码学习之十五:插件开发

Plugin接口规范 插件是openfire功能的增强表现,它的主要任务: l  在XMPP协议中作为附加功能实现 l  动态修改控制管理台 l  使用openfire api作为新功能添加到服务器 Openfire里面的插件都会存放在plugins(工程目录为:src/plugins)的住目录下.使用ant工具编译后插件会打成jar包生成在target/openfire/plugins目录下.一个完整的插件应该包含以下的结构: Yourplugin/ | -plugin.xml        

(转)OpenFire源码学习之十五:插件开发

转:http://blog.csdn.net/huwenfeng_2011/article/details/43418493 Plugin接口规范 插件是openfire功能的增强表现,它的主要任务: l  在XMPP协议中作为附加功能实现 l  动态修改控制管理台 l  使用openfire api作为新功能添加到服务器 Openfire里面的插件都会存放在plugins(工程目录为:src/plugins)的住目录下.使用ant工具编译后插件会打成jar包生成在target/openfire

OpenFire源码学习之十四:插件管理

Plugin管理 Openfire把插件模块加入到容器分为以下步骤: l 通过classloader加载lib目录下载的所有jar l 通过classloader加载dir目录下的所有文件 l 定位和加载module.xml到context l 遍历jive模块实体,负荷给定的类作为一个模块,然后启动它 Openfire插件加载流程图: Openfire的插件管理类PluginManager.加载插件的方法是loadPlugin(File pluginDir)这里的参数pluginDir是插件的

(转)OpenFire源码学习之十四:插件管理

转:http://blog.csdn.net/huwenfeng_2011/article/details/43418433 Plugin管理 Openfire把插件模块加入到容器分为以下步骤: l 通过classloader加载lib目录下载的所有jar l 通过classloader加载dir目录下的所有文件 l 定位和加载module.xml到context l 遍历jive模块实体,负荷给定的类作为一个模块,然后启动它 Openfire插件加载流程图: Openfire的插件管理类Plu

OpenFire源码学习之三十二:使用Tsung测试openfire(下)

Tsung使用 由于我们只是对openfire进行测试,因此我们主要讲解利用jabber_register.xml在openfire上面注册用户,以及利用jabber.xml模拟用户登录openfire上线.离开.会话操作的压力测试. 注册用户 1.执行以下命令进入到安装目录下的examples目录: # cd /usr/local/tsung/share/doc/tsung/examples/ 2.修改jabber_register.xml文件 3将修改后的jabber_register.xm

OpenFire源码学习之二十:在openfire中使用redis插件(下)

Redis插件实现 首先来看下插件目录: RedisServicePlugin 源码清单: import java.io.File; import java.sql.Connection; import java.sql.PreparedStatement; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.jivesoftware.database.DbConnecti