侯亮

1. 概述

Pluma是一个用C++开发的可用于管理插件的开源架构,其官网地址为:http://pluma-framework.sourceforge.net/。该架构是个轻量级架构,非常易于理解。

Pluma架构有以下基本概念:
1)插件的外在行为体现为一个纯虚类,可以叫作插件接口;
2)继承于同一个插件接口的若干派生类,被认为属于同一种插件,可以叫作插件类;
3)每一个插件接口或插件类都有个一一对应的Provider类,其中,插件接口对应的Provider类里会定义一个特殊字符串常量:PLUMA_PROVIDER_TYPE,表示这一类“插件Provider”共同的类型名称,而这个类型名称其实就是插件接口的类名字符串。
4)多个插件类可以被放入一个插件动态库中,而这个动态库文件名(不包括后缀部分)可以叫作“插件名”。
5)插件机制使用者可以在自己的架构中包含一个Pluma管理类,该类支持从所指定的位置加载一个或多个插件动态库,并将每个插件类对应的Provider,记录进内部的表中。
6)插件机制使用者可以在合适时机,利用Pluma获取内含的插件Provider,并调用某个插件Provider的create()函数,创建出对应的插件对象。
7)使用完插件对象后,不要忘了delete它。

现在我们画一张示意图:

2. Pluma管理类

我们刚刚也说了,插件机制使用者可以包含一个Pluma管理类。该类继承于PluginManager类。【pluma-1.1/include/pluma/PluginManager.hpp】

class PLUMA_API PluginManager{
public:~PluginManager();bool load(const std::string& path);bool load(const std::string& folder, const std::string& pluginName);int loadFromFolder(const std::string& folder, bool recursive = false);bool unload(const std::string& pluginName);void unloadAll();bool addProvider(Provider* provider);void getLoadedPlugins(std::vector<const std::string*>& pluginNames) const;bool isLoaded(const std::string& pluginName) const;protected:PluginManager();void registerType(const std::string& type, unsigned int version, unsigned int lowestVersion);const std::list<Provider*>* getProviders(const std::string& type) const;private:static std::string getPluginName(const std::string& path);static std::string resolvePathExtension(const std::string& path);private:typedef bool fnRegisterPlugin(Host&);typedef std::map<std::string,DLibrary*> LibMap;LibMap libraries;  ///< Map containing the loaded librariesHost host;         ///< Host app proxy, holding all providers
};

从上面的load()函数和loadFromFolder()函数可以看出,插件管理器既允许用户单独加载某个插件动态库,也允许批量性加载某个目录下所有的插件动态库。另外,值得注意的是,getProviders()函数是protected的成员,也就是说,这套架构是不希望用户直接使用这个PluginManager类的,即便用了,你也拿不到Provider。正确的做法是,使用PluginManager的子类:Pluma管理类。

另外,上面的成员变量libraries,就是记录所有已加载的插件动态库的映射表。而成员变量host则负责记录每个插件类对应的Provider信息。之所以被称为host(宿主),是针对插件而言的。也就是说插件本身实际上是没资格知道其真实宿主的全貌的,它只能访问和它相关的很小一部分数据而已,因此Pluma将这一小部分数据整理成一个host代理,供插件使用。

Pluma管理类的代码截选如下:
【pluma-1.1/include/pluma/Pluma.hpp】

class Pluma: public PluginManager{
public:Pluma();template<typename ProviderType>void acceptProviderType();template<typename ProviderType>void getProviders(std::vector<ProviderType*>& providers);
};#include <Pluma/Pluma.inl>

请大家注意上面代码中最后一行,这个Pluma.hpp还真是有点手黑,偷偷摸摸#include了个Pluma.inl文件,其实展开来就是acceptProviderType()和getProviders()这两个模板函数的实现。Pluma.inl文件的内容如下:
【pluma-1.1/include/pluma/Pluma.inl】

inline Pluma::Pluma(){// Nothing to do
}template<typename ProviderType>
void Pluma::acceptProviderType(){PluginManager::registerType(ProviderType::PLUMA_PROVIDER_TYPE,ProviderType::PLUMA_INTERFACE_VERSION,ProviderType::PLUMA_INTERFACE_LOWEST_VERSION);
}template<typename ProviderType>
void Pluma::getProviders(std::vector<ProviderType*>& providers){const std::list<Provider*>* lst = PluginManager::getProviders(ProviderType::PLUMA_PROVIDER_TYPE);if (!lst) return;providers.reserve(providers.size() + lst->size());std::list<Provider*>::const_iterator it;for (it = lst->begin() ; it != lst->end() ; ++it)providers.push_back(static_cast<ProviderType*>(*it));
}

看到了吧,重新定义了个getProviders(),还搞成一个模板函数,在函数体内会反过来通过模板参数,进一步得到所涉及的插件Provider的PLUMA_PROVIDER_TYPE信息,这个技巧挺重要。也就是说,外界传来的是vector<ProviderType>,而函数内部可以推断出ProviderType::PLUMA_PROVIDER_TYPE。将PLUMA_PROVIDER_TYPE传入父类的PluginManager::getProviders()函数,就可以拿到符合所指类型的所有Provider。

我们画一张Pluma简图,后面再细说相关细节:

同一类插件类,会对应一个ProviderInfo节点,该节点内部的providers列表,记录着同属一类的若干Provider。

2.1 Host代理

【pluma-1.1/include/pluma/Host.hpp】

class PLUMA_API Host{
friend class PluginManager;
friend class Provider;public:bool add(Provider* provider);private:Host();~Host();bool knows(const std::string& type) const;unsigned int getVersion(const std::string& type) const;unsigned int getLowestVersion(const std::string& type) const;void registerType(const std::string& type, unsigned int version, unsigned int lowestVersion);const std::list<Provider*>* getProviders(const std::string& type) const;void clearProviders();bool validateProvider(Provider* provider) const;bool registerProvider(Provider* provider);void cancelAddictions();bool confirmAddictions();private:struct ProviderInfo{unsigned int version;unsigned int lowestVersion;std::list<Provider*> providers;};typedef std::map<std::string, ProviderInfo > ProvidersMap;typedef std::map<std::string, std::list<Provider*> > TempProvidersMap;ProvidersMap knownTypes;       ///< Map of registered types.TempProvidersMap addRequests;  ///< Temporarily added providers
};

正如前文所说,Host代理是针对插件而言的。而Host只有一个public成员函数add(),说明其主要对外行为就是让插件将对应的provider注册进Host。

3. 插件类和其对应的Provider类

在说了一大堆插件管理类代码后,现在终于要开始说插件部分了。前文已经说过,插件的外在行为体现为一个纯虚类,可以叫作插件接口。我们现在就以Pluma源码中给出的例子为准,来说明一些细节。

3.1 Warrior接口和WarriorProvider类

Pluma中的插件接口例子是Warrior,其源码截选如下:
【pluma-1.1/example/src/interface/Warrior.hpp】

#include <Pluma/Pluma.hpp>
class Warrior{
public:virtual std::string getDescription() = 0;// (...)
};
PLUMA_PROVIDER_HEADER(Warrior);

这个接口里只象征性的写了一个成员函数getDescription(),大家明白意思即可。

需要注意的是类定义之后的那句PLUMA_PROVIDER_HEADER,这个宏负责定义和插件接口对应的Provider类。相关的宏定义如下:
【pluma-1.1/include/pluma/Pluma.hpp】

#define PLUMA_PROVIDER_HEADER(TYPE)\
PLUMA_PROVIDER_HEADER_BEGIN(TYPE)\
virtual TYPE* create() const = 0;\
PLUMA_PROVIDER_HEADER_END#define PLUMA_PROVIDER_HEADER_BEGIN(TYPE)\
class TYPE##Provider: public pluma::Provider{\
private:\friend class pluma::Pluma;\static const unsigned int PLUMA_INTERFACE_VERSION;\static const unsigned int PLUMA_INTERFACE_LOWEST_VERSION;\static const std::string PLUMA_PROVIDER_TYPE;\std::string plumaGetType() const{ return PLUMA_PROVIDER_TYPE; }\
public:\unsigned int getVersion() const{ return PLUMA_INTERFACE_VERSION; }#define PLUMA_PROVIDER_HEADER_END };

基于这些宏定义,我们可以将PLUMA_PROVIDER_HEADER(Warrior)展开为:

class WarriorProvider: public pluma::Provider{
private:friend class pluma::Pluma;static const unsigned int PLUMA_INTERFACE_VERSION;static const unsigned int PLUMA_INTERFACE_LOWEST_VERSION;static const std::string PLUMA_PROVIDER_TYPE;std::string plumaGetType() const{ return PLUMA_PROVIDER_TYPE; }
public:unsigned int getVersion() const{ return PLUMA_INTERFACE_VERSION; }virtual Warrior* create() const = 0;
};

代码很清晰,为Warrior接口声明一个配套的WarriorProvider类。这个类里包含着重要的PLUMA_PROVIDER_TYPE常量,以及最关键的create()函数。

Warrior的实现文件更加简单:
【pluma-1.1/example/src/interface/Warrior.cpp】

#include "Warrior.hpp"
PLUMA_PROVIDER_SOURCE(Warrior, 1, 1);

也在使用宏,展开宏后可见:

const std::string WarriorProvider::PLUMA_PROVIDER_TYPE = "Warrior";
const unsigned int WarriorProvider::PLUMA_INTERFACE_VERSION = 1;
const unsigned int WarriorProvider::PLUMA_INTERFACE_LOWEST_VERSION = 1;

因为Warrior本身是个纯虚类,所以WarriorProvider里也不用实现create()函数。

3.2 Warrior派生类和派生Provider

在pluma源码的例子中,提供了三个Warrior派生类,SimpleWarrior、Eagle和Jaguar。默认的是SimpleWarrior,它被集成进example/src/host目录。也就是说,即便我们一个额外的插件库都不提供,示例至少还可以使用SimpleWarrior。而Eagle和Jaguar则位于example/src/plugin目录,可以打包进一个插件动态库。

【pluma-1.1/example/src/host/SimpleWarrior.hpp】

#include "Warrior.hpp"
class SimpleWarrior: public Warrior{
public:std::string getDescription(){return "Commoner: leaded by calpoleque";}
};
PLUMA_INHERIT_PROVIDER(SimpleWarrior, Warrior);

前文我们已经看到,对于插件接口(Warrior)来说,用到的宏是PLUMA_PROVIDER_HEADER(Warrior),现在针对实际插件类(SimpleWarrior),会用到另一个宏PLUMA_INHERIT_PROVIDER(SimpleWarrior, Warrior)。这个宏的定义如下:

【pluma-1.1/include/pluma/Pluma.hpp】

#define PLUMA_INHERIT_PROVIDER(SPECIALIZED_TYPE, BASE_TYPE)\
class SPECIALIZED_TYPE##Provider: public BASE_TYPE##Provider{\
public:\BASE_TYPE * create() const{ return new SPECIALIZED_TYPE (); }\
};

展开后可见:

class SimpleWarriorProvider: public WarriorProvider{
public:Warrior * create() const { return new SimpleWarrior (); }
};

很简单,就是在完成Provider的核心使命,提供一个创建插件类对象的create()函数。与SimpleWarriorProvider类似,另外两个Warrior派生类Eagle和Jaguar大体也是这么写的。示意图如下:

​在研究Pluma所给示例时,我已事先将Pluma封装成静态库了,现在要把Eagle和Jaguar编译并封装成一个动态库,就需要链接Pluma静态库,除此之外,还需要编译其他一些辅助文件,列举如下:

  1. Connector.cpp
  2. dllmain.cpp
  3. Eagle.hpp
  4. Jaguar.hpp
  5. Warrior.cpp

其中Connector.cpp文件,是插件动态库向外界Host注册自己所有Provider的地方。它必须实现一个connect()函数,代码截选如下:

#include <Pluma/Connector.hpp>
#include "Eagle.hpp"
#include "Jaguar.hpp"PLUMA_CONNECTOR
bool connect(pluma::Host& host){host.add( new EagleProvider() );host.add( new JaguarProvider() );return true;
}

我们先不要着急分析上面的connect()动作,可以先跟着我看看插件的加载流程,后文我们就会知道,connect()只是加载流程的一环而已。

4. 插件加载流程

我们看一下Pluma架构所给例子的main()函数,就可以了解插件的加载流程了:

int main()
{pluma::Pluma pluma;pluma.acceptProviderType<WarriorProvider>();   // 表明用户感兴趣东西,添加ProviderInfopluma.load("plugins", "PlumaDemoWarriorPlugin");  // 加载动作,向ProviderInfo里加料std::vector<WarriorProvider*> providers;pluma.getProviders(providers);std::vector<WarriorProvider*>::iterator it;for (it = providers.begin(); it != providers.end(); ++it) {Warrior* warrior = (*it)->create();std::cout << warrior->getDescription() << std::endl;delete warrior;}pluma.unloadAll();std::cout << "Press any key to exit";std::cin.ignore(10000, '\n');return 0;
}

其中和加载插件相关的句子主要就是pluma.acceptProviderType和pluma.load两句了。前者主要负责在Host的knownTypes映射表中添加一个ProviderInfo节点,后者负责加载插件动态库,并将动态库里匹配的Provider指针记入ProviderInfo节点。

4.1 pluma.acceptProviderType<>()

我们先说pluma.acceptProviderType一句。在前文介绍Pluma.inl文件的内容时,我们已经看到一个叫作acceptProviderType的模板函数了,当时没有细说,现在我把它的代码再贴一下:
【pluma-1.1/include/pluma/Pluma.inl】

template<typename ProviderType>
void Pluma::acceptProviderType(){PluginManager::registerType(ProviderType::PLUMA_PROVIDER_TYPE,ProviderType::PLUMA_INTERFACE_VERSION,ProviderType::PLUMA_INTERFACE_LOWEST_VERSION);
}

里面调用的是PluginManager基类的registerType()函数。我们前文主要关心的是PLUMA_PROVIDER_TYPE,现在再说一下后两个参数。PLUMA_INTERFACE_VERSION表示管理器当前应该使用的插件接口的版本,因为我们不能确定更高版本的插件接口会不会增加或删除成员函数,所以这个值其实是个限定值,如果后续用户尝试加载更高版本的插件,那么是无法通过校验的。第三个参数PLUMA_INTERFACE_LOWEST_VERSION则是限定最低值,如果尝试加载比这个值更低版本的插件,肯定也是不会通过的。

在刚刚看到的main()函数里,是这样写的:

pluma.acceptProviderType<WarriorProvider>();

也就是说,Pluma插件管理器对Warrior接口对应的WarriorProvider类感兴趣。而当初定义Warrior时,在Warrior.cpp文件里的确指明了WarriorProvider能限定的当前版本号和最低版本号:

PLUMA_PROVIDER_SOURCE(Warrior, 1, 1);

这些类型信息、版本号限定信息都会被注册在Host的knownTypes映射表中,每种接口类型对应一个ProviderInfo节点。注册动作的代码如下:
【pluma-1.1/src/pluma/PluginManager.cpp】

void PluginManager::registerType(const std::string& type, unsigned int version,unsigned int lowestVersion){host.registerType(type, version, lowestVersion);
}

【pluma-1.1/src/pluma/Host.cpp】

void Host::registerType(const std::string& type, unsigned int version, unsigned int lowestVersion){if (!knows(type)){ProviderInfo pi;pi.version = version;pi.lowestVersion = lowestVersion;knownTypes[type] = pi;}
}

当然,新加的ProviderInfo节点的providers列表是个空列表,待后续再添加Provider*内容。

4.2 pluma.load()

接着,我们继续看main()函数里调用的pluma.load(),其实调用的是其父类PluginManager的load()。相关代码截选如下:
【pluma-1.1/src/pluma/PluginManager.cpp】

bool PluginManager::load(const std::string& path){std::string plugName = getPluginName(path);std::string realPath = resolvePathExtension(path);DLibrary* lib = DLibrary::load(realPath);......fnRegisterPlugin* registerFunction;registerFunction = reinterpret_cast<fnRegisterPlugin*>(lib->getSymbol("connect"));......if (!registerFunction(host)){......return false;}if (host.confirmAddictions())libraries[plugName] = lib;else{......return false;}return true;
}

可以看到,一开始就在着手加载动态库,并调用动态库里的connect()函数。前文我们实际上已经列举过示例代码里的connect()函数了,现在再贴一次:

PLUMA_CONNECTOR
bool connect(pluma::Host& host){host.add( new EagleProvider() );host.add( new JaguarProvider() );return true;
}

前文在阐述到connect()时,暂时没有细说add()动作,现在我们来看看它的代码:
【pluma-1.1/src/pluma/Host.cpp】

bool Host::add(Provider* provider){if (provider == NULL){fprintf(stderr, "Trying to add a null provider.\n");return false;}if (!validateProvider(provider)){delete provider;return false;}// 临时放进 addRequests表addRequests[ provider->plumaGetType() ].push_back(provider);return true;
}

上面代码中那个plumaGetType()函数其实是Provider的私有成员,一般人访问不了,但Host是它的友元类,所以可以访问。代码中会先校验待添加的Provider是否合格,如果合格则以plumaGetType()返回值为key值,并向临时映射表addRequests中添加该Provider指针。所谓合格是指,这个Provider的类型是Host感兴趣的,并且其版本号也是合适的。

值得注意的是,待添加的Provider*,只是临时先放进一个addRequests映射表中。addRequests映射表的定义如下:
【pluma-1.1/include/pluma/Host.hpp】

typedef std::map<std::string, std::list<Provider*> > TempProvidersMap;
......
TempProvidersMap addRequests;

那么这个临时性的addRequests映射表的内容会怎样处理呢?说起来也简单,会被“搬移”进Host的knownTypes映射表中某个ProviderInfo的内部列表去。main()在调用完connect()函数后,调用的confirmAddictions()就是做这个事情的:
【pluma-1.1/src/pluma/Host.cpp】

bool Host::confirmAddictions(){if (addRequests.empty()) return false;TempProvidersMap::iterator it;for( it = addRequests.begin() ; it != addRequests.end() ; ++it){std::list<Provider*> lst = it->second;std::list<Provider*>::iterator providerIt;for (providerIt = lst.begin() ; providerIt != lst.end() ; ++providerIt){knownTypes[it->first].providers.push_back(*providerIt);}}TempProvidersMap().swap(addRequests);  // 清空addRequests表return true;
}

我们画一张调用关系图看看:

我们可以通过这张调用关系图回顾一下,主要流程就是在加载插件动态库,并执行动态库里的connect()函数。该函数会将动态库里可用的所有Provider*记入Host的knownTypes映射表中。同时,动态库对应的DLibrary对象,也会插入Pluma管理类内部的libraries映射表中。

为了巩固知识,我们把前文的两张图再整合一下。

5. 使用插件Provider

5.1 pluma.getProviders()

在Providers都添加进Pluma管理类后,我们就可以在需要时获取provider了,为此Pluma类提供了getProviders()函数:
【pluma-1.1/src/pluma/PluginManager.cpp】

const std::list<Provider*>* PluginManager::getProviders(const std::string& type) const{return host.getProviders(type);
}

【pluma-1.1/src/pluma/Host.cpp】

const std::list<Provider*>* Host::getProviders(const std::string& type) const{ProvidersMap::const_iterator it = knownTypes.find(type);if (it != knownTypes.end())return &it->second.providers;return NULL;
}

代码很简单,就是帮使用者把感兴趣的某类插件Provider全部找出来。如果当初我们已经通过acceptProviderType()注册了对应的类型(PLUMA_PROVIDER_TYPE),那么至少可以拿到一个list,否则就只能拿到NULL了。如果我们可以拿到若干Provider,就可以调用其create()函数创建对应的插件对象了。

当工作做完后,用户应该及时delete掉之前创建出的插件对象。在程序退出之前,用户应该调用pluma.unloadAll()删除所有插件Provider及DLibrary对象。DLibrary对象析构时,会自动关闭已经打开的动态链接库。
【pluma-1.1/src/pluma/PluginManager.cpp】

void PluginManager::unloadAll(){host.clearProviders();LibMap::iterator it;for (it = libraries.begin() ; it != libraries.end() ; ++it){delete it->second;   // delete掉DLibrary对象}libraries.clear();
}
void Host::clearProviders(){ProvidersMap::iterator it;for (it = knownTypes.begin() ; it != knownTypes.end() ; ++it){std::list<Provider*>& providers = it->second.providers;std::list<Provider*>::iterator provIt;for (provIt = providers.begin() ; provIt != providers.end() ; ++provIt){delete *provIt;   // delete掉Provider对象}std::list<Provider*>().swap(providers);}
}

6. 结束

至此,Pluma架构的主体代码就分析完毕了,希望对大家有所帮助。

说说Pluma插件管理框架相关推荐

  1. 说说 Pluma 插件管理框架

    1. 概述 Pluma 是一个用 C++ 开发的可用于管理插件的开源架构,其官网地址为:http://pluma-framework.sourceforge.net/.该架构是个轻量级架构,非常易于理 ...

  2. 这些用来审计 Kubernetes RBAC 策略的方法你都见过吗?

    点击蓝色"程序猿DD"关注我 回复"资源"获取独家整理的学习资料! >> 「开学季」当当大促!4-5折优惠不了解一下? << 认证与授权 ...

  3. 云上自动化:云上编排让上云更简单

    摘要 本文介绍了为什么在一个好的公有云或私有云中必须要有一个编排系统来支持云上自动化,以及实现这个编排系统的困难和各家的努力.同时提供了一套实现编排系统的原型,它包括了理论分析及主体插件框架,还给出一 ...

  4. 云上自动化 vs 云上编排

    [摘要] 本文介绍了为什么在一个好的公有云或私有云中必须要有一个编排系统来支持云上自动化,以及实现这个编排系统的困难和各家的努力.同时提供了一套实现编排系统的原型,它包括了理论分析及主体插件框架,还给 ...

  5. Rational AppScan 标准版可扩展性和二次开发能力简介

    下载:IBM® Rational® AppScan 标准版  |   Web 应用安全与 IBM Rational AppScan 工具包 获取免费的 Rational 软件工具包系列,下载更多的 R ...

  6. 【.Net平台下插件开发】-MEF与MAF初步调研

    背景 Team希望开发一个插件的平台去让某搜索引擎变得更好.主要用于采集一些不满意信息(DSAT)给Dev.这些信息会由不同的team提供不同的tool分析.有的提供仅仅是一个website,有的提供 ...

  7. 应用上云可以有多快?

    摘要 本文介绍了为什么在一个好的公有云或私有云中必须要有一个编排系统来支持云上自动化,以及实现这个编排系统的困难和各家的努力.同时提供了一套实现编排系统的原型,它包括了理论分析及主体插件框架,还给出一 ...

  8. 深耕图形领域,华为HMS Core图形计算服务提升图形应用表现

    自从华为HMS Core 5.0上线以来,华为持续提升图形图像开发领域的服务能力.其中,华为图形计算服务(简称"CG Kit")将计算机图形学中的前沿技术提供给开发者,并通过对计算 ...

  9. 余额宝大规模服务化的技术创新(全球架构师大会演讲稿整理版)

    2018年7月初我在ArchSummit大会上做了题为<余额宝大规模服务化的技术创新>的分享,反响还不错.现将PPT和讲稿整理出来,分享给大家. 这次分享首先介绍的是余额宝的整体架构变迁历 ...

最新文章

  1. 强烈推荐!最好用的《机器学习实用指南》第二版终于来了,代码已开源!
  2. 为了研究因果关系,原来科学家在这么多方向上都有尝试(附链接)
  3. kettle 数据转换
  4. linux信号掩码线程,20.10 信号掩码(阻塞信号传递)
  5. 利用CSIDL打开特殊文件夹
  6. .NET Core开发实战(第25课:路由与终结点:如何规划好你的Web API)--学习笔记(下)...
  7. C++ Deque(双向队列
  8. MySQL 阿里巴巴JAVA开发手册-MySQL相关
  9. CSS:实现流光按钮类
  10. xp启用计算机共享打印机,Win7如何共享xp的打印机
  11. 联想笔记本电脑u盘重装win10系统教学
  12. 微信小程序中显示HTML格式内容的实例
  13. excel表格合并程序
  14. ffmpeg webm 提取_使用ffmpeg将webm转换为mp4
  15. Linux之jq命令的使用
  16. 2021爱智先行者—红外学习模块(空调遥控器)
  17. Mysql 增量备份和全量备份
  18. 泛海微电动修脚器开发方案和电动修脚器单片机方案
  19. Rocket之消息发送
  20. FPGA中ICAP原语的使用——Multiboot功能的实现

热门文章

  1. 基于opencv实现人脸识别及签到系统
  2. cad化工设备绘图_化工CAD制图基础篇,管道布置图学起来
  3. Python程序员爬出百套美女写真集,同样是爬虫,他为何如此突出?
  4. 微信小程序 主题皮肤切换(switch开关)
  5. Jenkins——Jenkins介绍+基于云平台的Jenkins安装和持续集成环境配置(插件+用户权限+凭据+Maven打包)
  6. ssh整合(spring+springmvc+hibernate)
  7. java静态变量、静态方法、代码块、main方法
  8. iptables防火墙
  9. 企业短信防火墙应用-爱侬「北京市家政服务龙头企业」
  10. 酷骑自行车可以骑啦!!ofo、摩拜没月卡了也不怕!!