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是电子名片的意思。首先,纸质名片是为了商业伙伴之间方便交换个人信息而诞生的。它里面包含姓名,电话,公司等信息。如下:
Vcard(电子名片)是互联网时代的产物,我们知道名片有一个特性,是用户想在里面写什么信息,就写什么信息。例如有的人需要写邮箱,有的人需要写QQ号。每个人都可能在名片里面写不同的信息。由于信息有可能多种多样,这对存储提出了一定的要求。
我们这里对电子名片的存储做一些思考。如果把这些信息存在数据库里面作为表的一行记录,那么很快,我们会发现,我们根本无法找出一个好的表结构,来存放这些信息。例如,现在表结构中有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中,用数据库中的ofvcard表来存放电子名片。Ofvcard表的设计如下图:
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加载,使用的,我们后续课程会详细讲解,敬请期待。
initialize函数执行后,我们似乎发现了一个漏洞。什么漏洞,我们建议您现在可以停下来,想一想。如果您在我给出答案之前,想到了,那么您的进步会更大。哈哈。
初始化完成后,如果我们改变了provider.vcard.className的值,是不是需要重新启动openfire呢?目前来看,是需要重新启动的,但是,对于一个超多用户使用的系统来说,重启会让用户在某段时间内,不能使用服务。举例来说,如果QQ停止工作5分钟,那么会有多少人抱怨,QQ这个sb,太烂了,都不能用了。
所以,对于生产系统,重启,我们必须慎重。
好了,现在我们就来解决这个需要重启的bug,使openfire不需要重启,也能更改provider.vcard.className的实现类。
方法就是:当provider.vcard.className这个属性的值变化后,重新调用initialize函数初始化VCard Manager模块。怎么监听属性的变化呢?在后续的课程中,我们会详细的讲解解,这里,由于本课的关键不在这里,篇幅又有限,我们对监听属性的变化,仅引出一个概念而已。
要让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是一个单列类,这说明了,在openfire的任何地方,都可以使用全局唯一的VCardManager类。
为了更方便的使用VCardManager类,在XMPPServer类中封装了对VCardManager的使用,代码如下:
public VCardManager getVCardManager() { return VCardManager.getInstance(); }
上面这段代码,说明,我们使用XMPPServer的getVCardManager函数获得电子名片模块。
本课讲解了VCardManager模块的使用,电子名片模块是openfire中比较简单的一个模块,但是却使用了openfire中经常被使用到的知识。例如,怎么定义一个模块、怎么使用缓存、怎么处理事件监听等。
希望通过本课的学习,使您对电子名片的设计原理有所感悟。我们不奢望您已经对openfire的设计着迷了,但是我们希望,您已经建立起来了对openfire的兴趣。因为,只有这样,后面的课程才适合您,您才能有机会成为一个高手。