Openfire分析之二:主干程序分析

  引言

  宇宙大爆炸,于是开始了万物生衍,从一个连人渣都还没有的时代,一步步进化到如今的花花世界。

  然而沧海桑田,一百多亿年过去了….

  好复杂,但程序就简单多了,main()函数运行,敲个回车,一行Hello World就出来了,所以没事多敲敲回车,可以练手感….

  一、程序入口

  Java的程序入口是main方法,Openfire也不例外。可以全局检索一下”void main”,可以看到,Openfire的main函数有两个:

  (1)org.jivesoftware.openfire.launcher.Launcher类,以图形界面的形式启动

public static void main(String[] args) throws AWTException {
    new Launcher();
}

  (2)org.jivesoftware.openfire.starter.ServerStarter类,以服务的形式启动

public static void main(String [] args) {
    new ServerStarter().start();
}

  一般Openfire都做为服务的形式运行,所以我们重点关注ServerStarter类即可。如果是用eclipse调试Openfire工程,Run Configure中的Main Class也是设置这个类的完整路径。

  org.jivesoftware.openfire.starter.ServerStarter类

|-- main()方法
    |-- start()

  ServerStarter.start( )方法加载配置,并用类加载的方法,实例化org.jivesoftware.openfire.XMPPServer。

  XMPPServer

  XMPPServer实例化之后,直接调用本类中的start( )方法,于是系统开始跑起来。运行逻辑如下:

|-- XMPPServer()构造方法
    |-- start()
        |-- 加载 conf/openfire.xml,设置host、xmpp.domain等全局参数
        |-- 验证数据库是否可用
        |-- 加载、初始化、并启动openfire Module
        |-- 加载并启动 plugins
        |-- 启动监听服务:listener.serverStarted()

  下面从XMPPServer.start( )方法开始,跟一下Openfire在启动的过程中,处理了哪些业务,以此来分析Openfire启动流程。

  三、服务启动流程分析

  1、XMPPServer.start()方法

    添加了部分注释,如下:

public void start() {
    try {

        //初始化配置
        initialize();
        // 插件管理
        File pluginDir = new File(openfireHome, "plugins");
        pluginManager = new PluginManager(pluginDir);

        if (!setupMode) {
            // 验证数据库是否可用,验证方法也很简单,就是执行一句sql语句,没有异常表示可用。
            verifyDataSource();
            //加载module
            loadModules();
            //初始化module
            initModules();
            //启动modeule
            startModules();
        }
        // 服务器流量计算类,用来计算服务器写入和读取的字节数,包括C-S,S-S或扩展的组件和连接的流量
        ServerTrafficCounter.initStatistics();

        //启动插件
        pluginManager.start();

        //打印启动日志
        String startupBanner = LocaleUtils.getLocalizedString("short.title") + " " + version.getVersionString() +
                " [" + JiveGlobals.formatDateTime(new Date()) + "]";
        logger.info(startupBanner);
        System.out.println(startupBanner);

        started = true;

        //通知其他的监听服务,服务器已启动
        for (XMPPServerListener listener : listeners) {
            listener.serverStarted();
        }
    }
    catch (Exception e) {
        e.printStackTrace();
        logger.error(e.getMessage(), e);
        System.out.println(LocaleUtils.getLocalizedString("startup.error"));
        shutdownServer();
    }
}

  从上面代码和注释的内容,可以确确看出,启动的过程,主要就做了这几件事:

  (1)初始化

  (2)加载启动各个模块

  (3)加载启动各个插件

  (4)启动监听

  下面,分别括概性的分析这几个部分,目的是使读者对Openfire的启动有个大致的印象,即可。

  2、初始化,initialize()方法

  在Openfire的安装目录,有一个openfire.xml配置文件,初始化主要是将配置文件中的信息载入系统,并处理一些与进程和缓存等相关的工作。 

private void initialize() throws FileNotFoundException {
    //用于确定openfire的工作目录,以及配置文件,并构造相应的File实例
    //这里不做深入,将处理的内容列举如下:
    //1. openfire配置文件所在的相对路径(conf/openfire.xml)。
    //2. 获取openfire工作目录的绝对路径homeProperty
    //3. 来获取配置文件的File实例verifyHome
    locateOpenfire();

    startDate = new Date();

    //获取计算机名
    try {
        host = InetAddress.getLocalHost().getHostName();
    }
    catch (UnknownHostException ex) {
        logger.warn("Unable to determine local hostname.", ex);
    }
    if (host == null) {
        host = "127.0.0.1";
    }

    version = new Version(4, 0, 3, Version.ReleaseStatus.Release, -1);
    if ("true".equals(JiveGlobals.getXMLProperty("setup"))) {
        setupMode = false;
    }

    if (isStandAlone()) {
        //设置服务器异常关机时执行的函数ShutdownHookThread(),当服务器异常关机时,必然会执行shutdownServer函数,关闭一些模块、执行一些监听函数等等
        logger.info("Registering shutdown hook (standalone mode)");
        Runtime.getRuntime().addShutdownHook(new ShutdownHookThread());

        //启动一个定时线程,该线程监听控制台的输入,如果为“exit”,则调用System.exit退出openfire进程。
        TaskEngine.getInstance().schedule(new Terminator(), 1000, 1000);
    }

    loader = Thread.currentThread().getContextClassLoader();

    try {
        //初始化了org.jivesoftware.util.cache.DefaultLocalCacheStrategy实例,该实例和缓存有关
        CacheFactory.initialize();
    } catch (InitializationException e) {
        e.printStackTrace();
        logger.error(e.getMessage(), e);
    }

    //migrateProperty()方法用于将数据从xml载入到数据库,并处理一些配置信息
    JiveGlobals.migrateProperty("xmpp.domain");

    name = JiveGlobals.getProperty("xmpp.domain", host).toLowerCase();

    JiveGlobals.migrateProperty(Log.LOG_DEBUG_ENABLED);
    Log.setDebugEnabled(JiveGlobals.getBooleanProperty(Log.LOG_DEBUG_ENABLED, false));

    // Update server info
    //最后初始化了XMPPServerInfoImpl实例
    xmppServerInfo = new XMPPServerInfoImpl(name, host, version, startDate);

    initialized = true;
}

  3、Module和Plugin 的加载

  从XMPPServer.start( )的方法执行的内容来看,主要加载两大主体,一个是module,一个是plugin,这两部分可以说是整套系统的所有功能实现。下面对这两个部分,先做一个简述。具体的机制,在后续另起章节描述分析。

  (1)Module

  Openfire的核心功能都依靠module实现,所有的module都继承自BasicModule,而BasicModule实现了Module接口。

  Module接口类定义了如下方法列表:

String getName();
void initialize(XMPPServer server);
void start();
void stop();
void destroy(); 

  从方法名可以看出,它描述了所有的module在整个生命周期内应调用的方法。

  而BaseModule则对Module进行了空实现,所有的module对BaseModule中的方法选择性覆写。

  各个module在XMPPServer启动之初,被装载在一个容器中:

Map<Class, Module> modules

  通过递归的方式,调用module所覆写的initialize()、start()、stop()、destroy()等方法,实现对module的管理。

  module的加载,有一点需要留意下:ConnectionManager是在最后加载,源码中有如下代码段及注释:

// Load this module always last since we don‘t want to start listening for clients
// before the rest of the modules have been started
loadModule(ConnectionManagerImpl.class.getName());

  Openfire的连接管理、端口监听,都在ConnectionManager这个模块中进行处理,这也是它为何要放在最后一个加载的原因。

  (2)plugin

  plugin的启动,是在module之后。

  pluginManager.start()方法中启动了PluginMonitor线程:

public void start() {
    executor = new ScheduledThreadPoolExecutor(1);
    // See if we‘re in development mode. If so, check for new plugins once every 5 seconds.
    // Otherwise, default to every 20 seconds.
    if (Boolean.getBoolean("developmentMode")) {
        executor.scheduleWithFixedDelay(pluginMonitor, 0, 5, TimeUnit.SECONDS);
    }
    else {
        executor.scheduleWithFixedDelay(pluginMonitor, 0, 20, TimeUnit.SECONDS);
    }
}

  线程的执行run()方法如下:

@Override
public void run() {
    ......
    try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(pluginDirectory, new DirectoryStream.Filter<Path>() {
        @Override
        public boolean accept(Path pathname) throws IOException {
            String fileName = pathname.getFileName().toString().toLowerCase();
            return (fileName.endsWith(".jar") || fileName.endsWith(".war"));
        }})) {
        for (Path jarFile : directoryStream) {
            ......
            if (Files.notExists(dir)) {
                unzipPlugin(pluginName, jarFile, dir);
            }
            ......
        }
    }
    ......

    // Load all plugins that need to be loaded.
    for (Path dirFile : dirs) {
        if (Files.exists(dirFile) && !plugins.containsKey(dirFile.getFileName().toString())) {
            loadPlugin(dirFile);
        }
    }
    ......

    // Trigger event that plugins have been monitored
    firePluginsMonitored();
    ......
}

  PluginMonitor线程的主要处理:解压插件目录下所有拓展名为jar和war的插件,用loadPlugin( )装载该插件,最后通过firePluginsMonitored( )函数调用插件的监听函数。

  firePluginsMonitored( )方法中,调用插件的监听函数pluginsMonitored( ):

private void firePluginsMonitored() {
    for(PluginManagerListener listener : pluginManagerListeners) {
        listener.pluginsMonitored();
    }
}

  PluginManagerListener.pluginsMonitored()监听函数,在ConnetionMamagerImpl模块启动时实现。

  在ConnetionMamagerImpl.startListeners()方法,省略一些无关的代码,如下:

private synchronized void startListeners()
{
    PluginManager pluginManager = XMPPServer.getInstance().getPluginManager();
    if (!pluginManager.isExecuted()) {
        pluginManager.addPluginManagerListener(new PluginManagerListener() {
            public void pluginsMonitored() {
                ......
            }
        });
        return;
    }
    ......
}

  也就解释了插件的启动是在module启动之后。

  事实上也可以理解:modules为openfire自带模块,plugins我们可以称为外来者。openfire需要对plugins进行管理、以及各种响应,那么自然需要其自身各个模块首先运作起来,这可以理解为一个主次顺序。

  4、最后,启动监听服务

// Notify server listeners that the server has been started
for (XMPPServerListener listener : listeners) {
    listener.serverStarted();
}

  通知PubSubModule、PresenceManagerImpl、MultiUserChatServiceImpl等module监听启动。

  至此,openfire完成了启动。

 四、服务关闭

  讲了系统的启动,接下来稍微提一下系统的stop。

  服务关闭相对就简单一些,当收到控制能上的exit指令、或者启动过程之中出现了异常时, 就会调用关闭程序,通知其他的服务模块关闭监听、所有的模块和插件都停止并注销、关闭数据库资源、关闭线程的监听等。

  这里贴一下代码:

 private void shutdownServer() {
        shuttingDown = true;
        ClusterManager.shutdown();
        // Notify server listeners that the server is about to be stopped
        for (XMPPServerListener listener : listeners) {
            try {
                listener.serverStopping();
            } catch (Exception ex) {
                logger.error("Exception during listener shutdown", ex);
            }
        }
        // If we don‘t have modules then the server has already been shutdown
        if (modules.isEmpty()) {
            return;
        }
        logger.info("Shutting down " + modules.size() + " modules ...");
        // Get all modules and stop and destroy them
        for (Module module : modules.values()) {
            try {
                module.stop();
                module.destroy();
            } catch (Exception ex) {
                logger.error("Exception during module shutdown", ex);
            }
        }
        // Stop all plugins
        logger.info("Shutting down plugins ...");
        if (pluginManager != null) {
            try {
                pluginManager.shutdown();
            } catch (Exception ex) {
                logger.error("Exception during plugin shutdown", ex);
            }
        }
        modules.clear();
        // Stop the Db connection manager.
        try {
            DbConnectionManager.destroyConnectionProvider();
        } catch (Exception ex) {
            logger.error("Exception during DB shutdown", ex);
        }

        // Shutdown the task engine.
        TaskEngine.getInstance().shutdown();

        // hack to allow safe stopping
        logger.info("Openfire stopped");
 }


  

  OK,主干程序就分析到此。Openfire中的消息机制是怎么样的,各个模块是如何协作,插件又该怎么编写,在后续的章节中解答。

  希望这一系列的文章对您有所帮助,Over!

时间: 2024-08-04 14:32:27

Openfire分析之二:主干程序分析的相关文章

vscode源码分析【二】程序的启动逻辑

上一篇文章:https://www.cnblogs.com/liulun/ (小广告:我做的开源免费的,个人知识管理及自媒体营销工具“想学吗”:https://github.com/xland/xiangxuema) 我们在package.json里能找到他的入口文件: "main": "./out/main", electron是分主进程和渲染进程的: 渲染进程是主进程启动的: ./out/main.js显然这就是主进程的入口程序: 确实不假 但别着急去分析这个文

spring security源码分析之二---web包分析

Spring 是一个非常流行和成功的 Java 应用开发框架.Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案.一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分.用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统.用户认证一般要求用户提供用户名和密码.系统通过校验用户名和密码来完成认证过程.用户授权指的是验证某个用户是否有权限执行某个操作.在一

C#程序分析

一.程序及问题 阅读下面程序,请回答如下问题: 问题1:这个程序要找的是符合什么条件的数? 问题2:这样的数存在么?符合这一条件的最小的数是什么? 问题3:在电脑上运行这一程序,你估计多长时间才能输出第一个结果?时间精确到分钟(电脑:单核CPU 4.0G Hz,内存和硬盘等资源充足). 问题4:在多核电脑上如何提高这一程序的运行效率? using System; using System.Collections.Generic; using System.Text; namespace Find

vscode源码分析【三】程序的启动逻辑,性能问题的追踪

第一篇: vscode源码分析[一]从源码运行vscode 第二篇:vscode源码分析[二]程序的启动逻辑,第一个窗口是如何创建的 启动追踪 代码文件:src\main.js 如果指定了特定的启动参数:trace vscode会在启动之初,执行下面的代码: const contentTracing = require('electron').contentTracing; const traceOptions = { categoryFilter: args['trace-category-f

linux程序分析工具介绍(二)—-ldd,nm

本文要介绍的ldd和nm是linux下,两个用来分析程序很实用的工具.ldd是用来分析程序运行时需要依赖的动态库的工具:nm是用来查看指定程序中的符号表相关内容的工具.下面通过例子,分别来介绍一下这两个工具: 1. ldd, 先看下面的例子, 用ldd查看cs程序所依赖的动态库: [email protected]:~/Public$ ldd cs linux-gate.so.1 => (0xffffe000) libz.so.1 => /lib/libz.so.1 (0xb7f8c000)

(IOS)BaiduFM 程序分析

本文主要分享下楼主在学习Swift编程过程中,对GitHub上的一个开源app BaiduFM的研究心得. 项目地址:https://github.com/belm/BaiduFM-Swift 一.项目简介 项目通过使用百度音乐的API实现了播放.下载与收藏音乐的FM基本功能.同时实现了歌词滚动,显示实时进度条,支持后台播放,锁屏显示歌曲信息及控制播放等附加功能并添加了对Apple Watch的支持.此APP可谓是功能十分强劲,下面楼主就来好好分析下这款APP的代码及功能实现原理. 二.APP

Linux内核分析(二)----内核模块简介|简单内核模块实现

Linux内核分析(二) 昨天我们开始了内核的分析,网上有很多人是用用源码直接分析,这样造成的问题是,大家觉得很枯燥很难理解,从某种意义上来说linux系统本身就是由一个个模块构成的,所以我会结合内核模块的设计,去分析内核,从而达到对linux内核的理解. 今天我们会分析到以下内容: 1.      Linux内核模块简介 2.      简单内核模块实现 l  Linux内核模块简介 1.       何为内核模块 在上一篇博文中我们先通过内核配置,在配置的过程中我们对内核的组件进行了选择(当

cocos2dx之tolua++全面分析(二):类注册

tolua被作为库使用时,首先会做大量内部初始化工作: 一.tolua_open是入口点,它创建很多用于管理的内部变量,以下用_G指代全局表,_R指定registry table: 1._R.TOLUA_VALUE_ROOT={}, 这个表是cocos2dx自己加的,它把所有传入lua的cppobj/userdata都塞到这个表里,而且这还不是一个弱表,也就意味着cocos2dx创建的cpp obj,永远都不会被gc!只有在c++层面被delete时,才会去这个表里删除自己.因此,每一个coco

Netlink 内核实现分析(二):通信

在前一篇博文<Netlink 内核实现分析(一):创建>中已经较为具体的分析了Linux内核netlink子系统的初始化流程.内核netlink套接字的创建.应用层netlink套接字的创建和绑定流程,本文来具体的分析一下内核是怎样实现netlink消息在内核和应用进程之间全双工异步通信的. 一.netlink通信数据结构 1.netlink消息报头:struct nlmsghdr struct nlmsghdr { __u32 nlmsg_len; /* Length of message