第十一课:openfire的属性模块-为灵活配置而生

阅读:10223

1、学习openfire的属性系统

2、学习openfire的属性系统的设计

3、学习属性的读取、设置

4、学习属性的迁移

一、前言:为灵活性编程

要一个系统稳定并灵活的运行,需要很多属性。我们以网站为例,需要数据库的地址和密码,网站才能连接数据库。需要邮件服务器的地址,密码,网站才能发送邮件。这些信息可以直接写死在程序里,不过,如果您那么做了,我会笑您太天真。

因为,您在为自己挖坑,然后把自己埋了。没有一个程序上线后是一成不变的。一旦数据库地址和密码改了,因为配置信息被您写死在程序中,您需要重新编译、测试系统,重新上线。毫无疑问,兄弟我多年的经验告诉我,这需要花费的很多很多的时间,是您我无法想象的。 所以,我们不应该天真的把这些重要的属性写到程序里。

二、应该怎么存储属性值

现在问题来了,我们应该怎么存储属性值。我这里告诉您三种方式,数据库、文件和zookeeper。

1、可靠的数据库存储

首先,您可以将属性以键值对的形式存储在数据库中,如下图:

这是openfire的ofproperty表。name是关键字,propValue是属性值。

我们这里多说一些与数据库相关的知识。数据库有很多种,关系型数据库、非关系型数据库,你都可以使用。大家灵活选择,不要死板的认为我们这里说的数据库就仅仅是关系型数据库。

2、简单传统的文件存储

另外,属性可以直接存储在文件中,存储的格式可以自己定义,也可以使用常用的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"


3、zookeeper存储

zookeeper主要应用于大型分布式系统中,举例来说,有500台服务器,同时依赖一个属性变量,怎么才能让500台机器几乎同时知道一个属性变量的变化呢?答案是使用zookeeper,zookeeper可以在属性变化时,第一时间通知500台机器,你们要的某个数据发生变化了。这个机制对集群来说是相当有用的。集群可以以最快的速度切换到新属性上运行。

例如集群访问的某台数据库坏了,就可以用zookeeper改变数据库的地址,集群中的所有机器第一时间知道了新的数据库地址,集群恢复正常。哈哈,当然,在现实生活中,架构师们不会这样设计集群,一个健壮的集群,肯定不会因为一台数据库坏了,就改变集群中的机器的数据库地址的。

ok,关于zookeeper的更多信息,请大家自己查阅资料吧,目前,openfire并没有使用到zookeeper。

三、openfire怎么存储系统属性信息

上面讲了三种属性存储方式,其中,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表如下图:

四、属性类JiveGlobals

openfire对属性的操作用JiveGlobals类来实现。Jive只是开发openfire这个组织的名字而已,除此没有其他意思。

1、JiveGlobals的几大功能

*属性初始化

*加载语言文件,openfire支持多语言系统,例如英文、中文、德文。

*设置、获得系统的时区

*设置、获取配置文件的路径

*设置属性到xml文件中,从xml文件读取属性、删除xml文件中的属性

*获取所有属性的名字

*迁移属性,将xml中的属性放到数据库中。

*对敏感属性进行加密存储

*可以自己指定属性加密算法。

下面,我们围绕着几个属性类的功能来讲解一下它的设计思路。

五、属性类JiveGlobals的初始化

在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是否已经经过第一次安装过程。

1、读取是否已经安装

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中的属性在内存中存放着。

2、loadOpenfireProperties函数详解

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);
			}            	
		}
	}
}

七、属性迁移

1、属性的最后归属地是openfire.xml,还是ofproperty数据库

openfire的属性可以保存在openfire.xml文件或者数据库的ofproperty表中。如果同一个属性保存在2个地方,始终不是很好,最好有一个地方统一保存。

在openfire.xml文件和ofproperty表中进行选择,谁作为最后的保存基地呢?这个不像选择两个女朋友一样难选择。我们选择数据库表ofproperty作为参数的最后保存基地。

因为,在升级openfire的过程中,我们可能将openfire.xml文件覆盖掉,但是数据库里面的数据却很难弄掉。另外,读取数据库比读取文件稍微高效一点,所以最后,数据库获胜。

知道了最后保存的地方,那么就有一个事情要做,当openfire.xml中添加了一个新属性,是不是需要在访问这个新属性的时候,将它同时保存到数据库中呢?这里可能要补充一下,为什么要在openfire.xml中添加一个新属性,其实这个很简单,当我们开发新的openfire插件的时候,不可避免有时候需要用到新的配置属性,这时候,我们可以写到openfire.xml中。

2、openfire.xml属性到数据库ofproperty的迁移

完成迁移这个操作的函数是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中的相关属性。

八、属性迁移migrateProperty的用处

属性迁移的最大用处体现在运维的过程中,例如我们在新插件中使用了一个属性“update.lastCheck”,这个属性用来存储插件最后更新的时间。

为了让这个插件和这个属性生效,我们会做以如下步骤来做一些事情:

1、上传插件到openfire

2、向数据库中写update.lastCheck的值,但是问题来了,openfire无法获得这个值。但是还有一种方式,向openfire.xml中写update.lastCheck的值,然后插件用到这个值的时候,会先从openfire.xml中将update.lastCheck迁移到数据库中,然后再放在JiveGlobals中永久保存。这样做的好处是,不用直接操作数据库,直接更改openfire.xml配置文件就可以了。更改配置文件,显然比直接操作数据库安全。

从安全、易操作性讲,这种方式是非常优秀的,大家在设计其他系统的时候,也可以参考这种方式。

九、直接获取属性getProperty

除了从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的实现有所帮助,我们期待下节课再见。

提问或评论

登陆后才可留言或提问哦:) 登陆 | 注册 登陆后请返回本课提问
用户名
密   码
验证码