1. 程式人生 > >Beego原始碼解析(二)-路由機制

Beego原始碼解析(二)-路由機制

上一篇文章介紹了 Beego關於配置項初始化的流程。那麼今天就來說說在 Beego中非常重要的路由機制.
Beego到現在 v1.6.1版本為止支援了:固定路由正則路由自動路由這三種路由方法.
關於這三種路由的詳細用法可以參考官方給出的開發文件,這裡面已經記錄的很全面了.

所以我們今天這篇文章就是要介紹這三種路由是如何在 Beego內部實現的.

關於 Beego的原始碼註釋可以見我的Github

一個簡單的示例

讓我們先從官網給出的示例開始,下面是會在瀏覽器中列印”HelloWorld”的一個Beego程式.

package main

import
( "github.com/astaxie/beego" ) type MainController struct { beego.Controller } func (this *MainController) Get() { this.Ctx.WriteString("Hello World") } func main() { beego.Router("/",&MainController{}) beego.Run() }

我們需要先知道它幹了什麼:
1. 自定義了一個內含 beego.Controller(這個型別後面會講到)控制器的 MainController
2. 重寫了 MainController的 Get()方法,熟悉 Go語言的應該知道這個方法來自 Controller
3. 在 main()函式中呼叫了 beego.Router()方法註冊了路由”/”和一個 MainController例項
4. 執行了 beego.Run()方法啟用了 beego程式

重要的型別和介面

為了不在接下來的流程中打斷,在介紹流程之前需要先了解 beego中關於路由的一些東西

ControllerInterface 介面

原始檔中的位置: beego/controller.go:90

type ControllerInterface interface {
        Init(ct *context.Context,controllerName,actionName string,app interface{})
        Prepare()
        Get()
        Post()
        Delete()
Put() Head() Patch() Options() Finish() Render() error XSRFToken() string CheckXSRFCookie() bool HandlerFunc(fn string) bool URLMapping() }

這個介面定義了 15個方法,看名字就能夠知道這是每個 Controller都需要實現的介面

Controller結構體

位置 beego/controller.go:60

type Controller struct {

//context data
Ctx  *context.Context
Data map[interface{}]interface{}

//route controller info
controllerName string
actionName     string
methodMapping  map[string]func() //method:routertree
gotofunc       string
AppController  interface{}

// template data
TplName        string
Layout         string
LayoutSections map[string]string // the key is the section name and the value is the template name
TplExt         string
EnableRender   bool

// xsrf data
_xsrfToken string
XSRFExpire int
EnableXSRF bool

// session
CruSession session.Store
}

這個結構體儲存了作為 Controller的一些必要的資訊,一些基礎的欄位看名字就比較好理解
在這裡的 context.Context(Beego中的上下文,封裝了 HTTP的輸入和輸出)和 Session.Store(用於儲存 Session)在以後的文章中會再提到

在這個原始檔中的後面部分都是對 Controller的一些方法實現,我們會注意到 Controller實現了 ControllerInterface的方法,但是在一些方法實現中卻是用 Ctx成員向客戶端進行錯誤輸出(例如 Get()方法)。
因為就像例子中給的一樣,當我們需要自己定義 Controller,並且使用 Get()函式來完成對客戶端 Get請求的處理時,我們就需要自己實現處理邏輯,這樣就覆蓋了本身輸出錯誤的方法.而對於我們沒有實現的方法(比如例子中的 Post()方法)沒有重寫,則對於客戶端的 Post請求就會輸出錯誤了

ControllerRegister結構體

這是一個非常關鍵的資料結構,為什麼說他關鍵呢?我們可以先看下 Beego中 App結構體的定義
位置: beego/app.go

type App struct {
    Handlers *ControllerRegister
    Server *http.Server
}

關於 App需要說下,在程式中 App型別的變數BeeApp(beego/app.go:32)在 init()函式中會呼叫 NewApp()創建出唯一的一個Beego程式例項
可以看到在例子中 main()函式最後呼叫了 beego.Run()函式,這個函式會在設定完hooks(關於回撥方法以後也會介紹)後進入 BeeApp.Run()函式並且在進入這個函式後就會根據配置項開始不同的 HTTP請求的處理(在 ControllerRegister實現的 ServeHTTP()方法中)
App中一共就兩個變數,一個Server(標準包中 http.Server型別,這個不做介紹,需要的可以看 Go語言文件).
另外一個就是 ControllerRegister,這個 ControllerRegister顧名思義就是註冊 Controller的管理器,那麼如何管理的呢?接下來看定義

位置: beego/router.go:115

type ControllerRegister struct {
    routers map[string][]*Tree
    enableFilter bool
    filters map[int][]*FilterRouter
    pool sync.Pool
}

可以看到這篇文章的主角已經出現了, routers就是我們程式執行時所需要的路由表, routers的 key是我們註冊的方法名(例如”get”、”post”等),而 value就是由註冊的路由構建出來的路由樹了(關於路由樹,後面也會講到).

ControllerInfo結構體

這個結構體是用來儲存我們自定義的控制器資訊的,看下定義便知道
位置: beego/router.go:104

type ControllerInfo struct {
    pattern string  //模式
    controllerType reflect.Type//型別
    methods map[string]string//支援的方法
    handler http.Handler//http.Handler介面
    runFunction FilterFunc
    routerType int//路由型別
}

Tree結構體

type Tree struct {
    //路由字首
    prefix string
    //不帶正則的路由
    fixrouters []*Tree
    //萬用字元,如果設定並且查詢 fixrouters失敗時會來查詢 wildcard
    wildcard *Tree
    //葉子節點,如果設定並且查詢 wildcard失敗後會查詢 leaves,裡面儲存了一些正則的資訊
    leaves []*leafInfo
}

leafInfo結構體

type leafInfo struct {
    wildcards []string//萬用字元
    regexps *regexp.Regexp//正則物件
    runObject interface{}//一般儲存得到的 ControllerInfo物件,在處理請求時會返回該物件,並呼叫處理方法
}

這兩個結構體就會構成一顆用來查詢路由的路由樹

路由的註冊過程

在前面的例項中可以看到需要註冊自己的 Controller時使用的是 beego.Router()函式(在官方開發文件中的基礎路由部分也可以使用 beego.Get()方法註冊路由,不過內部與用 beego.Router()註冊方法相比都會使用 addToRouter()函式,所以也是比較相似的)

看下 beego.Router的原型:

beego/app.go:211
func Router(rootpath string,c ControllerInterface,mappingMethods ...string *App) {
        BeeApp.Handlers.Add(rootpath,c,mappingMethods...)
        return BeeApp
}

看到第一個引數是需要註冊路由,而第二個引數是我們自定義實現了 ControllerInterface介面的控制器,第三個就是自定義路由中方法和處理函式的對映關係
函式內部實際呼叫了 App.ControllerRegister的Add()方法來註冊
接下來看看 Add()方法做了什麼:
位置: beego/router.go:144

func (p *ControllerRegister) Add(pattern string, c ControllerInterface, mappingMethods ...string) {
    reflectVal := reflect.ValueOf(c)    //反射獲得 value
    t := reflect.Indirect(reflectVal).Type()//反射獲得 type
    methods := make(map[string]string)
    if len(mappingMethods) > 0 {
        semi := strings.Split(mappingMethods[0], ";")//切分出每個以';'分隔的自定義方法和對應的函式
        for _, v := range semi {
            colon := strings.Split(v, ":")//切分出以':'分隔的方法名和對應的函式,colon[1]為處理的函式名
            if len(colon) != 2 {
                panic("method mapping format is invalid")
            }
            comma := strings.Split(colon[0], ",")//切分出以','分隔的方法名, comma包含了當前需要註冊的所有方法名
            for _, m := range comma {
                if _, ok := HTTPMETHOD[strings.ToUpper(m)]; m == "*" || ok {
                    //如果方法名為萬用字元'*'或者在支援的方法列表中.並使用反射包中的方法獲得一個繫結對應函式的 Value型別
                    //如果返回的值有效,就將當前方法加入到 methods中
                    if val := reflectVal.MethodByName(colon[1]); val.IsValid() {
                        methods[strings.ToUpper(m)] = colon[1]
                    } else {
                    //不支援方法時報錯
                        panic("'" + colon[1] + "' method doesn't exist in the controller " + t.Name())
                    }
                } else {
                        panic(v + " is an invalid method mapping. Method doesn't exist " + m)
                }
            }
        }
    }
    //新增 ControllerInfo型別來儲存此項路由規則
    route := &controllerInfo{}
    route.pattern = pattern
    route.methods = methods
    route.routerType = routerTypeBeego
    route.controllerType = t
    //當傳入的方法名為空時,給當前模式加入所有支援的方法
    if len(methods) == 0 {
        for _, m := range HTTPMETHOD {
            p.addToRouter(m, pattern, route)
        }
    } else {
        //方法名不為空時,判斷是否含有萬用字元 "*"
        for k := range methods {
            if k == "*" {
                for _, m := range HTTPMETHOD {
                    //含有萬用字元,加入所有方法
                    p.addToRouter(m, pattern, route)
                    }
            } else {
                    //只加入指定的方法
                    p.addToRouter(k, pattern, route)
            }
        }
    }
}

這是一個稍微長點的函式,不過通過註釋可以看出這個函式做了幾個工作:
1. 解析了傳入的 mappingMethods,得到其中包含的全部方法
2. 用傳入的4個引數構造出一個 ControllerInfo的例項,而這個例項中就儲存了我們自定的控制器的 reflct.Type型別(可參考ControllerInfo)

在函式的最後呼叫了 ControllerRegister的 addToRouter()方法

位置: beego/router.go:199

func (p *ControllerRegister) addToRouter(method, pattern string, r *controllerInfo) {
    if !BConfig.RouterCaseSensitive {
        pattern = strings.ToLower(pattern)
    }
    if t, ok := p.routers[method]; ok {
        //如果方法對應的路由樹存在就直接新增
        t.AddRouter(pattern, r)
    } else {
        //方法不存在這新建立一個路由樹
        t := NewTree()
        t.AddRouter(pattern, r)
        //設定新方法的路由樹
        p.routers[method] = t
    }
}

這個方法比較短,主要是判斷當前的方法是否在 ControllerRegister的成員 routers所支援的方法中
* 存在就直接插入對應的路由樹
* 否則建立一個新的路由樹

路由樹節點的插入操作就是 Tree.AddRouter()方法

位置: beego/tree.go:206

func (t *Tree) AddRouter(pattern string,runObject interface{}) {
    t.addseg(splitPath(pattern),runObject,nil,"")
}

可以看出它只是把 pattern中的路徑進行了切割(例如”/admin/users”切割成”[“admin”,”users”]”),並返回一個 string型別的陣列切片
那麼接下來的目的就很明確了,我們需要使用 Tree提供的 addseg方法給路由樹新增節點

這個函式也是最終的一個函數了,函式的邏輯可以看註釋

func (t *Tree) addseg(segments []string, route interface{}, wildcards []string, reg string) {
    if len(segments) == 0 {
        if reg != "" {
            //新增 leaves節點,並給 leaves新增正則規則
            t.leaves = append(t.leaves, &leafInfo{runObject: route, wildcards: wildcards, regexps: regexp.MustCompile("^" + reg + "$")})
        } else {
            t.leaves = append(t.leaves, &leafInfo{runObject: route, wildcards: wildcards})
        }
    } else {
        seg := segments[0]
        iswild, params, regexpStr := splitSegment(seg)//splitSegment函式在後面介紹
        // if it's ? meaning can igone this, so add one more rule for it
        if len(params) > 0 && params[0] == ":" {
            //當 params[0]為':'時,代表引數為空,開始解析下一個
            t.addseg(segments[1:], route, wildcards, reg)//遞迴呼叫
            params = params[1:]
        }
        //Rule: /login/*/access match /login/2009/11/access
        //if already has *, and when loop the access, should as a regexpStr
        //全匹配方式,可參考 http://beego.me/docs/mvc/controller/router.md 的正則路由->全匹配方式
        // utils.InSlice()檢查":solat"是否在wildcards中
        if !iswild && utils.InSlice(":splat", wildcards) {
            //如果使用了全匹配方式則繼續使用正則解析
            iswild = true
            regexpStr = seg
        }
        //Rule: /user/:id/*
        if seg == "*" && len(wildcards) > 0 && reg == "" {
            regexpStr = "(.+)"
        }
        //包含有正則表示式 
        if iswild {
            if t.wildcard == nil {
                t.wildcard = NewTree()
            }
            if regexpStr != "" {
                if reg == "" {
                    rr := ""
                    for _, w := range wildcards {
                        if w == ":splat" {
                            rr = rr + "(.+)/"
                        } else {
                            rr = rr + "([^/]+)/"
                        }
                    }
                    regexpStr = rr + regexpStr
                } else {
                    regexpStr = "/" + regexpStr
                }
            } else if reg != "" {
                if seg == "*.*" {
                    regexpStr = "/([^.]+).(.+)"
                    params = params[1:]
                } else {
                    for range params {
                        regexpStr = "/([^/]+)" + regexpStr
                    }
                }
            } else {
                if seg == "*.*" {
                    params = params[1:]
                }
            }
            t.wildcard.addseg(segments[1:], route, append(wildcards, params...), reg+regexpStr)//遞迴呼叫
        } else {
            var subTree *Tree
            for _, sub := range t.fixrouters {
                if sub.prefix == seg {
                    subTree = sub
                    break
                }
            }
            if subTree == nil {
                subTree = NewTree()
                subTree.prefix = seg
                t.fixrouters = append(t.fixrouters, subTree)
            }
            subTree.addseg(segments[1:], route, wildcards, reg)//遞迴呼叫
        }
    }
}

至此路由樹節點新增完成

這裡需要提一下 splitSegment這個函式
位置: beego/tree.go:489

// "admin" -> false, nil, ""
// ":id" -> true, [:id], ""
// "?:id" -> true, [: :id], ""        : meaning can empty
// ":id:int" -> true, [:id], ([0-9]+)
// ":name:string" -> true, [:name], ([\w]+)
// ":id([0-9]+)" -> true, [:id], ([0-9]+)
// ":id([0-9]+)_:name" -> true, [:id :name], ([0-9]+)_(.+)
// "cms_:id_:page.html" -> true, [:id_ :page], cms_(.+)(.+).html
// "cms_:id(.+)_:page.html" -> true, [:id :page], cms_(.+)_(.+).html
// "*" -> true, [:splat], ""
// "*.*" -> true,[. :path :ext], ""      . meaning separator
//正則路由,用於對正則的 Segment進行解析
//當 key中包含正則 返回true,否則返回false
//返回值第二個為不同的引數
//第三個為正則的規則
func splitSegment(key string) (bool, []string, string)

Final

最終我們從呼叫beego.Router()到最後給 ControllerRegister.router成功新增路由樹節點的過程就完成了
總結一下就是註冊路由的過程就是在新增 ControllerRegister中的路由樹的節點,而在 HTTP執行的過程中對這棵樹進行搜尋(這就到樹的搜尋方法了),從而判斷接受到的請求應該怎麼樣的處理(對應的根據 Controller不同的型別呼叫不同的方法)
完成 HTTP請求的正常處理過程:D

如果文章有誤,非常希望能給我提出,好讓我更正 :D