Tigase组件

Tigase组件第一节 – 概述和基础信息

Tigase组件是一个具有jid的实体。它可以接受/处理也可以产生packet。

举一些大家都知道的组件:MUC-多人聊天室或者PubSub-内容订阅。在Tigase服务器当中,所乎所有的东西实际上都是一个组件:Session Mananger/s2c connection manager/Message Router等等。组件基于服务器配置信息被加载,新添加的组件可以运行时被服务器加载和激活。你可以简单的在配置文件中修改类名来替换一个组件。

开发组件是Tigase开发过程中的一个必不可少的部分,在过程中可以使用很多现成的API和代码,让工作变得简单。下面的文档会帮助你熟悉现有的API,让组件开发更有效率。

组件实现第一节  – 开发基础

Tigase组件的开发很简单,使用现成的API可以让你只用简单几行代码就开发出功能强大的组件。你可以在很多地方找到API描述文字。这一系列课程使用代码样例来传授如何使用尽可能简单的代码和现成API来产生预期的效果。

Even though all Tigase components are just implementations of ServerComponent interface I will keep such a low level information to necessary minimum.基于实现接口来创建组件是可行的,但不够高效。我们的目的是教会你如何使用现成的代码,只需要简单的几段代码。

这就是第一课,第一课囊括了组件实现的基础知识。我们现在创建一个组件:

import java.util.logging.Logger;
import tigase.server.AbstractMessageReceiver;
import tigase.server.Packet;
 
public class TestComponent extends AbstractMessageReceiver {
 
  private static final Logger log = Logger.getLogger(TestComponent.class.getName());
 
  @Override
  public void processPacket(Packet packet) {
    log.finest("My packet: " + packet.toString());
  }
 
}



extend AbstractMessageReceiver 抽象类唯一需要做的事情就是实现 void processPacket(Packet packet) 方法。这其实是组件处理packet的真正逻辑处理代码实现。类的名字是TestComponent

只使用这么几行代码就完成了一个全功能的tigase组件,可以被加载到Tigase服务器,可以接收并处理packet,可以输出服务发现信息,可以对管理器的ad-hoc指令做出应答,支持脚本,生成统计信息,可以被部署为外部组件等。

在为接口实现添加更多功能之前,我们先对组件进行配置,确保在服务下一次启动的时候组件能够被顺利加载。假如init.properties文件是这样的:

config-type = --gen-config-def
--debug = server
--user-db = derby
--admins = [email protected]
--user-db-uri = jdbc:derby:/Tigase/tigasedb
--virt-hosts = devel.tigase.org
--comp-name-1 = muc
--comp-class-1 = tigase.muc.MUCComponent
--comp-name-2 = pubsub
--comp-class-2 = tigase.pubsub.PubSubComponent



配置文件当中已经有了两个组件muc和pubsub,现在我们添加第三个——我们自己的新组件:在后面添加两行配置项。
--comp-name-3 = test
--comp-class-3 = TestComponent



现在需要删除etc/tigase.xml,然后重启。

查看组件是否被服务器加载有几种方法,最简单的一种是通过XMPP客户端使用管理员帐号连接到服务器,然后查看服务发现列表。如果一切正常的话,你应该看到如图所示的一个列表,组件的描述信息是“未定义的描述”,这是一个默认的描述信息,我们可以在后面来对这个信息进行修改,组件的默认JID是“[email protected]”,“devel.tigase.org”是域名,“test”是组件的名称。

Tigase组件_第1张图片

另外一个方法是借助日志来看组件是否已经被加载。养成使用日志查看系统运行信息的习惯对于tigase的开发是非常有帮助的。日志文件存放于logs文件夹,logs/tigase.log.0的内容如下,如果组件已经被加载,你会看到类似于下面的信息:

MessageRouter.setProperties() FINER: Loading and registering message receiver: test
MessageRouter.addRouter() INFO: Adding receiver: TestComponent
MessageRouter.addComponent() INFO: Adding component: TestComponent
MessageRouter.addComponent() FINER: Adding: test component to basic-conf registrator.
Configurator.componentAdded() CONFIG:  component: test



如果你的组件没有被加载,那么首先应该检查的是配置文件。也许在启动tigase服务之前忘记了删除tigase.xml文件,或者是TestComponent没有正确得被放置到类路径下。

如果一切正常,就像第一张图片展示的那样,已经可以看到刚开发的组件。在组件上面双击,会弹出一个ad-hoc(管理器脚本)命令窗口。就像下图那样,窗口里应该只有两个选择——增加和删除。

Tigase组件_第2张图片

还可以在服务发现窗口里面查看新组件的统计信息,点击新组件就可以看到,它是非常基本的packet计数器。

Tigase组件_第3张图片

正如你所看到的,简单的几行代码就已经可以开发出功能强大的组件了。现在,你也许会问,这个组件能做些什么事情吗?比如接收和处理packet。使用你最熟悉的xmpp客户端向[email protected](如果你将你的domain设置为devel.tigase.org)发送一个消息看看会发生什么,根据processPacket(…) 当中的代码,它会把发送的消息打印日志。在测试中我发送了一个标题为“test message” body体为“this is a test”的消息,日志文件应该会包含有下面的项:

TestComponent.processPacket() FINEST: My packet: to=null, from=null,
data=
  test message
  this is a test
, XMLNS=jabber:client, priority=NORMAL



我们现在确定了所有的事情都如我们预期的那样,现在我们可以在 processPacket(…) 方法里面填充一些有意义的代码了。

Tigase组件第二节 – 配置(TODO:配置API和init.properties需要链接到中文文档)

可能很难为开发组件第一步做什么下定论。不同的开发者有不同的看法,对我而言,为组建添加一种能够在运行时修改配置文件,并允许组建动态得对配置改变做出正确处理的机制是首要任务。

下面的文字讲述当配置项变化时,组件该如何处理。专门有一篇文档会提供配置API的细节描述,所以这里不会深入去探讨,在必要时仅展示部分代码。

我们把组件打印哪一种package到日志文件作为可配置项,通过这个例子展示组件如何对配置修改做出正确的处理。一共有三种类型的消息:“message”,“presence”和“iq”。要求是:哪种或哪些消息需要打印到日志文件是可配置的;可以为日志内容添加前缀,且前缀是可配置的;且打印输出功能可以配置为“正常模式”或“隐私保护模式”。(隐私保护模式状态下的日志内容会替换CData标签的内容为“CData size: NN”。)

首先为组件添加一些私有变量:
private String[] packetTypes = {"message", "presence", "iq"};
private String prependText = "My packet: ";
private boolean secureLogging = false;



配置项的格式是键值对,我们首先为“刚才提到的几个配置要求”设置“配置项的键”:
private static final String PACKET_TYPES_KEY = "packet-types";
private static final String PREPEND_TEXT_KEY = "log-prepend";
private static final String SECURE_LOGGING_KEY = "secure-logging";



维护组件的配置信息有两种方法: getDefault(…) 获取缺省配置信息, setProperties(…) 设置配置项:
@Override
public Map getDefaults(Map params) {
  Map defs = super.getDefaults(params);
  defs.put(PACKET_TYPES_KEY, packetTypes);
  defs.put(PREPEND_TEXT_KEY, prependText);
  defs.put(SECURE_LOGGING_KEY, secureLogging);
  return defs;
}
 
@Override
public void setProperties(Map props) {
  super.setProperties(props);
  packetTypes = (String[])props.get(PACKET_TYPES_KEY);
  prependText = (String)props.get(PREPEND_TEXT_KEY);
  secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
}



你不必实现getDefaults(…)方法,也不必提供缺省的配置信息;但是如果你做了,它会对你带来帮助。

首先,从开发者的角度,你不必在setProperties(…)方法当中检查配置项的值是否为null(null总是缺省值,用户也可能因为没有在配置文件当中设定配置项而产生null的结果),因为配置管理框架会对比用户的输入项和缺省配置项,并最终挑选出可用的配置项。这会让你的setProperties(…)方法变得简洁。

其次,这样做也会让管理员轻松一些。就像在下面的截图里面展示的那样,配置项有缺省参数,并且可以通过配置ad-hoc命令来进行调整。所以管理员可以使用ta的XMPP客户端在运行时维护你的组件配置信息。

Tigase组件_第4张图片

而且所有的配置项的缺省值都被保存在tigase.xml配置文件当中(储天行注:Tigase从5.x版本之后已经取消了tigase.xml配置文件)。你可以查看并修改这些项,然后看看服务器运行是否如你预期的那样。

无论你是否实现了getDefaults(…)方法,你都可以手动在tigase.xml或init.properties文件里面添加配置项。
init.properties文件的配置语法也很简单,语法的细节请参阅TODO:这里是一个链接。就像上面图片所展示的那样,配置项的键名组成为:“组件名称” + “/” + “属性键名”。可以在init.properties文件当中添加下面的内容来对配置项进行设置:

test/log-prepend="My packet: "
test/packet-types[s]=message,presence,iq
test/secure-logging[B]=true



方括号里面是属性的类型,请参考TODO:这里是一个链接来获取更多的细节。

下面是组建增加了对配置修改相关操作的processPacket(…)方法的完整代码:

import java.util.Map;
import java.util.logging.Logger;
import tigase.server.AbstractMessageReceiver;
import tigase.server.Packet;
 
public class TestComponent extends AbstractMessageReceiver {
 
  private static final Logger log =
    Logger.getLogger(TestComponent.class.getName());
 
  private static final String PACKET_TYPES_KEY = "packet-types";
  private static final String PREPEND_TEXT_KEY = "log-prepend";
  private static final String SECURE_LOGGING_KEY = "secure-logging";
 
  private String[] packetTypes = {"message", "presence", "iq"};
  private String prependText = "My packet: ";
  private boolean secureLogging = false;
 
  @Override
  public void processPacket(Packet packet) {
    for (String pType : packetTypes) {
      if (pType == packet.getElemName()) {
        log.finest(prependText + packet.toString(secureLogging));
      }
    }
  }
 
  @Override
  public Map<String, Object> getDefaults(Map<String, Object> params) {
    Map<String, Object> defs = super.getDefaults(params);
    defs.put(PACKET_TYPES_KEY, packetTypes);
    defs.put(PREPEND_TEXT_KEY, prependText);
    defs.put(SECURE_LOGGING_KEY, secureLogging);
    return defs;
  }
 
  @Override
  public void setProperties(Map<String, Object> props) {
    super.setProperties(props);
    packetTypes = (String[])props.get(PACKET_TYPES_KEY);
    // Make sure we can compare element names by reference
    // instead of String content
    for (int i = 0; i < packetTypes.length; i++) {
      packetTypes[i] = packetTypes[i].intern();
    }
    prependText = (String)props.get(PREPEND_TEXT_KEY);
    secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
  }
 
}

当然我们还可以在方法里面添加更多“有用”的代码,这只是一个样例代码。请注意在 processPacket(…) 方法中比较packet的元素名和类型是允许的。出于对内存的消耗和系统性能的影响,所有字符串的比较都通过 String.intern(…) 方法实现。

Tigase组件第三节 – 多线程

拥有多个CPU或多核CPU的电脑已经非常普及了。你也许会把像XMPP服务这样的应用部署到多核或多CPU的电脑上。但是现在你的新组件还只能在单一线程里面处理所有的packet。

如果packet的处理是复杂的运算(比如垃圾信息过滤),那么后果可能很严重:一个CPU负载为100%,而另外的几颗CPU却还在闲置。你也许非常希望所有CPU或核心都能够分摊负载,共同参与到工作当中。

Tigase API提供一种非常方便的方式让组件的processPacket(Packet packet)在多个线程中运行。int processingThreads()方法返回组件被分配到几个线程当中。缺省情况下,返回值为“1”,这是因为并不是所有的组件都能够允许多线程并发处理packet。你可以通过覆写(overwrite)processThreads方法来指定processPacket可以在几个线程里面并发执行。

如果packet的处理只是简单的CPU运算,那么你也许会想要尽可能多的线程来压榨CPU的潜力:

@Override
public int processingThreads() {
  return Runtime.getRuntime().availableProcessors();
}



如果处理中需要使用IO(网络或者是数据库),那么在添加线程数的时候可能需要考虑更多。很难准确的定义到底需要多少个线程,只能通过一些测试来获取经验值。

现在新开发的组件已经有足够多的线程了。但还有个小问题,在多数情况下多个packet的处理顺序是不能颠倒的。如果组件的processPacket(…)方法由多个线程并发处理,很有可能会出现一个后发送的消息取代了先前发送的消息首先到达目的地,特别是在第一个packet很大而第二个很小的时候。我们可以通过调整“负责packet在线程中做分发的那个方法”来避免这种情况发生。

包的分发算法很简单:

int thread_idx = hashCodeForPacket(packet) % threads_total;



所以调整的关键在于 hashCodeForPacket(…) 方法。通过覆写这个方法可以保证所有发往同一个用户的packet都是由同一个线程来进行处理的:


@Override
public int hashCodeForPacket(Packet packet) {
  if (packet.getElemTo() != null) {
    return packet.getElemTo().hashCode();
  }
  // 程序不应该运行到这里,所有的packet都必须具有一个目的地地址,但是也许垃圾过滤器也许会过滤一些奇怪的地址
  if (packet.getElemFrom() != null) {
    return packet.getElemFrom().hashCode();
  }
  // 如果程序真的运行到这一部,就应该好好检查一下到达组件的packet是否正常,然后找到一个更好的计算hashCode方法。
  return 1;
}

刚刚提到的两个方法一个可以控制组件处理packet的线程数,另一个可以控制packet分发到线程的逻辑。它们还不是Tigase API在多线程处理时提供的全部方法。

有时还会用到周期性任务。你当然可以创建一个Timer实例,并使用TimerTasks来加载它;但当很多类似的工作都需要在各个组件当中执行的时候,你会需要更多额外的资源来支撑这么多个TimerTask。Tigase提供了一些允许你重用Timer对象来执行各种任务的方法。

首先,你有三个可以执行周期性任务的方法:

public synchronized void everySecond();
public synchronized void everyMinute();
public synchronized void everyHour();

一个“周期性向某些特定地址发送通知”的功能可以这样来实现:

@Override
public synchronized void everyMinute() {
  super.everyMinute();
  if ((++delayCounter) >= notificationFrequency) {
    addOutPacket(Packet.getMessage(abuseAddress, getComponentId(),
      StanzaType.chat, "Detected spam messages: " + spamCounter,
      "Spam counter", null, newPacketId("spam-")));
    delayCounter = 0;
    spamCounter = 0;
  }
}



这个方法每分钟(也就是notificationFrequency)向“abuseAddress”发送一个通知,内容为上个周期垃圾信息过滤器拦截了多少条消息。需要注意的是:你一定需要调用super.everyMinute()方法,确保父类的语句也能够被执行,同时也要让自己添加的语句尽可能高效率快速得完成,尤其是在复写的是everySecond()方法的时候。

还有两个可以安排在特定的时间执行任务的方法,它们非常类似于java.util.Timer API,区别在于Timer可以在类/父类/爷爷类等各个层级被重用。每一个类实例都有一个独立的Timer,这样能够避免组件间的干扰:

addTimerTask(TimerTask task, long delay, TimeUnit unit);
addTimerTask(TimerTask task, long delay);

还有一个和多线程没有直接联系,但是非常有用,可以在某一个特定时间点执行任务的方法。这个时间点就是在服务器完成初始化的时候,这个时候所有的组件都已经被创建,并接收到它们各自的配置项。这时Tigase服务器会调用每一个已加载组件的void initializationComplete()方法,你可以通过覆写这个方法来执行一些任务。

下面的代码使用到了上面提到的所有API:

import java.util.Arrays;
import java.util.Map;
import java.util.logging.Logger;
import tigase.server.AbstractMessageReceiver;
import tigase.server.Packet;
import tigase.util.JIDUtils;
import tigase.xmpp.StanzaType;
 
public class TestComponent extends AbstractMessageReceiver {
 
  private static final Logger log =
    Logger.getLogger(TestComponent.class.getName());
 
  private static final String BAD_WORDS_KEY = "bad-words";
  private static final String WHITELIST_KEY = "white-list";
  private static final String PREPEND_TEXT_KEY = "log-prepend";
  private static final String SECURE_LOGGING_KEY = "secure-logging";
  private static final String ABUSE_ADDRESS_KEY = "abuse-address";
  private static final String NOTIFICATION_FREQ_KEY = "notification-freq";
 
  private String[] badWords = {"word1", "word2", "word3"};
  private String[] whiteList = {"admin@localhost"};
  private String prependText = "Spam detected: ";
  private String abuseAddress = "abuse@locahost";
  private int notificationFrequency = 10;
  private int delayCounter = 0;
  private boolean secureLogging = false;
  private long spamCounter = 0;
 
  @Override
  public void processPacket(Packet packet) {
    // 这是一个message packet吗?
    if ("message" == packet.getElemName()) {
      String from = JIDUtils.getNodeID(packet.getElemFrom());
      // 消息的发送者在白名单内吗?
      if (Arrays.binarySearch(whiteList, from) < 0) {
        // 如果ta不在白名单里面,那么检查消息的内容
        String body = packet.getElemCData("/message/body");
        if (body != null && !body.isEmpty()) {
          body = body.toLowerCase();
          for (String word : badWords) {
            if (body.contains(word)) {
              log.finest(prependText + packet.toString(secureLogging));
              ++spamCounter;
              return;
            }
          }
        }
      }
    }
    // 不是垃圾信息,返回以便做下一步处理
    Packet result = packet.swapFromTo();
    addOutPacket(result);
  }
 
  @Override
  public int processingThreads() {
    return Runtime.getRuntime().availableProcessors();
  }
 
  @Override
  public int hashCodeForPacket(Packet packet) {
    if (packet.getElemTo() != null) {
      return packet.getElemTo().hashCode();
    }
    // 程序不应该运行到这里,所有的packet都必须具有一个目的地地址,但是也许垃圾过滤器也许会过滤一些奇怪的地址
    if (packet.getElemFrom() != null) {
      return packet.getElemFrom().hashCode();
    }
    // 如果程序真的运行到这一部,就应该好好检查一下到达组件的packet是否正常,然后找到一个更好的计算hashCode方法。
    return 1;
  }
 
  @Override
  public Map<String, Object> getDefaults(Map<String, Object> params) {
    Map<String, Object> defs = super.getDefaults(params);
    defs.put(BAD_WORDS_KEY, badWords);
    defs.put(WHITELIST_KEY, whiteList);
    defs.put(PREPEND_TEXT_KEY, prependText);
    defs.put(SECURE_LOGGING_KEY, secureLogging);
    defs.put(ABUSE_ADDRESS_KEY, abuseAddress);
    defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency);
    return defs;
  }
 
  @Override
  public void setProperties(Map<String, Object> props) {
    super.setProperties(props);
    badWords = (String[])props.get(BAD_WORDS_KEY);
    whiteList = (String[])props.get(WHITELIST_KEY);
    Arrays.sort(whiteList);
    prependText = (String)props.get(PREPEND_TEXT_KEY);
    secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
    abuseAddress = (String)props.get(ABUSE_ADDRESS_KEY);
    notificationFrequency = (Integer)props.get(NOTIFICATION_FREQ_KEY);
  }
 
  @Override
  public synchronized void everyMinute() {
    super.everyMinute();
    if ((++delayCounter) >= notificationFrequency) {
      addOutPacket(Packet.getMessage(abuseAddress, getComponentId(),
        StanzaType.chat, "Detected spam messages: " + spamCounter,
        "Spam counter", null, newPacketId("spam-")));
      delayCounter = 0;
      spamCounter = 0;
    }
  }
 
}


Tigase组件第四节 – 服务发现

    

新组件在服务发现列表当中仍然显示“未定义的描述”。它也没有提供任何有趣的特性和子节点。

接下来,我们将用简单的方式修改组件的基本信息,并添加一些服务发现特性。除此之外,文档还提供一些如何在运行时添加/删除服务发现节点,如何更新已有元素的指导。

组件的描述和类别/类型可以通过覆写下面两个方法来实现:

@Override
public String getDiscoDescription() {
  return "Spam filtering";
}
 
@Override
public String getDiscoCategoryType() {
  return "spam";
}



请注意,在 服务发现标识注册表 当中,并没有定义“Spam”类别/类型。这仅仅是用来演示。在真实应用的时候请参照服务发现标识注册表当中的类别/类型来挑选一个最适合。添加完上面两个方法并重新启动服务器之后,再查看服务发现列表窗口,可能会出现下图所示的效果。

Tigase组件_第5张图片


这很简单,但事实上除了显示效果上有一些变化之外,本质上组件并没有发生任何改变。下面我们修改它的“本质”。

通过覆写方法来修改组件描述和类别/类型的一个限制就是:你不能在运行时动态得改变组件的信息。那两个方法只在setProperties(…)方法执行的时候被调用一次,之后组件的服务发现信息就被创建了。但有时在运行时动态改变服务发现信息是很有意义的,别人可以从服务发现信息中获得有用信息。

以咱们的垃圾信息过滤组件为例,看看到底能从组件发现信息当中获得多少有用信息。如果在每一次接收到一条消息的时候都调用:

updateServiceDiscoveryItem(getName(), null,
  getDiscoDescription() + ": [" +
  (++messagesCounter) + "]", true);



关于服务性能的小贴士:在有些情况下调用“updateServiceDiscoveryItem(…)方法”会产生很大的性能开销,所以一个比较好的推荐是每100条消息才调用这个方法一次,而不是每条调用一次。

updateServiceDiscoveryItem(…)方法的第一个入口参数是组件的名称,它会显示在服务发现列表的JID列里。之所以不使用JID是因为:Tigase服务器可能为多个虚拟域名提供服务,域名部分会在低层方法当中被添加,所以我们在这里只使用组件名称。第二个入口参数是服务发现节点,顶级的disco条目项应该传递为null。第三个参数是它的描述(在disco规范中实际上称之为“name”)。最后一个参数是它是否仅对管理员可见。

使用上面的方法我们还可以为组件元素添加子节点。虽然XMPP的服务发现信息本来不是用来显示计数器的,但是这个例子很好的展示了Tigase API的功能,所以接下来我们使用服务发现信息来显示计数器。这一次,第二个参数不再传null,我们传递一些有意义的文字看看会产生什么效果:

// 当组件接收一条消息之后调用下面的语句
updateServiceDiscoveryItem(getName(), "messages",
  "Messages processed: [" + (++messagesCounter) + "]", true);
// 当组件确定消息是垃圾信息的时候调用下面的语句
updateServiceDiscoveryItem(getName(), "spam", "Spam caught: [" +
  (++totalSpamCounter) + "]", true);



看看最下面的完整代码样例。之后我们向组件发送几条消息,其中一些是垃圾信息(包含垃圾信息关键字)。再打开服务发现列表窗口,就会出现下面的截图:

Tigase组件_第6张图片


依据我们的实现方式,在服务器没有接收到任何消息之前,组件的服务发现信息是不会有子节点的;只有当组件接收到消息才会调用“updateServiceDiscoveryItem(…)”方法。如果希望在服务启动之后就包含子节点,那么可以在“setProperties(…)”方法当中调用“updateServiceDiscoveryItem(…)”。

请注意,“updateServiceDiscoveryItem(…)”方法可以添加或修改服务发现信息项,如果是删除操作还有一个单独的方法:

void removeServiceDiscoveryItem(String jid,
  String node, String description)



实际上只有前两个参数比较重要:“jid”和“node”必须对应已经存在的服务发现条目。

update方法还提供两个附加的变量可以用来更好得控制服务发现条目项。可以为条目项设置不同的类型/类别,也可以让它展示一些额外的特性。其中一个比较容易理解的变量可以更新服务发现条目规范。XMPP有一个专门的规范文档来描述那些已存在并注册的特性,我们创建的垃圾信息过滤组件使用了一个未经定义的特性“垃圾过滤”。下面的文字将说明如何创建两个特性标识字符串,并把他们设置到我们的新组件当中。我们可以这样调用update方法:

updateServiceDiscoveryItem(getName(), null, getDiscoDescription(),
  true, "tigase:x:spam-filter", "tigase:x:spam-reporting");



最好在setProperties(…)方法当中调用上面这个方法,这样组件的服务发现信息可以在一开始就被设定好。我们为组件的disco信息设置了两个特性:“tigase:x:spam-filter”和“tigase:x:spam-reporting”。update方法可以接受任意多个入口参数,所以我们可以为它设置任意多个需要的特性(或者依据java规范,传递特性字符串数组)。

更新好代码之后重新启动服务器,看看服务发现信息发生了什么改变。

Tigase组件_第7张图片

最后一个功能可能对于咱们的垃圾过滤组件用处不大,但是对于类似MUC/PubSub这种设置了正确类别和类型的服务发现条目项而言还是非常有用的。下面我们为垃圾过滤组件设置“自动”类别和“垃圾过滤”类型:


updateServiceDiscoveryItem(getName(), null, getDiscoDescription(),
  "automation", "spam-filtering", true,
  "tigase:x:spam-filter", "tigase:x:spam-reporting");



当然所有的这些设置可以应用到任何一个服务发现条目当中,即使是条目子节点。下面是完整的代码样例:


import java.util.Arrays;
import java.util.Map;
import java.util.logging.Logger;
import tigase.server.AbstractMessageReceiver;
import tigase.server.Packet;
import tigase.util.JIDUtils;
import tigase.xmpp.StanzaType;
 
public class TestComponent extends AbstractMessageReceiver {
 
  private static final Logger log =
    Logger.getLogger(TestComponent.class.getName());
 
  private static final String BAD_WORDS_KEY = "bad-words";
  private static final String WHITELIST_KEY = "white-list";
  private static final String PREPEND_TEXT_KEY = "log-prepend";
  private static final String SECURE_LOGGING_KEY = "secure-logging";
  private static final String ABUSE_ADDRESS_KEY = "abuse-address";
  private static final String NOTIFICATION_FREQ_KEY = "notification-freq";
 
  private String[] badWords = {"word1", "word2", "word3"};
  private String[] whiteList = {"admin@localhost"};
  private String prependText = "Spam detected: ";
  private String abuseAddress = "abuse@locahost";
  private int notificationFrequency = 10;
  private int delayCounter = 0;
  private boolean secureLogging = false;
  private long spamCounter = 0;
  private long totalSpamCounter = 0;
  private long messagesCounter = 0;
 
  @Override
  public void processPacket(Packet packet) {
    // 这是一个message packet吗?
    if ("message" == packet.getElemName()) {
      updateServiceDiscoveryItem(getName(), "messages",
        "Messages processed: [" + (++messagesCounter) + "]", true);
      String from = JIDUtils.getNodeID(packet.getElemFrom());
      // 消息的发送者在白名单内吗?
      if (Arrays.binarySearch(whiteList, from) < 0) {
        // 如果ta不在白名单里面,那么检查消息的内容
        String body = packet.getElemCData("/message/body");
        if (body != null && !body.isEmpty()) {
          body = body.toLowerCase();
          for (String word : badWords) {
            if (body.contains(word)) {
              log.finest(prependText + packet.toString(secureLogging));
              ++spamCounter;
              updateServiceDiscoveryItem(getName(), "spam", "Spam caught: [" +
                (++totalSpamCounter) + "]", true);
              return;
            }
          }
        }
      }
    }
    // 不是垃圾信息,返回以便做下一步处理
    Packet result = packet.swapElemFromTo();
    addOutPacket(result);
  }
 
  @Override
  public int processingThreads() {
    return Runtime.getRuntime().availableProcessors();
  }
 
  @Override
  public int hashCodeForPacket(Packet packet) {
    if (packet.getElemTo() != null) {
      return packet.getElemTo().hashCode();
    }
    // 程序不应该运行到这里,所有的packet都必须具有一个目的地地址,但是也许垃圾过滤器也许会过滤一些奇怪的地址
    if (packet.getElemFrom() != null) {
      return packet.getElemFrom().hashCode();
    }
    // 如果程序真的运行到这一部,就应该好好检查一下到达组件的packet是否正常,然后找到一个更好的计算hashCode方法。
    return 1;
  }
 
  @Override
  public Map<String, Object> getDefaults(Map<String, Object> params) {
    Map<String, Object> defs = super.getDefaults(params);
    defs.put(BAD_WORDS_KEY, badWords);
    defs.put(WHITELIST_KEY, whiteList);
    defs.put(PREPEND_TEXT_KEY, prependText);
    defs.put(SECURE_LOGGING_KEY, secureLogging);
    defs.put(ABUSE_ADDRESS_KEY, abuseAddress);
    defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency);
    return defs;
  }
 
  @Override
  public void setProperties(Map<String, Object> props) {
    super.setProperties(props);
    badWords = (String[])props.get(BAD_WORDS_KEY);
    whiteList = (String[])props.get(WHITELIST_KEY);
    Arrays.sort(whiteList);
    prependText = (String)props.get(PREPEND_TEXT_KEY);
    secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
    abuseAddress = (String)props.get(ABUSE_ADDRESS_KEY);
    notificationFrequency = (Integer)props.get(NOTIFICATION_FREQ_KEY);
    updateServiceDiscoveryItem(getName(), null, getDiscoDescription(),
      "automation", "spam-filtering", true,
      "tigase:x:spam-filter", "tigase:x:spam-reporting");
  }
 
  @Override
  public synchronized void everyMinute() {
    super.everyMinute();
    if ((++delayCounter) >= notificationFrequency) {
      addOutPacket(Packet.getMessage(abuseAddress, getComponentId(),
        StanzaType.chat, "Detected spam messages: " + spamCounter,
        "Spam counter", null, newPacketId("spam-")));
      delayCounter = 0;
      spamCounter = 0;
    }
  }
 
  @Override
  public String getDiscoDescription() {
    return "Spam filtering";
  }
 
  @Override
  public String getDiscoCategoryType() {
    return "spam";
  }
 
}


Tigase组件第五节 – 统计信息

大多数情况,你都想从组件中拿到一些运行时的统计信息,并依据它们来检查组件的运行情况,或者找到可能的性能瓶颈和拥堵。所有的统计信息都可以通过XMPP的ad-hoc指令/Http/JXM获取,其中一部分也可以通过SNMP获取。作为组件的开发者,你不需要关注如何把统计信息通过上面的协议进行输出,需要做的只是准备出统计数据就行,管理员可以通过上面任意一种方式来获取这些数据。

这一节将教会你如何产生自己的统计数据,如何让产生统计数据的过程不影响系统性能。

你的组件在最初只能提供父类产生的那些统计数据,我们现在为它添加一些额外的统计项:

@Override
public void getStatistics(StatisticsList list) {
  super.getStatistics(list);
  list.add(getName(), "Spam messages found", totalSpamCounter, Level.INFO);
  list.add(getName(), "All messages processed", messagesCounter, Level.FINER);
  if (list.checkLevel(Level.FINEST)) {
    // 可以把那些非常消耗系统资源的统计数据产生代码写在下面
  }
}



代码应该很容易理解:D。首先调用“super.getStatistics(…)”方法来获取父类的统计项并更新父类的状态。StatisticsList是一个保存所有统计项的集合,它可以非常方便的对统计项进行更新和查找。实际上你不需要知道实现的所有细节,但是如果你感兴趣,可以查看源码并阅读javadoc文档。

add()方法的第一个参数是组件名称。所有的统计项都会以组件名称来进行分组,当想要看特定组件的统计数据的时候可以很容易得进行查找;第二个参数是统计项的描述;第三个是统计项的值,它可以是数字或字符串。最后一个参数最有意思,它的设计思路来自于日志框架。每一个统计项都有一个重要级别,最重要的级别为“SEVERE”,“FINEST”是最不重要的。这个参数可以用来提高系统性能,也能帮助统计数据进行分类显示。当“StatisticsList”对象被创建的时候,会被用户指定一个特定的级别。当调用add方法向其中添加一个低级别统计项的时候,这个统计项不会被添加到列表当中。这样可以节省网络带宽,提高统计数据的接收速度,也能为最终用户提供一个简单明了的展现。

Tigase组件_第8张图片


一个比较容易产生困惑的地方是,当一个值为数字“0”的统计项被添加到统计列表的时候,统计项的级别别会被强制设定为“FINEST”。这基于一个假设:管理员在大多数情况下不会关注值为0的统计项,所以除非在ta有意请求低级别的统计列表的时候,否则ta无法看到0值统计项。

“if”语句也需要一些解释。在大多数情况,添加一个统计项并不会消耗多少系统资源,所以可以直接调用add方法来进行添加。但有些时候,生成统计数据是一个耗时较长并且消耗系统资源的过程(比如需要从数据库当中读取数据并进行计算)。统计信息的收集可能非常频繁,所以每次收集全部的统计项(尤其是某些统计项的级别低于统计列表的级别)是没有意义的。在这种情况,还是建议检查一下统计项的级别是否低于统计列表的级别,如果低于可以直接跳过。

就像你所看到的,统计信息的产生和展现API是非常简单和直接的。只有一个方法需要覆写,下面是样例的完整代码:

import java.util.Arrays;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import tigase.server.AbstractMessageReceiver;
import tigase.server.Packet;
import tigase.stats.StatisticsList;
import tigase.util.JIDUtils;
import tigase.xmpp.StanzaType;
 
public class TestComponent extends AbstractMessageReceiver {
 
  private static final Logger log =
  Logger.getLogger(TestComponent.class.getName());
 
  private static final String BAD_WORDS_KEY = "bad-words";
  private static final String WHITELIST_KEY = "white-list";
  private static final String PREPEND_TEXT_KEY = "log-prepend";
  private static final String SECURE_LOGGING_KEY = "secure-logging";
  private static final String ABUSE_ADDRESS_KEY = "abuse-address";
  private static final String NOTIFICATION_FREQ_KEY = "notification-freq";
 
  private String[] badWords = {"word1", "word2", "word3"};
  private String[] whiteList = {"admin@localhost"};
  private String prependText = "Spam detected: ";
  private String abuseAddress = "abuse@locahost";
  private int notificationFrequency = 10;
  private int delayCounter = 0;
  private boolean secureLogging = false;
  private long spamCounter = 0;
  private long totalSpamCounter = 0;
  private long messagesCounter = 0;
 
  @Override
  public void processPacket(Packet packet) {
    // 这是一个message packet吗?
    if ("message" == packet.getElemName()) {
      updateServiceDiscoveryItem(getName(), "messages",
        "Messages processed: [" + (++messagesCounter) + "]", true);
      String from = JIDUtils.getNodeID(packet.getElemFrom());
      // 消息的发送者在白名单内吗?
      if (Arrays.binarySearch(whiteList, from) < 0) {
        // 如果ta不在白名单里面,那么检查消息的内容
        Stringbody = packet.getElemCData("/message/body");
        if (body != null && !body.isEmpty()) {
          body = body.toLowerCase();
          for (String word : badWords) {
            if (body.contains(word)) {
              log.finest(prependText + packet.toString(secureLogging));
              ++spamCounter;
              updateServiceDiscoveryItem(getName(), "spam", "Spam caught: [" +
                (++totalSpamCounter) + "]", true);
              return;
            }
          }
        }
      }
    }
    // 不是垃圾信息,返回以便做下一步处理
    Packet result = packet.swapElemFromTo();
    addOutPacket(result);
  }
 
  @Override
  public int processingThreads() {
    return Runtime.getRuntime().availableProcessors();
  }
 
  @Override
  public int hashCodeForPacket(Packet packet) {
    if (packet.getElemTo() != null) {
      return packet.getElemTo().hashCode();
    }
    // 程序不应该运行到这里,所有的packet都必须具有一个目的地地址,但是也许垃圾过滤器也许会过滤一些奇怪的地址
    if (packet.getElemFrom() != null) {
      return packet.getElemFrom().hashCode();
    }
    // 如果程序真的运行到这一部,就应该好好检查一下到达组件的packet是否正常,然后找到一个更好的计算hashCode方法。
    return 1;
  }
 
  @Override
  public Map<String, Object> getDefaults(Map<String, Object> params) {
    Map<String, Object> defs = super.getDefaults(params);
    defs.put(BAD_WORDS_KEY, badWords);
    defs.put(WHITELIST_KEY, whiteList);
    defs.put(PREPEND_TEXT_KEY, prependText);
    defs.put(SECURE_LOGGING_KEY, secureLogging);
    defs.put(ABUSE_ADDRESS_KEY, abuseAddress);
    defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency);
    return defs;
  }
 
  @Override
  public void setProperties(Map<String, Object> props) {
    super.setProperties(props);
    badWords = (String[])props.get(BAD_WORDS_KEY);
    whiteList = (String[])props.get(WHITELIST_KEY);
    Arrays.sort(whiteList);
    prependText = (String)props.get(PREPEND_TEXT_KEY);
    secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
    abuseAddress = (String)props.get(ABUSE_ADDRESS_KEY);
    notificationFrequency = (Integer)props.get(NOTIFICATION_FREQ_KEY);
    updateServiceDiscoveryItem(getName(), null, getDiscoDescription(),
      "automation", "spam-filtering", true,
      "tigase:x:spam-filter", "tigase:x:spam-reporting");
  }
 
  @Override
  public synchronized void everyMinute() {
    super.everyMinute();
    if ((++delayCounter) >= notificationFrequency) {
      addOutPacket(Packet.getMessage(abuseAddress, getComponentId(),
        StanzaType.chat, "Detected spam messages: " + spamCounter,
        "Spam counter", null, newPacketId("spam-")));
      delayCounter = 0;
      spamCounter = 0;
    }
  }
 
  @Override
  public String getDiscoDescription() {
    return "Spam filtering";
  }
 
  @Override
  public String getDiscoCategoryType() {
    return "spam";
  }
 
  @Override
  public void getStatistics(StatisticsList list) {
    super.getStatistics(list);
    list.add(getName(), "Spam messages found", totalSpamCounter, Level.INFO);
    list.add(getName(), "All messages processed", messagesCounter, Level.FINE);
    if (list.checkLevel(Level.FINEST)) {
      // 可以把那些非常消耗系统资源的统计数据产生代码写在下面
    }
  }
 
}


Tigase组件第六节 – 脚本支持

脚本支持是Tigase的一个基本内置API,不需要任何额外的代价就能让所有的组件都自动支持脚本。但它只能访问那些通过你的代码继承到的父类组件变量,所以你需要把你的数据传递给脚本API。这篇文档会教你如何扩展现有的脚本API来访问组件的数据结构。

组件与脚本引擎的集成只需要简单几行代码:

private static final String BAD_WORDS_VAR = "badWords";
private static final String WHITE_LIST_VAR = "whiteList";
 
@Override
public void initBindings(Bindings binds) {
  super.initBindings(binds);
  binds.put(BAD_WORDS_VAR, badWords);
  binds.put(WHITE_LIST_VAR, whiteList);
}



上面的代码传递了两个组件变量给脚本:“badWords”和“whiteList”,在脚本中变量的名称是一致的。当然也可以使用不同的名称,但一致的名称让事情变得简单和清晰易懂,所以我们在脚本中使用相同的命名。

这样就可以了,实际上,所有的事情都已经完成。在我们过去的版本中,这两个变量是java的字符串数组,所以我们只能够改变她们的元素,却不能通过脚本向数据结构添加或删除元素。这种方式不够“智慧”,为脚本的开发带来了很多限制。为了解决这个问题,我们把保存白名单和垃圾关键字的数据结构调整为“java.util.Set”。这给我们访问数据带来了很多便利也更加灵活。

因为组件已经可以和脚本API进行交互了,接下来我们演示如何通过ad-hoc指令来发送脚本,并对数据结构当中的数据进行添加或删除操作。

如果你使用Psi客户端:首先,在服务发现列表窗口当中双击“test”组件,会弹出一个包含ad-hoc命令列表的新窗口,其他客户端的展现方式也许不同。

Tigase组件_第9张图片

点击“New command Script”指令会弹出下面的窗口,你需要填写脚本描述和脚本ID。在样例中我们使用Groovy语言,但其实你可以使用更多脚本语言。

Tigase组件_第10张图片


如果想要添加更多脚本语言支持,请参考Tigase脚本文档来获得全部细节。对Tigase API而言,所有的语言都是一样的。你需要从窗口的下拉菜单中选择一个合适的语言。如果想使用的脚本语言不在下拉菜单中,那么它没有被正确的安装,所以Tigase无法检测到。

使用Groovy语言来获取当前垃圾关键字列表的代码如下:

def badw = (java.util.Set)badWords
def result = ""
for (s in badw) { result += s + "\n" }
return result



就像你在脚本中看到的那样,你需要定义一个脚本变量来引用组件中的变量,请使用正确的类型。剩下的事情就是非常简单的纯脚本工作了。执行脚本的结果如下图:

Tigase组件_第11张图片

下面的脚本允许你更新(添加/删除)垃圾关键字:


import tigase.server.Command
import tigase.server.Packet
  
def WORDS_LIST_KEY = "words-list"
def OPERATION_KEY = "operation"
def REMOVE = "Remove"
def ADD = "Add"
def OPERATIONS = [ADD, REMOVE]
  
def badw = (java.util.Set)badWords
def Packet p = (Packet)packet
def words = Command.getFieldValue(p, WORDS_LIST_KEY)
def operation = Command.getFieldValue(p, OPERATION_KEY)
  
if (words == null) {
  // No data to process, let's ask user to provide
  // a list of words 
  def res = (Packet)p.commandResult(Command.DataType.form)
  Command.addFieldValue(res, WORDS_LIST_KEY, "", "Bad words list")
  Command.addFieldValue(res, OPERATION_KEY, ADD, "Operation",
    (String[])OPERATIONS, (String[])OPERATIONS)
  return res
}
  
def words_list = words.tokenize(",")
  
if (operation == ADD) {
  words_list.each { badw.add(it.trim()) }
  return "Words have been added."
}
  
if (operation == REMOVE) {
  words_list.each { badw.remove(it.trim()) }
  return "Words have been removed."
}
  
return "Unknown operation: " + operation



学习这两个脚本只是开始。脚本应用的空间是非常广泛,现在我们仅仅为脚本添加了很少的几行代码,未来你可以借助脚本在运行时扩展你的应用,为它添加各种各样的功能;你也可以重新加载脚本,添加/修改或删除你需要的功能。不需要重启服务器,也不需要重新编译代码,更可以使用任何你希望使用的脚本语言。

当然了白名单的操作其实和垃圾关键字的操作是完全一样的,这里不再多讲了。

下面是我们在文章一开始提到的把字符串数组调整为Set的完整代码:

import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.script.Bindings;
import tigase.server.AbstractMessageReceiver;
import tigase.server.Packet;
import tigase.stats.StatisticsList;
import tigase.util.JIDUtils;
import tigase.xmpp.StanzaType;
  
public class TestComponent extends AbstractMessageReceiver {
  
  private static final Logger log =
    Logger.getLogger(TestComponent.class.getName());
  
  private static final String BAD_WORDS_KEY = "bad-words";
  private static final String WHITELIST_KEY = "white-list";
  private static final String PREPEND_TEXT_KEY = "log-prepend";
  private static final String SECURE_LOGGING_KEY = "secure-logging";
  private static final String ABUSE_ADDRESS_KEY = "abuse-address";
  private static final String NOTIFICATION_FREQ_KEY = "notification-freq";
  
  private static final String BAD_WORDS_VAR = "badWords";
  private static final String WHITE_LIST_VAR = "whiteList";
  private static final String[] INITIAL_BAD_WORDS = {"word1", "word2", "word3"};
  private static final String[] INITIAL_WHITE_LIST = {"admin@localhost"};
  
  /**
   * 当Set在一个线程当中进行遍历的时候内容有可能被另一个线程修改,我们认为这种修改是非常小并且很少会发生的,因为绝大多数的操作仅仅是遍历
   */
  private Set<String> badWords = new CopyOnWriteArraySet<String>();
  /**
   * 当Set在一个线程当中进行遍历的时候内容有可能被另一个线程修改,我们认为这种修改是非常小并且很少会发生的,因为绝大多数的操作仅仅是调用contains(...)方法
   */
  private Set<String> whiteList = new ConcurrentSkipListSet<String>();
  private String prependText = "Spam detected: ";
  private String abuseAddress = "abuse@locahost";
  private int notificationFrequency = 10;
  private int delayCounter = 0;
  private boolean secureLogging = false;
  private long spamCounter = 0;
  private long totalSpamCounter = 0;
  private long messagesCounter = 0;
  
  @Override
  public void processPacket(Packet packet) {
    // 这是一个message packet吗?
    if ("message" == packet.getElemName()) {
      updateServiceDiscoveryItem(getName(), "messages",
        "Messages processed: [" + (++messagesCounter) + "]", true);
      String from = JIDUtils.getNodeID(packet.getElemFrom());
      // 消息的发送者在白名单内吗?
      if (!whiteList.contains(from)) {
        // 如果ta不在白名单里面,那么检查消息的内容
        String body = packet.getElemCData("/message/body");
        if (body != null && !body.isEmpty()) {
          body = body.toLowerCase();
          for (String word : badWords) {
            if (body.contains(word)) {
              log.finest(prependText + packet.toString(secureLogging));
              ++spamCounter;
              updateServiceDiscoveryItem(getName(), "spam", "Spam caught: [" +
                (++totalSpamCounter) + "]", true);
              return;
            }
          }
        }
      }
    }
    // 不是垃圾信息,返回以便做下一步处理
    Packet result = packet.swapElemFromTo();
    addOutPacket(result);
  }
  
  @Override
  public int processingThreads() {
    return Runtime.getRuntime().availableProcessors();
  }
  
  @Override
  public int hashCodeForPacket(Packet packet) {
    if (packet.getElemTo() != null) {
      return packet.getElemTo().hashCode();
    }
    // 程序不应该运行到这里,所有的packet都必须具有一个目的地地址,但是也许垃圾过滤器也许会过滤一些奇怪的地址
    if (packet.getElemFrom() != null) {
      return packet.getElemFrom().hashCode();
    }
    // 如果程序真的运行到这一部,就应该好好检查一下到达组件的packet是否正常,然后找到一个更好的计算hashCode方法。
    return 1;
  }
  
  @Override
  public Map<String, Object> getDefaults(Map<String, Object> params) {
    Map<String, Object> defs = super.getDefaults(params);
    Collections.addAll(badWords, INITIAL_BAD_WORDS);
    Collections.addAll(whiteList, INITIAL_WHITE_LIST);
    defs.put(BAD_WORDS_KEY, INITIAL_BAD_WORDS);
    defs.put(WHITELIST_KEY, INITIAL_WHITE_LIST);
    defs.put(PREPEND_TEXT_KEY, prependText);
    defs.put(SECURE_LOGGING_KEY, secureLogging);
    defs.put(ABUSE_ADDRESS_KEY, abuseAddress);
    defs.put(NOTIFICATION_FREQ_KEY, notificationFrequency);
    return defs;
  }
  
  @Override
  public void setProperties(Map<String, Object> props) {
    super.setProperties(props);
    Collections.addAll(badWords, (String[])props.get(BAD_WORDS_KEY));
    Collections.addAll(whiteList, (String[])props.get(WHITELIST_KEY));
    prependText = (String)props.get(PREPEND_TEXT_KEY);
    secureLogging = (Boolean)props.get(SECURE_LOGGING_KEY);
    abuseAddress = (String)props.get(ABUSE_ADDRESS_KEY);
    notificationFrequency = (Integer)props.get(NOTIFICATION_FREQ_KEY);
    updateServiceDiscoveryItem(getName(), null, getDiscoDescription(),
      "automation", "spam-filtering", true,
      "tigase:x:spam-filter", "tigase:x:spam-reporting");
  }
  
  @Override
  public synchronized void everyMinute() {
    super.everyMinute();
    if ((++delayCounter) >= notificationFrequency) {
      addOutPacket(Packet.getMessage(abuseAddress, getComponentId(),
        StanzaType.chat, "Detected spam messages: " + spamCounter,
        "Spam counter", null, newPacketId("spam-")));
      delayCounter = 0;
      spamCounter = 0;
    }
  }
  
  @Override
  public String getDiscoDescription() {
    return "Spam filtering";
  }
  
  @Override
  public String getDiscoCategoryType() {
    return "spam";
  }
  
  @Override
  public void getStatistics(StatisticsList list) {
    super.getStatistics(list);
    list.add(getName(), "Spam messages found", totalSpamCounter,
      Level.INFO);
    list.add(getName(), "All messages processed", messagesCounter,
       Level.FINE);
    if (list.checkLevel(Level.FINEST)) {
      // 可以把那些非常消耗系统资源的统计数据产生代码写在下面
    }
  }
  
  @Override
  public void initBindings(Bindings binds) {
    super.initBindings(binds);
    binds.put(BAD_WORDS_VAR, badWords);
    binds.put(WHITE_LIST_VAR, whiteList);
  }
  
}


Tigase组件 – 配置API

组件的配置API实际上非常简单,它包含两个方法:



Map getDefaults(Map params);
void setProperties(Map properties);



第一个方法从组件当中获取缺省配置,第二个方法为组件设置新的配置项。尽管看起来它们非常简单,但如果想高效得使用,还需要了解更多知识。


组件的启动顺序:

在我们深入了解全部的细节之前,需要首先知道组件的初始化顺序,组件是如何“获得生命”的,以及配置是何时被设置的。组件的加载和启动顺序是这样的:

  1. 组件类被加载,并使用无参的public构造函数产生一个实例。
  2. 组件实例的setName(compName)方法被调用,实例获得组件名称。在组件的生命周期中这个方法(应该)只被调用一次。
  3. 组件实例的start()方法被调用,组件启动了所有它自己的内部线程。start()stop()方法可以在一起被调用多次,这通常用来让组件hold住和重新启动。通常情况开发者不需要关注这个方法,除非想覆写这个方法。
  4. 组件实例的getDefaults()方法被调用,Tigase服务器获得了所有该组建的缺省配置信息。在组件的生命周期中这个方法通常只被调用一次。
  5. 用户提供的配置信息与组件的缺省配置信息被合并。对于相同的配置项,用户提供的配置信息会覆写缺省值;对于不同的配置项,会保留各自的部分并进行累加。
  6. 组件实例的setProperties()方法被调用,组件获得最终配置信息。在组件的生命周期中,这个方法可以被多次调用。
  7. 组件实例的initializationCompleted()方法被调用,在调用时组件知道了全局服务器的初始化过程已经完成。在服务器的启动过程中,这个方法只被调用一次(即所有的组件都完成了配置工作和初始化)。这个方法主要被network connection manager使用,当服务器的所有功能都可用之后,它会开始监听网络socket连接。

配置相关事项当中很重要的一点是:所有的组件都不会读/请求/询问配置信息,配置信息是由configuration manager推送给组件的。在服务器运行的时候,组件的setProperties()方法可以多次在任意时间被调用。这样设计允许对服务器在运行时进行重新配置,开发者一定需要知道这一点,这样才能够在运行时对重新配置的数据进行正确处理。

配置API:两个关于配置的方法具有相同的入口参数Map<String, Object>,所以本质上组件的配置信息全部是键值对。Object对象可以是:

  • String
  • Integer
  • Long
  • Double
  • Boolean
  • 或上述任意一种类型的数组

如果组件返回的缺省配置项是上面提到的任何一种数据类型,那么setProperties()方法可以确保设置的配置项一定是同一种类型。这对于开发者而言,在代码中只需要有限得进行类型转换。

getDefaults();

Map getDefaults(Map params);



这个方法在通常情况下只被调用一次,就是在组件实例被创建之后。这个方法被用来从组件实例获取一些缺省的配置信息,然后服务器可以拿它们与用户的提供的配置信息进行合并,产生缺省或最终的初始化配置信息。我们建议这个方法返回所有的配置项及缺省值,这样最终用户在对组件进行配置的时候可以看到所有的配置项,方便定位问题。组件的任何初始化代码都不允许出现在该方法里,开发者也不能想当然得认为该方法只被调用一次。在任何时候这个方法被调用,都只能返回组件的缺省配置,而不能返回通过setProperties()方法获得的最终配置信息。入口参数Map<String, Object>可以包含一些“暗示”或者“预初始化”参数,它们会影响缺省配置的产生过程。这是因为一些组件的配置信息可能很复杂,而且这些配置信息具有很多的预设值或者会根据具体情况自动进行优化。预设值可以用来产生适合系统运行的缺省配置。如果组件 extend AbstractMessageReceiver,那么这个方法的实现应该始终类似于下面的代码:



@Override
public Map getDefaults(Map params) {
  Map defs = super.getDefaults(params);
  defs.put(CONF_ENTRY_KEY, conf_entry_val);
  return defs;
}

setProperties()

void setProperties(Map<String, Object> properties);

这个方法用来为组件设置配置信息。它可以在服务器的生命周期中在任意时间被多次调用。输入的配置信息包含了所有通过调用getDefaults()方法所获得的缺省配置项,但是其中一些项的值可能由于用户录入的配置数据被修改。如果组件extend AbstractMessageReceiver,那么实现代码可能为:

@Override
public void setProperties(Map properties) {
  super.setProperties(properties);
  int conf_entry_val = (Integer) properties.get(CONF_ENTRY_KEY);
}

可能被用到的预设值:

有一些会提供给所有的组件并被它们所用到的全局配置项。通常这些配置项是每个组件都会用到的共享资源。

  • SHARED_USER_REPO_PROP_KEY 是保存用户数据的数据库实例(广义数据库)配置项键,这个实例可以被所有的组件共享使用,可以用来存储组件数据或访问用户数据。可以使用下面的代码来访问用户数据库实例:
UserRepository user_repo;
user_repo = (UserRepository) properties.get(SHARED_USER_REPO_PROP_KEY);

SHARED_USER_REPO_POOL_PROP_KEY 是用户数据库连接池(广义数据库)的配置项键,在大多数情况用户数据库是SQL DB。可以使用下面的代码来访问用户数据库实例:

UserRepository user_repo;
user_repo = (UserRepository) properties.get(SHARED_USER_REPO_POOL_PROP_KEY);

SHARED_AUTH_REPO_PROP_KEY 是认证数据库(广义数据库)的配置项键,组件在通常情况下不需要访问认证数据库,除非它们需要对用户进行认证,并且用户的认证信息与用户其他的信息保存在不同的数据库里面。可以通过下面的代码来访问认证数据库:

UserAuthRepository auth_repo;
auth_repo = (UserAuthRepository) properties.get(SHARED_AUTH_REPO_PROP_KEY);


Tigase组件 – Packet过滤

Tigase为所有组件都提供了一个packet过滤API。你可以分别过滤传入和传出packet。

通过学习过滤,我们可以知道如何对packet进行拦截,如何修改packet内容,如何彻底得屏蔽packet。通过屏蔽,我们可以让packet跳过后续的处理过程,也可以彻底抛弃它。

packet的过滤基于PacketFilterIfc接口,请参考接口的javadoc来获取全部细节。过滤的主要方法是Packet filter(Packet packet),它以packet作为入口参数并对其进行处理,里面可以填写代码,针对packet的特定内容进行报警(如果后续处理忽视被报警的内容,可以有效的减小系统负载),并最终产生一个传出Packet用来做后续处理。如果传出的packet为null,说明这个包已经被屏蔽,它不需要再进行任何的后续处理;否则它会传出最原始的packet实例(即传入packet)或者是传入packet副本的修改版本

需要强调的是:即使Packet对象不是immutable的,我们也绝不建议对已经存在的Packet实例做任何修改。这是因为Packet实例可能同时被其他的组件或线程进行处理,对Packet实例所做的任何修改,都可能产生不可预估的后果。

请参考PacketCounter的代码,它是用来统计不同类型packet个数的简单过滤器。在缺省情况下它会被所有组件加载,用来绘制packet的流量图。你可以获得各个类型消息的统计个数,可以获得packet发源地的消息个数,packet途径地的消息个数等等。

这是因为过滤器自己可以产生统计数据,并且Tigase内嵌的统计监控机制可以访问到这些数据。如果要充分利用内嵌的统计监控功能,过滤器需要实现void getStatistics(StatisticsList list)方法。这个方法缺省状态可能是空的,但你可以在里面填写代码,产生统计数据并把它们添加到列表当中,请参考PacketCounter代码。

配置:

过滤器是可配置的,多个过滤器可以在Tigase服务器中多个组件中分别进行配置,且传入packet和传出packet可依据配置分别进行过滤。这为开发者控制Tigase服务器中的数据流量提供极大的灵活性。比如你可以加载一些特定的packet过滤器,阻止一些特别的消息来源向用户发送消息。你也可以过滤一些会导致消耗大量系统资源的packet来减轻系统负载。

缺省的配置信息会让每一个组件都加载同一个过滤器——PacketCounter,它作用于传入和传出方向。

message-router/incoming-filters=tigase.server.filters.PacketCounter
message-router/outgoing-filters=tigase.server.filters.PacketCounter
sess-man/incoming-filters=tigase.server.filters.PacketCounter
sess-man/outgoing-filters=tigase.server.filters.PacketCounter
c2s/incoming-filters=tigase.server.filters.PacketCounter
c2s/outgoing-filters=tigase.server.filters.PacketCounter
s2s/incoming-filters=tigase.server.filters.PacketCounter
s2s/outgoing-filters=tigase.server.filters.PacketCounter
bosh/incoming-filters=tigase.server.filters.PacketCounter
bosh/outgoing-filters=tigase.server.filters.PacketCounter
muc/incoming-filters=tigase.server.filters.PacketCounter
muc/outgoing-filters=tigase.server.filters.PacketCounter

假如你实现了一个自己的过滤器——垃圾信息过滤器(com.company.SpamBlocker)。你希望关闭所有组件的PacketCounter过滤器,只有message router保持原状态(开启);并在connection manager上安装垃圾信息过滤器。

需要注意的是:在connection manager里,传入传出可能并不是你所想像的那样,而是恰恰相反。

  • 传入:是message router提交给组件的packet流向,这些packet需要进行后续的处理。对connection manager而言,后续的处理意味着需要把packet发送出网络。
  • 传出:是组件产生的结果packet流向,这些packet从组件发出并被提交给message router,message router决定将这些packet发送到哪里并进行后续的处理。对于connection manager而言,传出方向是从网络当中获取packet。

根据上面的解释,我们需要把垃圾信息过滤器部署到所有connection manager的传出方向。也许你还有第二个需求,即比较bosh连接和标准的c2s连接各自产生的packet流量,那么我们可以开启connection manager上的PacketCounter过滤器。下面是我们开启connection manager的垃圾过滤器,并开启message router和connection manager上的PacketCounter的配置信息:

message-router/incoming-filters=tigase.server.filters.PacketCounter
message-router/outgoing-filters=tigase.server.filters.PacketCounter
sess-man/incoming-filters=
sess-man/outgoing-filters=
c2s/incoming-filters=tigase.server.filters.PacketCounter
c2s/outgoing-filters=tigase.server.filters.PacketCounter,com.company.SpamBlocker
s2s/incoming-filters=
s2s/outgoing-filters=com.company.SpamBlocker
bosh/incoming-filters=tigase.server.filters.PacketCounter
bosh/outgoing-filters=tigase.server.filters.PacketCounter,com.company.SpamBlocker
muc/incoming-filters=
muc/outgoing-filters=

将这些配置项应用于Tigase服务器的最简单方法就是把它们写进init.properties文件。

Tigase组件第七节 – 数据库

在某些情况,你需要通过组件对需要保存的数据进行持久化。当然你可以使用组件配置API向组件提供一些数据库连接配置项,自己实现数据库连接,并向数据库中保存数据。但我们其实已经提供了一个非常简单方便的数据访问框架,它允许你透明得访问数据库或文件系统,读取或保存数据。也提供了ad-hoc命令调用接口,所以你可以使用具备ad-hoc功能的XMPP客户端直接操作你的组件数据。

你可能感兴趣的:(Tigase组件)