1. 程式人生 > >CTK框架——CTK Plugin Framework快速入門

CTK框架——CTK Plugin Framework快速入門

CTK Plugin Framework

CTK框架——CTK Plugin Framework快速入門

一、CTK Plugin Framework簡介

1、CTK Plugin Framework簡介

CTK Plugin Framework基於Qt Plugin System和Qt Service Framework實現,並且增加了以下特性來擴展:
A、插件元數據(由MANIFEST.MF文件提供);
B、一個定義良好的插件生命周期和上下文;
C、綜合服務發現和註冊;
在Qt Plugin System中,插件的元數據由JSON文件提供。
CTK Plugin Framework的核心架構主要包含兩個組件:Plugin System和Service Registry。這兩個組件是相互關聯的,在API級別上的組合使得系統更加全面、靈活。

A、Plugin System
CTK Core依賴於QtCore模塊,因此CTK Plugin Framework基於Qt Plugin System。Qt API允許在運行時加載和卸載插件,熱插拔功能在CTK Plugin Framework中得到了加強,以支持透明化延遲加載和解決依賴關系。
插件的元數據被編譯進插件內部,可以通過API進行提取。此外,插件系統還使用SQLite緩存了元數據,以避免應用程序加載時間問題。另外,Plugin System支持通過中央註冊中心使用服務。
B、Service Registry
Qt Service Framework是Qt Mobility項目發布的一個Qt 解決方案,Qt服務框架允許“聲明式服務”和按需加載服務實現。為了啟用動態(非持久性)服務,Qt Mobility服務框架可以與Service Registry一起使用。

2、CTK Plugin Framework框架的優點

CTK Plugin Framework以OSGi規範為模型,並實現了幾乎完整的OSGI框架API,因此使用CTK Plugin Framework開發基於Qt的C++應用程序有如下優點:
A、降低復雜性
使用CTK Plugin Framework進行應用開發只需進行插件開發,插件隱藏了內部實現,並通過定義良好的服務來和其它插件通信。隱藏內部機制意味著可以自由地更改實現,不僅有助於Bug數量的減少,還使得插件的開發變得更加簡單,因為只需要實現已經定義好的一定數量的功能接口即可。
B、可復用
標準化的組件模型,在應用程序中使用第三方組件變得非常簡單。

C、版本控制
在CTK Plugin Framework中,所有的插件都經過嚴格的版本控制,只有能夠協作的插件才會被連接在一起。
D、動態更新
OSGi組件模型是一個動態模型,插件可以在不關閉整個系統的情況下被安裝、啟動、停止、更新和卸載。
E、自適應
OSGi組件模型是從頭設計的,以允許組件的混合和匹配,要求必須指定組件的依賴關系,並且需要組件在其可選依賴性並不總是可用的環境中生存。Service Registry是一個動態註冊表,其中插件可以註冊、獲取和監聽服務。OSGI動態服務模型允許插件找出系統中可用的功能,並調整它們所能提供的功能,使得代碼更加靈活, 並且能夠更好地適應變化。
F、透明性
插件和服務是CTK插件環境中的一等公民。管理API提供了對插件的內部狀態的訪問,以及插件之間的連接方式。可以停止部分應用程序來調試某個問題,或者可以引入診斷插件。
G、開發簡單
CTK插件相關的API非常簡單,核心API不到25個類。核心API足以編寫插件、安裝、啟動、停止、更新和卸載,並且還包含了所有的監聽類。
CTK Plugin Framework不僅僅是組件的標準,還指定了如何安裝和管理組件的API。API可以被插件用來提供一個管理代理,管理代理可以非常簡單,如命令shell、圖形桌面應用程序、Amazon EC2的雲計算接口、或IBM Tivoli管理系統。標準化的管理API 使得在現有和未來的系統中集成CTK Plugin Framework變得非常容易。
H、懶加載
OSGi技術有很多的機制來保證只有當類真正需要的時候才開始加載插件。例如,插件可以用餓漢式啟動,但是也可以被配置為僅當其它插件使用它們時才啟動。服務可以被註冊,但只有在使用時才創建。懶加載場景可以節省大量的運行時成本。
I、非獨占性
CTK Plugin Framework不會接管整個應用程序,可以選擇性地將所提供的功能暴露給應用程序的某些部分,或者甚至可以在同一個進程中運行該框架的多個實例。
J、非侵入
在一個CTK插件環境中,不同插件均有自己的環境。插件可以使用任何設施,框架對此並無限制。CTK服務沒有特殊的接口需求,每個QObject都可以作為一個服務,每個類(包括非QObject)都可以作為一個接口。

二、CTK Plugin Framework架構

1、CTK Plugin Framework簡介

CTK Plugin Framework設計參考了OSGi(Java的動態組件系統),並提供了一種能讓應用程序(動態地)由許多不同的可重用組件組成的開發模型,允許通過服務進行通信。
OSGi規範的核心部分是一個框架,核心框架定義了應用程序的生命周期模式和服務註冊。基於OSGI核心框架定義了大量的OSGi服務:日誌、配置管理、HTTP(運行servlet)、XML分析、設備訪問、軟件包管理、許可管理、用戶管理、IO連接、連線管理、Jini和UPnP。
CTK Plugin Framework框架的分層模型如下:
技術分享圖片
A、Plugins(插件):由開發人員創建的CTK組件;
B、Services Layer(服務層):通過為C++對象提供一個
publish-find-bind模型,以動態方式連接插件;
C、Life Cycle Layer(生命周期層):用於安裝、啟動、停止、更新和卸載插件的API;
D、Security(安全性):處理安全方面。

2、插件模塊層

Plugin是CTK Plugin Framework的核心,是模塊化特性的體現。
插件由插件激活器類Activator啟動,激活器可以獲取代表插件框架的插件上下文環境,插件上下文對象不能共享。
技術分享圖片
Plugin是基於C++/Qt的一個共享庫,包含了資源文件和元數據(metadata)。
元數據的目的在於準確描述Plugin的特征,除了讓CTK Plugin Framework對Plugin適當地進行各種處理(例如:依賴解析)外,還能更好的對Plugin進行標識,以幫助用戶對Plugin進行理解。
技術分享圖片
元數據被定義在MANIFEST.MF文件中,典型的MANIFEST.MF文件如下:

Plugin-SymbolicName: HelloCTK
Plugin-ActivationPolicy: eager
Plugin-Category: demo
Plugin-ContactAddress: https://github.com/scorpiostudio
Plugin-Description: A plugin for say hello
Plugin-Name: HelloCTK
Plugin-Vendor: scorpio
Plugin-Version: 1.0.0

元數據主要分為兩部分:
A、Plugin的標識符(必須):唯一標識一個 Plugin,由Plugin-SymbolicName表示。
B、可讀信息(可選):幫助更好地理解和使用Plugin,不對模塊化特性產生任何的影響。可選信息如Plugin-Name、Plugin-Vendor。
3、服務層
CTK插件框架提供了插件間通信的動態服務模型,一個激活的插件可以在任何時候註冊(註銷)0個或多個服務到框架。服務註冊是一個具有可選註冊屬性的發布接口。通過接口和過濾表達式可以從插件框架獲得服務引用。框架發布服務生命周期事件。
服務可以通過ctkPluginContext對象註冊到插件框架。服務的註冊和註銷可以在任何時候進行。
服務是服務的提供者和使用者之間的一個契約,使用者一般不關心其實現的細節,只關心是否滿足契約(服務應該提供什麽功能、滿足什麽格式)。使用服務的過程包含了發現服務和達成協議的形式,即需要通過服務的標誌性特征來找到對應的服務。
一個插件可以創建一個對象,並在一個或多個接口(通常是一個只有純虛方法的C++類)下使用CTK Service Registry註冊它。其它插件可以要求registry列出在特定接口下註冊的所有服務(對象)。一個插件甚至可以等待一個特定的服務出現,然後收到回復。
因此,一個插件可以註冊一個服務,也可以獲得一個服務並偵聽服務的出現或消失。任意數量的插件可以在相同的接口下註冊服務,並且任意數量的插件都可以得到相同的服務。publish-find-bind模型如下:
技術分享圖片
如果多個插件在同一個接口下註冊對象,則可以通過其屬性進行區分。每個服務註冊都有一套標準的自定義屬性,可以使用過濾器來選擇感興趣的服務。屬性也可以被用於應用程序級的其他角色。
A、發布服務
為了讓其它Plugin能發現服務,必須用上下文對其進行註冊,需要用到接口名、服務對象(接口的具體實現)和一個可選的ctkDictionary類型的屬性信息:
ctkDictionary properties;
properties.insert("name", "scorpio");
properties.insert("age", 30);
ctkServiceRegistration registration = context->registerService<HelloService>(new HelloServiceImpl(), properties);
得到一個ctkServiceRegistration對象,用於更新服務的屬性:
registration.setProperties(newProperties);
註銷服務:
registration.unregister();
registration對象不能和其它Plugin共享,因為registration對象和發布服務的Plugin的生命周期相互依存。如果Plugin已經不存在於框架執行環境中,那麽registration對象也不應該存在。
此外,如果在刪除發布的服務前Plugin停止,框架會幫助刪除這些服務。
B、獲取服務
一旦服務被發布,服務將對其他Plugin可用。獲取服務的方式非常簡單,只需要提供一個接口名即可:

ctkServiceReference reference = context->getServiceReference<HelloService>();

reference對象是服務對象的間接引用。間接引用可以將服務的使用和服務的實現進行解耦。將服務註冊表作為兩者的中間人,不僅能夠達到跟蹤和控制服務的目的,同時還可以在服務消失以後通知使用者。
接口的返回類型是ctkServiceReference,可以在Plugin之間互享,因為reference對象和使用服務的Plugin的生命周期無關。

4、生命周期層

生命周期層主要用於控制Plugin的安裝、啟動、停止、更新和卸載,可以從外部管理應用或者建立能夠自我管理的應用(或將兩者相結合),並且給了應用本身很大的動態性。
Plugin的使用需要使用生命周期層的API來和CTK Plugin Framework的生命周期層進行交互。
Plugin生命周期的狀態轉換圖:
技術分享圖片
生命周期層的API主要由三個核心部分組成:ctkPluginActivator、ctkPluginContext和ctkPlugin。
(1)、ctkPluginActivator
ctkPluginActivator:自定義plugin的啟動和停止。
ctkPluginActivator是一個接口,必須由框架中的每個插件實現。插件必須提供一個由插件框架調用的插件激活器類。框架可以根據需要創建一個插件的ctkPluginActivator實例。如果一個實例的ctkPluginActivator::start()方法成功執行,則需要保證在插件停止時調用同一個實例的ctkPluginActivator::stop() 方法。
當插件進入ACTIVE狀態時,框架會調用start方法,當插件離開ACTIVE狀態時,插件框架會調用stop方法。每一個插件都會接收到一個訪問插件框架的唯一ctkPluginContext對象。
(2)、ctkPluginContext
ctkPluginContext是一個plugin在框架內的執行上下文,用於授予對其它方法的訪問,以便該插件可以與框架交互。
ctkPluginContext提供的方法允許插件:
A、訂閱由框架發布的事件;
B、使用Framework Service Registry註冊服務對象;
C、從Framework Service Registry檢索ServiceReferences;
D、為引用的服務獲取和發布服務對象;
E、在框架中安裝新的插件;
F、獲取框架中安裝的插件列表;
G、獲得一個插件的ctkPlugin對象;
H、為(由框架為插件提供的)持久存儲區域中為文件創建QFile對象。
當使用ctkPluginActivator::start()方法啟動時,將創建一個 ctkPluginContext對象,並將其提供給與此上下文關聯的插件。當使用ctkPluginActivator::stop()方法停止時,相同的ctkPluginContext對象將被傳遞給與此上下文關聯的插件。ctkPluginContext對象通常用於其關聯插件的私有用途,並不意味著與插件環境中的其它插件共享。
與ctkPluginContext對象關聯的ctkPlugin對象稱為上下文插件。
ctkPluginContext對象只有在它的上下文插件執行時才有效;即在上下文插件處於STARTING、STOPPING和ACTIVE狀態的時段內。如果隨後使用ctkPluginContext對象,則必須拋出一個ctkIllegalStateException異常。當上下文插件停止後,ctkPluginContext對象不能被重用。
Framework是唯一能夠創建ctkPluginContext對象的實體,並且ctkPluginContext對象只在創建它們的Framework中有效。
(3)、ctkPlugin
ctkPlugin是Framework中已安裝的插件。
ctkPlugin對象是定義一個已安裝插件的生命周期的訪問點,在插件環境中安裝的每個插件都必須有一個相關的ctkPlugin對象。此外,插件必須有一個唯一的標識,在插件的生命周期中,唯一標識不能改變(即使是在插件更新時),卸載和重新安裝插件必須創建一個新的唯一標識。
插件有以下狀態(狀態是動態可變的,在特定條件下可以互相轉換):
UNINSTALLED
INSTALLED
RESOLVED
STARTING
STOPPING
ACTIVE
要確定插件是否處於有效狀態之一,可以使用States類型進行“或”運算。
插件只能在狀態為STARTING、ACTIVE或STOPPING狀態時執行代碼。一個UNINSTALLED插件是一個僵屍,不能被設置為另一個狀態。
框架是唯一允許創建ctkPlugin對象的實體,並且ctkPlugin對象僅在創建它們的框架內有效。

三、CTK Plugin Framework加載插件

1、CTK Plugin Framework使用流程

CTK插件框架的使用流程如下:
A、初始化並啟動插件框架:由ctkPluginFramework::init() 和ctkPluginFramework::start()完成;
B、獲取上下文:由ctkPluginFramework::getPluginContext()完成
C、安裝插件:由ctkPluginContext::installPlugin()完成,返回一個ctkPlugin對象。
D、啟動插件:由ctkPlugin::start()完成。
E、獲取服務引用:由ctkPluginContext::getServiceReference()完成。
F、獲取指定ctkServiceReference引用的服務對象:由ctkPluginContext::getService()完成。
G、調用服務。

2、創建CTK插件

本節創建一個簡單的插件。
接口定義HelloService.h如下:

#ifndef HELLOSERVICE_H
#define HELLOSERVICE_H

#include <QtPlugin>

class HelloService
{
public:
    virtual ~HelloService() {}
    virtual void printHello() = 0;
};

#define HelloService_iid "org.commontk.service.demo.HelloService"
Q_DECLARE_INTERFACE(HelloService, HelloService_iid)

#endif // HELLOSERVICE_H

接口實現:
HelloServiceImpl.h文件:

#ifndef HELLOSERVICEIMPL_H
#define HELLOSERVICEIMPL_H

#include <QObject>
#include "HelloService.h"
#include <ctkPluginContext.h>

class HelloServiceImpl : public QObject, public HelloService
{
    Q_OBJECT
    Q_INTERFACES(HelloService)
public:
    HelloServiceImpl(ctkPluginContext* context);
    void printHello();
};

#endif // HELLOSERVICEIMPL_H

HelloServiceImpl.cpp文件:

#include "HelloServiceImpl.h"
#include <ctkPluginContext.h>
#include <QtDebug>

HelloServiceImpl::HelloServiceImpl(ctkPluginContext *context)
{
    context->registerService<HelloService>(this);
}

void HelloServiceImpl::printHello()
{
    qDebug() << "Hello,CTK Plugin!";
}

插件實現:
HelloCTKPlugin.h文件:

#ifndef HELLOCTKPLUGIN_H
#define HELLOCTKPLUGIN_H

#include <QObject>
#include <QtPlugin>
#include <ctkPluginActivator.h>
#include <QScopedPointer>
#include "HelloService.h"

class HelloCTKPlugin : public QObject, public ctkPluginActivator
{
    Q_OBJECT
    Q_INTERFACES(ctkPluginActivator)
    //Qt5版本
#if(QT_VERSION >= QT_VERSION_CHECK(5, 0, 0))
    Q_PLUGIN_METADATA(IID "HelloCTKPlugin")
#endif
public:
    void start(ctkPluginContext* context);
    void stop(ctkPluginContext* context);
private:
    QScopedPointer<HelloService> m_service;

};

#endif // HELLOCTKPLUGIN_H

HelloCTKPlugin.cpp文件:

#include "HelloCTKPlugin.h"
#include "HelloServiceImpl.h"

void HelloCTKPlugin::start(ctkPluginContext *context)
{
    m_service.reset(new HelloServiceImpl(context));
}

void HelloCTKPlugin::stop(ctkPluginContext *context)
{
    Q_UNUSED(context)
}
//Qt4版本
#if(QT_VERSION < QT_VERSION_CHECK(5, 0, 0))
Q_EXPORT_PLUGIN2(HelloCTK, HelloCTKPlugin)
#endif

插件元數據文件MANIFEST.MF:

Plugin-SymbolicName: HelloCTKPlugin
Plugin-ActivationPolicy: eager
Plugin-Category: demo
Plugin-ContactAddress: https://github.com/scorpiostudio
Plugin-Description: A plugin for print hello
Plugin-Name: HelloCTKPlugin
Plugin-Vendor: scorpio
Plugin-Version: 1.0.0

QRC資源文件resource.qrc:

<RCC>
    <qresource prefix="/HelloCTKPlugin/META-INF">
        <file>MANIFEST.MF</file>
    </qresource>
</RCC>

插件工程文件HelloCTK.pro:

QT += core
QT -= gui

TARGET = HelloCTKPlugin
CONFIG += plugin
TEMPLATE = lib

# CTK 安裝路徑
CTK_INSTALL_PATH = /usr/local/CTK

# CTK插件相關庫所在路徑(CTKCore.lib、CTKPluginFramework.lib)
CTK_LIB_PATH = $$CTK_INSTALL_PATH/lib/ctk-0.1

# CTK插件相關頭文件所在路徑(ctkPluginFramework.h)
CTK_INCLUDE_PATH = $$CTK_INSTALL_PATH/include/ctk-0.1

LIBS += -L$$CTK_LIB_PATH -lCTKCore -lCTKPluginFramework

INCLUDEPATH += $$CTK_INCLUDE_PATH

HEADERS +=     HelloService.h     HelloServiceImpl.h     HelloCTKPlugin.h

SOURCES +=     HelloServiceImpl.cpp     HelloCTKPlugin.cpp

RESOURCES +=     Resource.qrc

工程樹如下:
技術分享圖片

3、CTK插件框架使用實例

本例以HelloCTKPlugin插件為例,演示如何使用CTK Plugin Framework來加載插件並獲取特定的服務。
工程文件如下:

QT       += core
QT       -= gui
TARGET = test
CONFIG   += console
CONFIG   -= app_bundle

TEMPLATE = app

#CTK安裝路徑
CTK_INSTALL_PATH = /usr/local/CTK

#CTK插件相關庫所在路徑(CTKCore.lib、CTKPluginFramework.lib)
CTK_LIB_PATH = $$CTK_INSTALL_PATH/lib/ctk-0.1

#CTK插件相關頭文件所在路徑(ctkPluginFramework.h)
CTK_INCLUDE_PATH = $$CTK_INSTALL_PATH/include/ctk-0.1

LIBS += -L$$CTK_LIB_PATH -lCTKCore -lCTKPluginFramework
INCLUDEPATH += $$CTK_INCLUDE_PATH

SOURCES += main.cpp

main.cpp文件:

#include <QCoreApplication>
#include <QDirIterator>
#include <QtDebug>

#include <ctkPluginFrameworkFactory.h>
#include <ctkPluginFramework.h>
#include <ctkPluginException.h>
#include <ctkPluginContext.h>

#include "../HelloCTK/HelloService.h"

int main(int argc, char *argv[])
{
    QCoreApplication app(argc, argv);

    ctkPluginFrameworkFactory frameWorkFactory;
    QSharedPointer<ctkPluginFramework> framework = frameWorkFactory.getFramework();
    try
    {
        // 初始化並啟動插件框架
        framework->init();
        framework->start();
        qDebug() << "CTK Plugin Framework start ...";
    }
    catch (const ctkPluginException &e)
    {
        qDebug() << "Failed to initialize the plugin framework: " << e.what();
        return -1;
    }

    // 獲取插件上下文
    ctkPluginContext* context = framework->getPluginContext();

    // 獲取插件所在位置
    QString path = QCoreApplication::applicationDirPath() + "/plugins";

    // 遍歷路徑下的所有插件
    QDirIterator itPlugin(path, QStringList() << "*.dll" << "*.so", QDir::Files);
    while (itPlugin.hasNext())
    {
        QString strPlugin = itPlugin.next();
        try
        {
            // 安裝插件
            QSharedPointer<ctkPlugin> plugin = context->installPlugin(QUrl::fromLocalFile(strPlugin));
            // 啟動插件
            plugin->start(ctkPlugin::START_TRANSIENT);
            qDebug() << QString("Plugin %1 start ...").arg(
                         plugin.data()->getSymbolicName());
        }
        catch (const ctkPluginException &e)
        {
            qDebug() << "Failed to install plugin" << e.what();
            return -1;
        }
    }

    // 獲取服務引用
    ctkServiceReference reference = context->getServiceReference<HelloService>();
    if (reference)
    {
        // 獲取指定 ctkServiceReference 引用的服務對象
        HelloService* service = qobject_cast<HelloService *>(
                                    context->getService(reference));
        if (service != NULL)
        {
            // 調用服務
            service->printHello();
        }
    }

    return app.exec();
}

ctkPluginContext是框架內一個插件的執行上下文,ctkPluginContext用於授予對其它方法的訪問權,以便插件可以與框架進行交互。使用ctkPluginContext對象可以安裝新的插件,並獲得發布服務對象。
在運行程序前,需要在構建目錄中(或可執行程序同級目錄)創建一個plugins目錄,用於專門存放插件。
將HelloCTKPlugin插件libHelloCTKPlugin.so放到plugins目錄下,執行結果如下:
技術分享圖片
HelloCTKPlugin插件已經可以對外提供服務。

CTK框架——CTK Plugin Framework快速入門