c++ web框架實現之靜態反射實現

MicroDeLe發表於2022-05-20

0 前言

最近在寫web框架,框架寫好後,需要根據網路發來的請求,選擇使用者定義的servlet來處理請求。一個問題就是,我們框架寫好後,是不知道使用者定義了哪些處理請求的類的,怎麼辦?
在java裡有一個叫反射的機制,他允許我們通過傳入類名來建立物件,這樣我們就可以讓使用者在配置檔案裡(java可以用註解,不需要配置檔案實現)宣告處理url的類名。這樣,當我們的框架收到網路請求後,就可以根據使用者在配置檔案的類名去生成物件從而呼叫處理請求的方法。遺憾的是,迄今為止,c++還不支援反射機制,
眾所周知,c++是個造輪子的語言,沒有條件可以創造條件。網上有很多實現方法,我選擇了一個簡單易懂的實現應用到專案,但是沒有搞懂的程式碼不敢引入專案,就花了一點時間研究了一下程式碼,程式碼不長,但實現巧妙,由於是利用靜態實現的,因此執行緒安全。
原文連結https://www.jianshu.com/p/9259609df791
程式碼主要包含3個部分,原文中是用巨集來簡化註冊反射程式碼的,但不好理解,這裡,我把巨集展開來介紹,當然,引入程式碼時,可以直接用原文的程式碼

1. HttpServlet類

首先我們定義一個HttpServlet類,所有使用者定義的處理網路請求類都必須繼承這個類,並實現service方法,這樣,通過多型機制,我們就能通過這個基類的指標去呼叫真正處理業務的子類
HttpServlet程式碼

class HttpServlet {
public:
    virtual void service() = 0;
    virtual ~HttpServlet() = default;
    // 呼叫使用者自定義的程式碼
};

程式碼很簡單,一個虛擬函式service,讓繼承的子類去實現業務處理,供我們呼叫。一個虛解構函式,保證在delete的時候能呼叫子類的虛構函式。

2. 兩個業務實現的類 LoginServlet, IndexServlet

這兩個類是業務的實現類,繼承HttpServlet然後實現service方法, 程式碼如下

// 處理使用者登陸資訊
class LoginServlet : public HttpServlet {
public:
    void service() override {
        // 業務處理程式碼...
        cout << "LoginServlet" << endl;
    }
};

// 處理首頁資訊
class IndexServlet: public HttpServlet {
public:
    void service() override {
        // 業務處理程式碼...
        cout << "IndexServlet" << endl;
    }
};

OK,基本框架就是這樣,當發生網路請求時,我們生成這兩個類的物件,呼叫service方法,來完成網路請求。回到原來的問題,在不改變HttpService的情況下,我們怎麼生成與之對應的處理類呢。
我們希望HttpServlet類中 呼叫使用者自定義的程式碼是這樣的

class HttpServlet {
public:
    virtual void service() = 0;
    virtual ~HttpServlet() = default;

    // 呼叫使用者自定義的程式碼
    HttpServlet *httpServlet = getClassByName("className");   // 通過類名獲取物件
    httpServlet->service();                                   // 呼叫使用者實現的程式碼
    delete httpServlet;                                       // 刪除物件
};

現在問題就在 getClassByName()怎麼實現了

3 引入反射

很容易想到,建立一個map,儲存類名和類的對映,這樣,我們就能通過類名得到類物件了,引發幾個問題:

  1. 什麼時候建立這個字典?
  2. 怎麼儲存物件?

首先是第一個問題:最好的實現是在所有程式碼執行之前建立好對映,但這不太容易實現的,在編譯期間用不了map這樣高階資料結構。那麼,我們退而求其次,在我們伺服器啟動之前建立,好像可以實現,那麼我們怎麼保證呢?靜態變數,對,c++編譯器保證靜態變數的初始化在main函式開始之前。問題解決,我們用靜態變數的方式在man函式執行之前建立好對映關係。我們建立一個類靜態物件,這樣就可以把註冊寫到建構函式裡,當程式執行時,編譯器幫我們構造物件,呼叫建構函式,從而註冊反射,LoginServlet擴充

class LoginServlet : public HttpServlet {
public:
    void service() override {
        cout << "LoginServlet" << endl;
    }

    LoginServlet() = default;

private:
    static LoginServlet LoginServlet_;
    explicit LoginServlet(const string &registName) {
        auto &reflect = Reflect<HttpServlet>::getReflect();
        reflect.addReflect(registName, regist);
    }

    static HttpServlet *regist() {
        return new LoginServlet;
    }
};

LoginServlet LoginServlet::LoginServlet_("login");

現在我們類擴充了:IndexServlet就不貼了,他也有一份相同的程式碼,現在介紹一下為什麼增加這些程式碼

  1. LoginServlet() 由於我們定義了一個有參建構函式,所以需要顯式定義無參構造
  2. static LoginServlet LoginServlet_; 靜態物件,編譯器會在main函式呼叫之前為我們構造物件,呼叫建構函式,我們就在這個建構函式裡完成註冊
  3. explicit LoginServlet(const string &registName) 這個就是我們執行對映的建構函式,main函式之前該建構函式就會執行,裡面有個模板類Reflect<HttpServlet>我們還沒定義,馬上介紹
  4. static HttpServlet *regist() 就一條語句,建立定義的類並返回指標,當我們需要建立物件的時候呼叫這個函式,所以我們只需要儲存這個函式的地址就能隨時隨地建立物件,在explicit LoginServlet(const string &registName) 函式裡,我們就是儲存這個函式的地址,從而能在需要的時候呼叫這個函式,完成物件呼叫。reflect.addFlect(registName, regist);第一個引數是註冊的名字,我們通過這個註冊的名字生成物件,第二個引數就是該函式地址。

4 Reflect類

為了保證通用性,我們使用模板類實現,先貼程式碼再逐個介紹

template<typename T>
class Reflect {
private:
    using LoginType = T *(*)();                   
    unordered_map<string, LoginType> classInfoMap;  

public:
    void addReflect(const string &className, LoginType classType) {
        classInfoMap[className] = classType;
    }

    T *get(const string &className) {
        return classInfoMap[className]();
    }

    static Reflect<T> &getReflect() {
        static Reflect<T> reflect;
        return reflect;
    }
};

ok 先貼程式碼,然後逐條介紹

  1. using LoginType = T *(*)(); 給函式指標起個別名,其實就是上面說的regist函式
  2. unordered_map<string, LoginType> classInfoMap; 儲存對映資訊的字典
  3. void addReflect(const string &className, LoginType classType) 新增對映,其實就是插入一條資料到map裡
  4. T *get(const string &className) 通過類名獲取物件,怎麼做到的呢 classInfoMap[className]獲取regist函式,這個函式上面提過是可以返回物件,那麼classInfoMap[className]()加()就呼叫了這個函式,所以就返回了這個物件的父類指標
  5. static Reflect<T> &getReflect() 單例模式,保證整個程式同一個模板引數只存在一個物件

關鍵的程式碼就這幾行,只是要理解程式碼執行的過程,註冊是在main函式之前完成的

5. 總結

雖然程式碼不長,但理解起來還是有點難度的,這種方式實現簡單,但有個缺點就是,靜態變數的生存週期是初始化到程式結束,也就是說我們註冊用到的靜態類會在程式的整個週期都存在
這個url和類名的對映關係,我們可以讓使用者寫到配置檔案裡,這樣我們來一個請求後,根據請求位置獲取處理該業務的類名,然後根據類名建立物件處理業務
整體流程就是:請求資源位置 -> 獲取類名 -> 建立物件 -> 處理業務

6 整體程式碼

點選檢視程式碼
#include <iostream>
#include <string>
#include <unordered_map>

using namespace std;
template<typename T>
class Reflect {
private:
    using LoginType = T *(*)();
    unordered_map<string, LoginType> classInfoMap;

public:
    void addReflect(const string &className, LoginType classType) {
        classInfoMap[className] = classType;
    }

    T *get(const string &className) {
        return classInfoMap[className]();
    }

    static Reflect<T> &getReflect() {
        static Reflect<T> reflect;
        return reflect;
    }
};

class HttpServlet {
public:
    virtual void service() = 0;
    virtual ~HttpServlet() = default;
    // 呼叫使用者自定義的程式碼
    static void process() {
        auto reflect = Reflect<HttpServlet>::getReflect();
        HttpServlet *httpServlet = reflect.get("login");        // 通過類名獲取物件
        httpServlet->service();                         // 呼叫使用者實現的程式碼
        delete httpServlet;                             // 刪除物件
    }
};

class LoginServlet : public HttpServlet {
public:
    void service() override {
        cout << "使用者自定義程式碼 LoginServlet 執行" << endl;
    }

    LoginServlet() = default;

private:
    explicit LoginServlet(const string &registName) {
        auto &reflect = Reflect<HttpServlet>::getReflect();
        reflect.addReflect(registName, regist);
    }

    static LoginServlet LoginServlet_;

    static HttpServlet *regist() {
        return new LoginServlet;
    }
};

LoginServlet LoginServlet::LoginServlet_("login");

int main() {
    HttpServlet::process();
}

7 執行結果

相關文章