第十三课:openfire对电子名片的支持,一种获得用户详细信息的国际标准

阅读:11875

1、什么是电子名片

2、电子名片在openfire中的使用

3、电子名片在openfire中的实现原理

一、前言

您是否经常遇到这样一个需求。在设计用户体系的时候,V1版本的用户体系是:每个用户需要一个用户名、邮箱、电话、备注。

papi酱作为一个2016年才出名的程序员,马上设计了一个数据库表,解决了这个问题。下面是它设计的数据库表:

随着用户越来越多,业务越来越复杂,某一时刻又出现了V2版本,V2版本的用户体系是:每个用户除了有V1版本的信息外,还应该有头像,性别,qq号,微信号。

作为一个已经有半年经验的程序员,Papi酱在数据库中有添加了几个字段。

V2版本的用户体系上线后,人越来越多,又出现了V3版本的用户体系,这个体系是:每个用户除了有V2版本的信息外,还应该职位、公司地址。

作为一个已经有9月经验的程序员,Papi酱在数据库中又有添加了几个字段。

随着版本的升级,又出现V4、V5、V6版,字段越来越多,而且并不是每个字段,所有用户都需要。现在的用户表已经有100多个字段了。怎么办,怎么办。这个问题,一直困扰这Papi酱。

直到类似电子名片一样的设计出现,Papi酱才恍然大悟,知道怎么解决这个问题了。

二、电子名片与用户详细信息

Vcard是电子名片的意思。首先,纸质名片是为了商业伙伴之间方便交换个人信息而诞生的。它里面包含姓名,电话,公司等信息。如下:

1、电子名片信息的多样性

Vcard(电子名片)是互联网时代的产物,我们知道名片有一个特性,是用户想在里面写什么信息,就写什么信息。例如有的人需要写邮箱,有的人需要写QQ号。每个人都可能在名片里面写不同的信息。由于信息有可能多种多样,这对存储提出了一定的要求。

2、如何存储电子名片

我们这里对电子名片的存储做一些思考。如果把这些信息存在数据库里面作为表的一行记录,那么很快,我们会发现,我们根本无法找出一个好的表结构,来存放这些信息。例如,现在表结构中有QQ这个字段,不久后用户想写上自己的msn,遗憾的是数据库中没有msn这个字段。那么问题来了。现在我们需要在数据库中添加msn这样一个字段。 添加msn字段后,好像问题暂时解决了。但是不久之后,新的用户,他们希望自己的名片上有头像、公司、所在国家,电话想留2个座机、公司的logo、职位等。不幸的是,我们的数据库表里面根本没有这些字段,那么我们是再添加上这些字段吗?我们可以通过添加字段的方式来解决,但是,这不是长久之计,因为用户对名片的个性化需求是永远无法穷举的。

如果,我们决定以添加字段的方式来解决名片存储的问题,那么不久后,我们会发现,我们给自己挖了一个无法填补的坑,总有一天这个表的字段会成千上万。

三、电子名片格式举例

电子名片是以xml的格式存储的,如下所示:

<vCard xmlns="vcard-temp">
    <N>
        <FAMILY>Dombiak</FAMILY>
        <GIVEN>Gaston</GIVEN>
        <MIDDLE>Maximiliano</MIDDLE>
    </N>
    <ORG>
        <ORGNAME>Jive Software</ORGNAME>
        <ORGUNIT />
    </ORG>
    <FN>Gaston Maximiliano Dombiak</FN>
    <ROLE />
    <DESC />
    <JABBERID>gato@jivesoftware.com</JABBERID>
    <userName>12011349</userName>
    <server>ss.ctbc.com.br</server>
    <URL />
    <NICKNAME>Gato</NICKNAME>
    <TITLE />
    <PHOTO>
        <TYPE>image/jpeg</TYPE>
        			<BINVAL>iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAIAAADYYG7Q.............I=</BINVAL>
    </PHOTO>
    <EMAIL>
        <WORK />
        <INTERNET />
        <PREF />
        <USERID>gaston@jivesoftware.com</USERID>
    </EMAIL>
    <EMAIL>
        <HOME />
        <INTERNET />
        <PREF />
        <USERID>gaston@jivesoftware.com</USERID>
    </EMAIL>
    <TEL>
        <PAGER />
        <WORK />
        <NUMBER />
    </TEL>
    <TEL>
        <CELL />
        <WORK />
        <NUMBER />
    </TEL>
    <TEL>
        <VOICE />
        <WORK />
        <NUMBER />
    </TEL>
    <TEL>
        <FAX />
        <WORK />
        <NUMBER />
    </TEL>
    <TEL>
        <PAGER />
        <HOME />
        <NUMBER />
    </TEL>
    <TEL>
        <CELL />
        <HOME />
        <NUMBER />
    </TEL>
    <TEL>
        <VOICE />
        <HOME />
        <NUMBER />
    </TEL>
    <TEL>
        <FAX />
        <HOME />
        <NUMBER />
    </TEL>
    <ADR>
        <WORK />
        <EXTADD />
        <PCODE>97204</PCODE>
        <REGION>Oregon</REGION>
        <STREET>317 SW Alder St Ste 500</STREET>
        <CTRY>USA</CTRY>
        <LOCALITY>Portland</LOCALITY>
    </ADR>
    <ADR>
        <HOME />
        <EXTADD />
        <PCODE />
        <REGION />
        <STREET />
        <CTRY />
        <LOCALITY />
    </ADR>
</vCard>

这是一个以xml格式存储的电子名片,其中的关键字可以任意取名字,但是也有一个规则,就是取的名字最好能一眼看懂是什么意思。例如上面的TEL,一看就知道是电话;ADR一看就知道是地址;PHOTO一看就知道是照片,注意在电子名片中,照片以图片的Base64编码后存储。

好了,学习了上面这段话,是不是感觉电子名片很简单呢?不就是以xml格式保存各种信息吗?是的,明白这一点就够了。

四、openfire怎么存储电子名片

帅哥们,我们很自然的可以想到,一个人对应一张电子名片,那么电子名片怎么存储呢?在openfire中,用数据库中的ofvcard表来存放电子名片。Ofvcard表的设计如下图:

这个表只有2个字段,分别是username表示用户名;vcard是存放电子名片的字段,它是一个文本类型,就是存放上一节讲的xml数据。 这两个字段联合起来的意思是:某个人的电子名片是什么。

五、openfire关于vcard的实现

Openfire自己实现了一套电子名片的功能,如果你觉得不满意,可以自己通过继承VCardProvider这个类,实现自己的电子名片功能。这里,我们简要的讲解一下VCardProvider这个接口。

默认情况下,openfire的电子名片是使用DefaultVCardProvider类来实现的,大家可以打开源码,看一下这个类的实现,这样您对电子名片会有更深入的解释。如果想更进一步了解,可以参看我们的中高级视频课程。里面有更详细的解释和原理分析。

六、替换默认的电子名片的实现。

如果想替换默认的电子名片的实现,那么需要更改数据库表ofproperty中的provider.vcard.className 值为您新写的电子名片类,默认情况下,这个字段的值是org.jivesoftware.openfire.vcard.DefaultVCardProvider。DefaultVCardProvider这个类是openfire默认的名片信息提供类。

七、电子名片模块

我们知道,openfire是以模块化的方式开发的。电子名片也是openfire模块之一,它是由VCardManager类实现,这是一个单例类,它的定义是:

public class VCardManager extends BasicModule implements ServerFeaturesProvider

由上面一句代码可知,VCardManager继承于BasicModule,BasicModule是openfire的基本模块类。因为VCardManager继承于BasicModule,所以,我们很容易知道VCardManager被设计成了一个openfire的模块。顺便说一下,BasicModule是模块的基类。

打开src\java\org\jivesoftware\openfire\vcard\VCardManager.java源码,首先看一下它的成员变量,代码如下(注意,本处的代码不完整,最好自己也打开VCardManager.java文件):

// 日志类,用于打印日志
private static final Logger Log = LoggerFactory.getLogger(VCardManager.class);
// 电子名片提供者类,用于获取、创建、修改电子名字的信息。
private VCardProvider provider;
// instance指向VCardManager自己,说明是一个单例类
private static VCardManager instance;
// 事件触发器,用来监听openfire系统事件
private EventHandler eventHandler;
// 缓存电子名片,每一个名片都是一个xml,所以用Element表示,这是一个xml对象。
private Cache<String, Element> vcardCache;

看了VCardManager的成员变量,我们在看一下它的构造函数,代码如下:

public VCardManager() {
    // 给模块命一个名字
	super("VCard Manager");
	String cacheName = "VCard";
	// 在缓存系统中申请一块缓存,关于缓存系统的设计实现,我们后面几节课会详细介绍,稍安勿躁。
	vcardCache = CacheFactory.createCache(cacheName);
	this.eventHandler = new EventHandler();

	// 事件监听器,当电子名片发生变化的时候,会执行VCardListener中的函数。
	VCardEventDispatcher.addListener(new VCardListener() {
		public void vCardCreated(String username, Element vCard) {
			// 如果电子名片被创建,那么将其加入缓存。
			vcardCache.put(username, vCard);
		}

		public void vCardUpdated(String username, Element vCard) {
			// 当电子名片被更新,则更新缓存。
			vcardCache.put(username, vCard);
		}

		public void vCardDeleted(String username, Element vCard) {
			// 当电子名片被删除时,删除缓存
			vcardCache.remove(username);
		}
	});
}

下面对上面代码关键几点,简要分析:

1、首先来说说vcardCache = CacheFactory.createCache(cacheName)申请的缓存。关于缓存系统的详细介绍,我们会在后续课程讲解。目前,大家仅仅只需要知道,缓存是为了加速访问使用的。

例如这里获取用户电子名片的操作需要访问数据库,访问数据库就需要读取磁盘,要知道相对于内存来说,访问磁盘是很慢的。另外,如果这个操作很频繁,就可以使用缓存来实现。

缓存系统将常用数据存储在内存或者集群中,是的,您没有看错,openfire的实现中也包括集群缓存系统,是不是很高大上。如果不通过这些技巧来实现,那么很难达到高并发。关于这些技术的详细讲解,由于篇幅,我们只能放到后面了。

2、代码中的VCardEventDispatcher是一个事件监听器,当有vcard创建、删除,修改的时候,会触发事件。这里,当事件发生的时候,对vcardCache缓存进行了操作,我相信大家一看就懂,不懂,就只有问官网下面的老师了。

八、电子名片模块的初始化

要使用VCardManager模块,必须对VCardManager模块进行初始化。电子名片作为openfire的模块,继承自BasicModule,那么可以重载initialize方法,对模块进行一些初始化。Initialize这个函数在openfire加载模块的时候,自动调用,所以,我们不用关心什么时候调用initialize函数。

VCardManager中initialize函数实现了初始化的工作,其代码如下:

@Override
public void initialize(XMPPServer server) {
	instance = this;
	// 将openfire.xml中的配置信息provider.vcard.className,合并到数据库中的ofpropery表中。直接更改数据库信息,可能是不太安全的,我们可以通过编辑openfire.xml文件,添加provider.vcard.className属性,来更改电子名片的默认实现类。
	JiveGlobals.migrateProperty("provider.vcard.className");

	// 从openfire属性中,获取电子名片的默认实现类的类名。
	String className = JiveGlobals.getProperty("provider.vcard.className",
			DefaultVCardProvider.class.getName());
	// 通过java的反射机制,实例化“电子名片”类
	try {
		Class c = ClassUtils.forName(className);
		provider = (VCardProvider) c.newInstance();
	}
	catch (Exception e) {
		Log.error("Error loading vcard provider: " + className, e);
		// 如果异常的话,就使用默认的类
		provider = new DefaultVCardProvider();
	}
}

下面对上面代码进行简要分析。

1、代码中用到了反射,反射通过ClassUtils.forName函数实现。如果您连反射都不明白是什么,那么只能说您out了,请自行百度学习一下。

2、Initialize函数中,为什么使用反射来创建自己的提供者类(DefaultVCardProvider)呢?这个问题需要好好解释一下。使用反射,完全是因为灵活性,可以在数据库或者配置文件中,修改provider.vcard.className属性的值。将provider.vcard.className的值修改为您自己实现的电子名片提供者类,您就可以在不重新编译、打包、部署openfire的情况下,动态将电子名片提供者类替换了。这个技巧非常好用,也非常强大。

3、什么时候会初始化呢?Openfire启动的时候,会逐个调用模块的初始化函数。关于模块是怎么被openfire加载,使用的,我们后续课程会详细讲解,敬请期待。

九、VCardManager模块的启动函数start

1、电子名片的bug

initialize函数执行后,我们似乎发现了一个漏洞。什么漏洞,我们建议您现在可以停下来,想一想。如果您在我给出答案之前,想到了,那么您的进步会更大。哈哈。

初始化完成后,如果我们改变了provider.vcard.className的值,是不是需要重新启动openfire呢?目前来看,是需要重新启动的,但是,对于一个超多用户使用的系统来说,重启会让用户在某段时间内,不能使用服务。举例来说,如果QQ停止工作5分钟,那么会有多少人抱怨,QQ这个sb,太烂了,都不能用了。

所以,对于生产系统,重启,我们必须慎重。

好了,现在我们就来解决这个需要重启的bug,使openfire不需要重启,也能更改provider.vcard.className的实现类。

方法就是:当provider.vcard.className这个属性的值变化后,重新调用initialize函数初始化VCard Manager模块。怎么监听属性的变化呢?在后续的课程中,我们会详细的讲解解,这里,由于本课的关键不在这里,篇幅又有限,我们对监听属性的变化,仅引出一个概念而已。

2、电子名片管理类监听属性的变化

要让VCardManager类能监听属性的变化,我们需要看一下start函数。start函数解决了我们刚才分析的bug。

当模块初始化完成后,会调用模块的start函数,start函数的源码如下:

@Override
public void start() {
// UserEventDispatcher是用户监听器,当用户添加、删除,修改的时候,会通知它的参数eventHandler。其实这里的意思是,如果我们数据库中的电子名片可以修改,那么当用户删除的时候,可以告诉这里的eventHandler类,eventHandler类可以在这时候删除用户的电子名片。毕竟用户都不在的,电子名片留着也没用。正所谓,皮之不存毛将焉附。
	if (!provider.isReadOnly()) {
		UserEventDispatcher.addListener(eventHandler);
	}

	// 这里是一个属性监听器,当provider.vcard.className属性变化的时候,会回调这里的函数。
	PropertyEventListener propListener = new PropertyEventListener() {
		public void propertySet(String property, Map params) {
			if ("provider.vcard.className".equals(property)) {
				initialize(XMPPServer.getInstance());
			}
		}

		public void propertyDeleted(String property, Map params) {
			//Ignore
		}

		public void xmlPropertySet(String property, Map params) {
			//Ignore
		}

		public void xmlPropertyDeleted(String property, Map params) {
			//Ignore
		}
	};
//  将这里定义的属性监听器加入属性监听列表中。
	PropertyEventDispatcher.addListener(propListener);
}

Ok,start函数的大部分解释都在注释里面了,希望您能看懂。看不懂也没关系,择良日再看看。

十、获得电子名片的某一个属性

经过上面的步骤之后,电子名片的功能就能够使用了。要获得某一个用户的电子名片中的某一个属性,调用VCardManager的getVCardProperty函数,这个函数的原型是:

public String getVCardProperty(String username, String name)

第一个参数是openfire的用户名,第二个参数是这个用户电子名片中的某一个属性。如果不存在这个属性,那么返回null。注意第二个参数name,从源码中可以看出,如果要取EMAIL中的USERID属性,那么name的取值应该是”EMAIL:USERID”,获取xml中的子节点,中间需要加“:”。

下面我们来看看getVCardProperty的源码:

public String getVCardProperty(String username, String name) {
	String answer = null;
	// getOrLoadVCard要么从缓存中,要么从数据库ofvcard中获得用户的电子名片信息,以xml元素的方式返回。
	Element vCardElement = getOrLoadVCard(username);
	if (vCardElement != null) {
		// A vCard was found for this user so now look for the correct element
		Element subElement = null;
        // 用词法分析器,将name变为一个个的单词。
		StringTokenizer tokenizer = new StringTokenizer(name, ":");
		// 循环到指定的xml路径,并获得最后一个子路径的Element元素。
		while (tokenizer.hasMoreTokens()) {
			String tok = tokenizer.nextToken();
			if (subElement == null) {
				subElement = vCardElement.element(tok);
			}
			else {
				subElement = subElement.element(tok);
			}
			if (subElement == null) {
				break;
			}
		}
		// 获取元素中的值,trim时候去掉左右空格的意思。
		if (subElement != null) {
			answer = subElement.getTextTrim();
		}
	}
	// 如果没有找到,返回null
	return answer;
}

从源码中,我们可以看出,getVCardProperty函数就是获取电子名片中某个属性的值。

十一、删除某个人的电子名片

如果一个人不再需要电子名片,也可以将其删除。我们深入一点思考,为什么要删除电子名片呢?其实很简单,电子名片是和用户关联的,例如当用户删除的时候,就可以删除电子名片了。

使用VCardManager的deleteVCard函数删除电子名片,该函数的原型是:

public void deleteVCard(String username) 

参数username表示,需要删除的那个人的名字。deleteVCard函数的代码如下:

public void deleteVCard(String username) {
	if (provider.isReadOnly()) {
	    // 如果不允许删除,则抛出异常。
		throw new UnsupportedOperationException("VCard provider is read-only.");
	}
	// 从缓存中或者数据库中取电子名片
	Element oldVCard = getOrLoadVCard(username);
	if (oldVCard != null) {
	    // 从缓存中移出
		vcardCache.remove(username);
		// 从数据库中删除
		provider.deleteVCard(username);
		// 告诉监听器,有人的电子名片被删除了。
		VCardEventDispatcher.dispatchVCardDeleted(username, oldVCard);
	}
}

上面的代码,关于VCardEventDispatcher事件分发器,我们还有话要说。VCardEventDispatcher使用了设计模式中,非常流行的观察者模式,如果您对观察者模式不了解,那么百度一下吧。

十二、设置用户的电子名片

以给某个人设置一个电子名片,使用函数setVCard,它的原型是:

public void setVCard(String username, Element vCardElement) throws Exception

参数一就不用说了吧,说多了,说我啰嗦。

参数二vCardElement是一个org.dom4j.Element类型,其实就是表示xml节点的数据类型。我们电子名片是用xml来表示的,所以,这里是直接给一个xml,它会被存储到数据库中。

侯捷有句话,“源码面前,了无秘密”,让我们看一下setVCard的源码吧:

public void setVCard(String username, Element vCardElement) throws Exception {
	boolean created = false;
	boolean updated = false;
	
	// 如果只读,就不能设置,抛出不支持异常
	if (provider.isReadOnly()) {
		throw new UnsupportedOperationException("VCard provider is read-only.");
	}
	// 获取以前的电子名片
	Element oldVCard = getOrLoadVCard(username);
	Element newvCard = null;
	
	if (oldVCard != null) {
		// 更新或者创建电子名片
		if (!oldVCard.equals(vCardElement)) {
			try {
				newvCard = provider.updateVCard(username, vCardElement);
				vcardCache.put(username, newvCard);
				updated = true;
			}
			catch (NotFoundException e) {
				Log.warn("Tried to update a vCard that does not exist", e);
				newvCard = provider.createVCard(username, vCardElement);
				vcardCache.put(username, newvCard);
				created = true;
			}
		}
	}
	// 如果以前没有电子名片,那么插入一个
	else {
		try {
			newvCard = provider.createVCard(username, vCardElement);
			vcardCache.put(username, newvCard);
			created = true;
		}
		catch (AlreadyExistsException e) {
			Log.warn("Tried to create a vCard when one already exist", e);
			newvCard = provider.updateVCard(username, vCardElement);
			vcardCache.put(username, newvCard);
			updated = true;
		}
	}
	if (created) {
		// 新建的时候,触发新建的事件
		VCardEventDispatcher.dispatchVCardCreated(username, newvCard);
	} else if (updated) {
		// 更新的时候,触发更新的时间
		VCardEventDispatcher.dispatchVCardUpdated(username, newvCard);
	}
}

关于电子名片的设置,大家看一下源码,应该不难理解,这里就不多说了。

十三、怎么使用VCardManager模块

我们前面说了,VCardManager是一个单列类,这说明了,在openfire的任何地方,都可以使用全局唯一的VCardManager类。

为了更方便的使用VCardManager类,在XMPPServer类中封装了对VCardManager的使用,代码如下:

public VCardManager getVCardManager() {
	return VCardManager.getInstance();
}

上面这段代码,说明,我们使用XMPPServer的getVCardManager函数获得电子名片模块。

十四、小结

本课讲解了VCardManager模块的使用,电子名片模块是openfire中比较简单的一个模块,但是却使用了openfire中经常被使用到的知识。例如,怎么定义一个模块、怎么使用缓存、怎么处理事件监听等。

希望通过本课的学习,使您对电子名片的设计原理有所感悟。我们不奢望您已经对openfire的设计着迷了,但是我们希望,您已经建立起来了对openfire的兴趣。因为,只有这样,后面的课程才适合您,您才能有机会成为一个高手。

[1楼] wang** 2018-12-19 17:25

[2楼] liub** 2021-10-12 11:10

提问或评论

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