1、学习openfire的属性系统
2、学习openfire的属性系统的设计
3、学习属性的读取、设置
4、学习属性的迁移
要一个系统稳定并灵活的运行,需要很多属性。我们以网站为例,需要数据库的地址和密码,网站才能连接数据库。需要邮件服务器的地址,密码,网站才能发送邮件。这些信息可以直接写死在程序里,不过,如果您那么做了,我会笑您太天真。
因为,您在为自己挖坑,然后把自己埋了。没有一个程序上线后是一成不变的。一旦数据库地址和密码改了,因为配置信息被您写死在程序中,您需要重新编译、测试系统,重新上线。毫无疑问,兄弟我多年的经验告诉我,这需要花费的很多很多的时间,是您我无法想象的。 所以,我们不应该天真的把这些重要的属性写到程序里。
现在问题来了,我们应该怎么存储属性值。我这里告诉您三种方式,数据库、文件和zookeeper。
首先,您可以将属性以键值对的形式存储在数据库中,如下图:
这是openfire的ofproperty表。name是关键字,propValue是属性值。
我们这里多说一些与数据库相关的知识。数据库有很多种,关系型数据库、非关系型数据库,你都可以使用。大家灵活选择,不要死板的认为我们这里说的数据库就仅仅是关系型数据库。
另外,属性可以直接存储在文件中,存储的格式可以自己定义,也可以使用常用的ini格式,json、xml都是可以的。
昨天有个同学问我ini是什么格式,我当时有点生气,就算不知道,百度一下也应该知道啊。不要这样“无知者无过的态度”啊,这里一个学习计算机高级知识的网站,不是计算机导论一样的课程,某些东西不懂,要学会自己百度,google啊。好了,发牢骚完了。:)
好了,最后,我们还是列出ini文件的样子如下:
[xcache-common] zend_extension = /usr/local/php/lib/php/extensions/no-debug-non-zts-20060613/xcache.so [xcache.admin] xcache.admin.auth = On xcache.admin.user = "mOo"
zookeeper主要应用于大型分布式系统中,举例来说,有500台服务器,同时依赖一个属性变量,怎么才能让500台机器几乎同时知道一个属性变量的变化呢?答案是使用zookeeper,zookeeper可以在属性变化时,第一时间通知500台机器,你们要的某个数据发生变化了。这个机制对集群来说是相当有用的。集群可以以最快的速度切换到新属性上运行。
例如集群访问的某台数据库坏了,就可以用zookeeper改变数据库的地址,集群中的所有机器第一时间知道了新的数据库地址,集群恢复正常。哈哈,当然,在现实生活中,架构师们不会这样设计集群,一个健壮的集群,肯定不会因为一台数据库坏了,就改变集群中的机器的数据库地址的。
ok,关于zookeeper的更多信息,请大家自己查阅资料吧,目前,openfire并没有使用到zookeeper。
上面讲了三种属性存储方式,其中,openfire采用了2种方式存放系统属性。第一钟是xml文件,第二钟是数据库中的ofproperty表。
Xml文件举例如下图:
<?xml version="1.0" encoding="UTF-8"?> <jive> <adminConsole> <!-- Disable either port by setting the value to -1 --> <port>9090</port> <securePort>9091</securePort> </adminConsole> <locale>zh_CN</locale> <setup>true</setup> <clustering> <enabled>true</enabled> </clustering> </jive>
Ofproperty表如下图:
openfire对属性的操作用JiveGlobals类来实现。Jive只是开发openfire这个组织的名字而已,除此没有其他意思。
*属性初始化
*加载语言文件,openfire支持多语言系统,例如英文、中文、德文。
*设置、获得系统的时区
*设置、获取配置文件的路径
*设置属性到xml文件中,从xml文件读取属性、删除xml文件中的属性
*获取所有属性的名字
*迁移属性,将xml中的属性放到数据库中。
*对敏感属性进行加密存储
*可以自己指定属性加密算法。
下面,我们围绕着几个属性类的功能来讲解一下它的设计思路。
在openfire的启动过程中,基本上,可以说是在openfire启动的最早期。就会去读取conf目录下的openfire.xml文件。这个文件存放了openfire最基本的信息,例如管理控制台的端口号9090,openfire控制台显示的语言,是英文还是中文等。 一个最简单的openfire.xml文件如下,可以在其中按照xml的格式任意的添加属性。Openfire.xml在系统编译后的conf目录下。
<?xml version="1.0" encoding="UTF-8"?> <!-- This file stores bootstrap properties needed by Openfire. Property names must be in the format: "prop.name.is.blah=value" That will be stored as: <prop> <name> <is> <blah>value</blah> </is> </name> </prop> Most properties are stored in the Openfire database. A property viewer and editor is included in the admin console. --> <!-- root element, all properties must be under this element --> <jive> <adminConsole> <!-- Disable either port by setting the value to -1 --> <port>9090</port> <securePort>9091</securePort> </adminConsole> <locale>en</locale> --> </jive>
JiveGlobals中最早被执行的函数是setHomeDirectory和setConfigName。分别设置openfire的home路径和配置文件的名字。代码如下:
JiveGlobals.setHomeDirectory(openfireHome.toString()); //除此之外,还会设置配置文件的名字为conf\openfire.xml JiveGlobals.setConfigName(jiveConfigName);
把一个复杂的功能设计得简单,一直是架构师的追求。openfire的设计者,也追求简单,JiveGlobals的设计就非常简单。
经过调用上面的setHomeDirectory、setConfigName函数后,属性系统就初始化好了,现在属性系统就可以任意的读取属性信息了。
在openfire启动的过程中,第一个被读取的属性是setup,表示openfire是否已经经过第一次安装过程。
JiveGlobals.getXMLProperty("setup")在openfire启动后不久就被调用。
getXMLProperty函数是读取xml文件中的属性的意思,我们来看一下它的源码,如下:
public static String getXMLProperty(String name) { if (openfireProperties == null) { loadOpenfireProperties(); } return openfireProperties.getProperty(name); }
当openfireProperties为null的时候,调用loadOpenfireProperties去加载属性。否则直接返回openfireProperties中的属性。openfireProperties中的属性在内存中存放着。
loadOpenfireProperties函数试图从conf\openfire.xml中获得属性。将openfire.xml中的属性转换为一个xml属性对象XMLProperties,并存到openfireProperties中。以后要获得openfire.xml中的属性,就从openfireProperties中获取了。
loadOpenfireProperties函数代码如下:
private synchronized static void loadOpenfireProperties() { if (openfireProperties == null) { // 如果openfire的home目录都没有设置,那么会返回错误。 if (home == null && !failedLoading) { failedLoading = true; StringBuilder msg = new StringBuilder(); msg.append("Critical Error! The home directory has not been configured, \n"); msg.append("which will prevent the application from working correctly.\n\n"); System.err.println(msg.toString()); } // 读取openfire.xml文件 else { try { openfireProperties = new XMLProperties(home + File.separator + getConfigName()); } catch (IOException ioe) { Log.error(ioe.getMessage()); failedLoading = true; } } // create a default/empty XML properties set (helpful for unit testing) if (openfireProperties == null) { try { openfireProperties = new XMLProperties(); } catch (IOException e) { Log.error("Failed to setup default openfire properties", e); } } } }
openfire的属性可以保存在openfire.xml文件或者数据库的ofproperty表中。如果同一个属性保存在2个地方,始终不是很好,最好有一个地方统一保存。
在openfire.xml文件和ofproperty表中进行选择,谁作为最后的保存基地呢?这个不像选择两个女朋友一样难选择。我们选择数据库表ofproperty作为参数的最后保存基地。
因为,在升级openfire的过程中,我们可能将openfire.xml文件覆盖掉,但是数据库里面的数据却很难弄掉。另外,读取数据库比读取文件稍微高效一点,所以最后,数据库获胜。
知道了最后保存的地方,那么就有一个事情要做,当openfire.xml中添加了一个新属性,是不是需要在访问这个新属性的时候,将它同时保存到数据库中呢?这里可能要补充一下,为什么要在openfire.xml中添加一个新属性,其实这个很简单,当我们开发新的openfire插件的时候,不可避免有时候需要用到新的配置属性,这时候,我们可以写到openfire.xml中。
完成迁移这个操作的函数是migrateProperty,它负责将openfire.xml中的属性迁移到数据库中。migrateProperty代码如下:
public static void migrateProperty(String name) { // 如果是第一次安装openfire,那么什么也做,安装都没完成,数据库中都没有ofproperty表,还做什么呢? if (isSetupMode()) { return; } // 如果没有加载过openfire.xml文件,那么先加载 if (openfireProperties == null) { loadOpenfireProperties(); } // 迁移 openfireProperties.migrateProperty(name); }
上面的大部分代码有注释,也是非常利于理解的,如果不理解,请打开您的eclipse,然后搜索migrateProperty,找到这份代码,倒一杯咖啡,好好看一下。
migrateProperty函数最后调用了openfireProperties.migrateProperty这句代码,这是真正的迁移函数,源码如下:
public void migrateProperty(String name) { // openfire.xml中有name属性,就迁移 if (getProperty(name) != null) { if (JiveGlobals.getProperty(name) == null) { // 迁移到数据库,并存放在内存中 Log.debug("JiveGlobals: Migrating XML property '"+name+"' into database."); JiveGlobals.setProperty(name, getProperty(name)); // 删除openfire.xml中的属性 deleteProperty(name); } else if (JiveGlobals.getProperty(name).equals(getProperty(name))) { Log.debug("JiveGlobals: Deleting duplicate XML property '"+name+"' that is already in database."); deleteProperty(name); } else if (!JiveGlobals.getProperty(name).equals(getProperty(name))) { Log.warn("XML Property '"+name+"' differs from what is stored in the database. Please make property changes in the database instead of the configuration file."); } } }
这份代码主要的意思是,如果配置文件中,没有名字为name的属性,那么就不需要迁移到数据库表中。
如果JiveGlobals.getProperty中没有name这个属性,注意JiveGlobals.getProperty中是系统运行时候的属性,如果您现在手动修改openfire.xml,在openfire.xml中添加一个属性,那么JiveGlobals.getProperty中是没有这个属性的。JiveGlobals.getProperty中的属性和数据库中的属性一一对应。
有点跑题了,我们重新看一下。如果JiveGlobals.getProperty中没有name这个属性,那么就将openfire.xml中的属性插入数据库中。这时,数据库和openfire.xml中都有同样的属性了。代码会删除openfire.xml中的属性,也就是调用deleteProperty函数。
另一种情况是 JiveGlobals.getProperty中的属性和openfire.xml中的属性值相等。这时候,说明已经迁移过数据了,只需要删除openfire.xml中的属性即可。
最后一种情况是JiveGlobals.getProperty中的属性和openfire.xml中的属性值不相等,这时,数据库中的属性优先级大于openfire.xml中的属性,我们当openfire.xml中的属性无效。不过代码中,通过这句话Log.warn("XML Property '"+name+"' differs from what is stored in the database. Please make property changes in the database instead of the configuration file.");给出了警告,这时,最好手动删除openfire.xml中的相关属性。
属性迁移的最大用处体现在运维的过程中,例如我们在新插件中使用了一个属性“update.lastCheck”,这个属性用来存储插件最后更新的时间。
为了让这个插件和这个属性生效,我们会做以如下步骤来做一些事情:
1、上传插件到openfire
2、向数据库中写update.lastCheck的值,但是问题来了,openfire无法获得这个值。但是还有一种方式,向openfire.xml中写update.lastCheck的值,然后插件用到这个值的时候,会先从openfire.xml中将update.lastCheck迁移到数据库中,然后再放在JiveGlobals中永久保存。这样做的好处是,不用直接操作数据库,直接更改openfire.xml配置文件就可以了。更改配置文件,显然比直接操作数据库安全。
从安全、易操作性讲,这种方式是非常优秀的,大家在设计其他系统的时候,也可以参考这种方式。
除了从xml中获取属性,还可以直接从JiveGlobals对象实例中直接获取某个属性。这个功能使用getProperty函数实现,其原型如下:
public static String getProperty(String name) { if (properties == null) { if (isSetupMode()) { return null; } properties = JiveProperties.getInstance(); } return properties.get(name); }
下面对代码进行详细解释,properties的类型是JiveProperties,它是一个存放属性的数据结构。其原型如下:
public class JiveProperties implements Map<String, String>
从原型,我们可以看出,它是一个hash表,key是属性名,value是属性值。
JiveProperties是一个单例类,它初始化的时候会调用init函数,进行初始化。现在,您不妨打开JiveProperties这个类,看一下它的init函数,代码如下:
public void init() { if (properties == null) { properties = new ConcurrentHashMap<String, String>(); } else { properties.clear(); } loadProperties(); }
这个函数会从数据库中将属性保存到properties中。loadProperties就是从数据库中加载属性值。
有了getProperty函数,其实我们就可以获取属性了。意外的彩蛋时候,JiveGlobals为大家考虑得很周到。除了获取字符串值,还可以直接获取int、long、boolean值。分别使用getIntProperty、getLongProperty、getBooleanProperty函数。为什么,没有直接获取double的函数,我想是,因为,至今为止还没有在ofproperty表中存取浮点型的需求吧。哈哈
我们打开数据库的ofproperty表,就能够看到所有系统用到的属性。例如下面的属性。
这幅图中database.defaultProvider.username和database.defaultProvider.password都是加密的。也许有同学觉得,没有必要加密,这其实是不对的,对于一些关键数据,还是应该加密的,例如用户名和密码。
也许你会说,首先不是每个人能进入数据库啊,还加什么密呢?是的,不是每一个人都能进入数据库,但是,一旦进入数据库,就能够知道用户名和密码这些信息始终是不安全的。
所以,我们建议大家对关键信息都进行加密。这里我们确实应该好好学习一下openfire的设计者,openfire本身的核心任务应该是xmpp协议的转发,关于配置文件的加密,其实完全可以不用开发,但是他们开发了,这一定是一个加分的项目。
如果您要加密属性,只需要在控制台,点击“加密按钮”就可以了,如下图。
当然,最终的加密算法是很简单的,使用任何一种“加密后可以解密的算法”就可以了。
关于属性,我们很可能遇到这样一个场景。openfire中有很多类使用到了一个属性变量。当这个属性变化时,我们不可能重启系统,让所有类都重新读一下JiveGlobal中的属性值。这时候,就需要使用属性事件通知的方式。
JiveProperties的put函数中,实现了上面讲解的事件通知。当属性变化的时候,会主动向关注这个属性变化的类发出通知,告诉他们属性变化了。Put函数代码如下:
public String put(String key, String value) { if (value == null) { // This is the same as deleting, so remove it. return remove(key); } if (key == null) { throw new NullPointerException("Key cannot be null. Key=" + key + ", value=" + value); } if (key.endsWith(".")) { key = key.substring(0, key.length()-1); } key = key.trim(); String result; synchronized (this) { if (properties.containsKey(key)) { if (!properties.get(key).equals(value)) { updateProperty(key, value); } } else { insertProperty(key, value); } result = properties.put(key, value); } // Generate event. Map<String, Object> params = new HashMap<String, Object>(); params.put("value", value); PropertyEventDispatcher.dispatchEvent(key, PropertyEventDispatcher.EventType.property_set, params); // Send update to other cluster members. CacheFactory.doClusterTask(PropertyClusterEventTask.createPutTask(key, value)); return result; }
代码中, PropertyEventDispatcher.dispatchEvent(key, PropertyEventDispatcher.EventType.property_set, params);这一行就是发送通知事件给需要知道属性变化的类。
更为神奇的是,openfire的设计者们也考虑到了集群的情况。集群的时候,有可能各个集群中的节点,使用同样的属性,一个属性变化了,就应该立刻通知其他节点。代码CacheFactory.doClusterTask(PropertyClusterEventTask.createPutTask(key, value));创建了一个任务,告诉其他节点,有数据需要更新了。
由于篇幅和时间原因,我们就不多说了,其实后面的中高级课程,关于这些有很多详细的、深入代码原理的分析介绍。感谢大家阅读。
openfire中属性系统是非常重要的。它作为openfire的粘合剂被openfire的各个模块使用。使用方式也非常简单,仅仅是调用JiveGlobals中的几个get和set方法就可以了,希望本课对您了解openfire的实现有所帮助,我们期待下节课再见。