Beego源碼分析(轉)

摘要html

beego 是 @astaxie 開發的重量級Go語言Web框架。它有標準的MVC模式,完善的功能模塊,和優異的調試和開發模式等特色。而且beego在國內企業用戶較多,社區發達和Q羣,文檔齊全,特別是 @astaxie 本人對bug和issue等回覆和代碼修復很快,很是敬業。beego框架自己模塊衆多,沒法簡單描述全部的功能。我簡單閱讀了源碼,記錄一下beego執行過程。官方文檔已經圖示了beego執行過程圖,而我會比較詳細的解釋beego的源碼實現。

beego 是 @astaxie 開發的重量級Go語言Web框架。它有標準的MVC模式,完善的功能模塊,和優異的調試和開發模式等特色。而且beego在國內企業用戶較多,社區發達和Q羣,文檔齊全,特別是 @astaxie 本人對bug和issue等回覆和代碼修復很快,很是敬業。beego框架自己模塊衆多,沒法簡單描述全部的功能。我簡單閱讀了源碼,記錄一下beego執行過程。官方文檔已經圖示了beego執行過程,而我會比較詳細的解釋beego的源碼實現。 <!--more--> 注意,本文基於beego 1.1.4 (2014.04.15) 源碼分析,且不是beego的使用教程。使用細節的問題在這裏不會說明。git

本文小站地址:http://fuxiaohei.me/article/27/beego-source-study.htmlgithub

1. 啓動應用

beego官方首頁提供的示例很是簡單:json

package main import "github.com/astaxie/beego" func main() { beego.Run() } 

那麼,從Run()方法開始,在beego.go#179瀏覽器

func Run() { initBeforeHttpRun() if EnableAdmin { go beeAdminApp.Run() } BeeApp.Run() } 

額呵呵呵,還在更裏面,先看initBeforeHttpRun(),在beego.go#L189:cookie

func initBeforeHttpRun() { // if AppConfigPath not In the conf/app.conf reParse config if AppConfigPath != filepath.Join(AppPath, "conf", "app.conf") { err := ParseConfig() if err != nil && AppConfigPath != filepath.Join(workPath, "conf", "app.conf") { // configuration is critical to app, panic here if parse failed panic(err) } } // do hooks function for _, hk := range hooks { err := hk() if err != nil { panic(err) } } if SessionOn { var err error sessionConfig := AppConfig.String("sessionConfig") if sessionConfig == "" { sessionConfig = `{"cookieName":"` + SessionName + `",` + `"gclifetime":` + strconv.FormatInt(SessionGCMaxLifetime, 10) + `,` + `"providerConfig":"` + SessionSavePath + `",` + `"secure":` + strconv.FormatBool(HttpTLS) + `,` + `"sessionIDHashFunc":"` + SessionHashFunc + `",` + `"sessionIDHashKey":"` + SessionHashKey + `",` + `"enableSetCookie":` + strconv.FormatBool(SessionAutoSetCookie) + `,` + `"cookieLifeTime":` + strconv.Itoa(SessionCookieLifeTime) + `}` } GlobalSessions, err = session.NewManager(SessionProvider, sessionConfig) if err != nil { panic(err) } go GlobalSessions.GC() } err := BuildTemplate(ViewsPath) if err != nil { if RunMode == "dev" { Warn(err) } } middleware.VERSION = VERSION middleware.AppName = AppName middleware.RegisterErrorHandler() } 

從代碼看到在Run()的第一步,初始化AppConfig,調用hooks,初始化GlobalSessions,編譯模板BuildTemplate(),和加載中間件middleware.RegisterErrorHandler(),分別簡單敘述。session

1.1 加載配置

加載配置的代碼是:mvc

if AppConfigPath != filepath.Join(AppPath, "conf", "app.conf") { err := ParseConfig() if err != nil && AppConfigPath != filepath.Join(workPath, "conf", "app.conf") { // configuration is critical to app, panic here if parse failed panic(err) } } 

判斷配置文件是否是AppPath/conf/app.conf,若是不是就ParseConfig()。顯然他以前就已經加載過一次了。找了一下,在config.go#L152,具體加載什麼就不說明了。須要說明的是AppPathworkPath這倆變量。找到定義config.go#72app

workPath, _ = os.Getwd()
workPath, _ = filepath.Abs(workPath)
// initialize default configurations AppPath, _ = filepath.Abs(filepath.Dir(os.Args[0])) AppConfigPath = filepath.Join(AppPath, "conf", "app.conf") if workPath != AppPath { if utils.FileExists(AppConfigPath) { os.Chdir(AppPath) } else { AppConfigPath = filepath.Join(workPath, "conf", "app.conf") } } 

workPathos.Getwd(),即當前的目錄;AppPathos.Args[0],即二進制文件所在目錄。有些狀況下這兩個是不一樣的。好比把命令加到PATH中,而後cd到別的目錄執行。beego以二進制文件所在目錄爲優先。若是二進制文件所在目錄沒有發現conf/app.conf,再去workPath裏找。框架

1.2 Hooks

hooks就是鉤子,在加載配置後就執行,這是要作啥呢?在 beego.go#L173 添加新的hook:

// The hookfunc will run in beego.Run() // such as sessionInit, middlerware start, buildtemplate, admin start func AddAPPStartHook(hf hookfunc) { hooks = append(hooks, hf) } 

hooks的定義在beego.go#L19

type hookfunc func() error //hook function to run var hooks []hookfunc //hook function slice to store the hookfunc 

hook就是func() error類型的函數。那麼爲何調用hooks能夠實現代碼註釋中的如middleware start, build template呢?由於beego使用的是單實例的模式。

1.3 單實例

beego的核心結構是beego.APP,保存路由調度結構*beego.ControllerRegistor。從beego.Run()方法的代碼BeeApp.Run()發現,beego有一個全局變量BeeApp是實際調用的*beego.APP實例。也就是說整個beego就是一個實例,不須要相似NewApp()這樣的寫法。

所以,不少結構都做爲全局變量如beego.BeeApp暴露在外。詳細的定義在 config.go#L18,特別注意一下SessionProvider(string),立刻就要提到。

1.4 會話 GlobalSessions

繼續beego.Run()的閱讀,hooks調用完畢後,初始化會話GlobalSessions

if SessionOn { var err error sessionConfig := AppConfig.String("sessionConfig") if sessionConfig == "" { sessionConfig = `{"cookieName":"` + SessionName + `",` + `"gclifetime":` + strconv.FormatInt(SessionGCMaxLifetime, 10) + `,` + `"providerConfig":"` + SessionSavePath + `",` + `"secure":` + strconv.FormatBool(HttpTLS) + `,` + `"sessionIDHashFunc":"` + SessionHashFunc + `",` + `"sessionIDHashKey":"` + SessionHashKey + `",` + `"enableSetCookie":` + strconv.FormatBool(SessionAutoSetCookie) + `,` + `"cookieLifeTime":` + strconv.Itoa(SessionCookieLifeTime) + `}` } GlobalSessions, err = session.NewManager(SessionProvider, sessionConfig) if err != nil { panic(err) } go GlobalSessions.GC() } 

beego.SessionOn定義是否啓動Session功能,而後sessionConfig是Session的配置,若是配置爲空,就使用拼接的默認配置。sessionConfig是json格式。

session.NewManager()返回*session.Manager,session的數據存儲引擎是beego.SessionProvider定義,好比"file",文件存儲。

go GlobalSessions.GC()開啓一個goroutine來處理session的回收。閱讀一下GC()的代碼,在session/session.go#L183

func (manager *Manager) GC() { manager.provider.SessionGC() time.AfterFunc(time.Duration(manager.config.Gclifetime)*time.Second, func() { manager.GC() }) } 

這是個無限循環time.AfterFunc()在通過一段時間間隔time.Duration(...)以後,又調用本身,至關於又開始啓動time.AfterFunc()等待下一次到期。manager.provider.SessionGC()是不一樣session存儲引擎的回收方法(實際上是session.Provider接口的)。

1.5 模板構建

繼續beego.Run(),session初始化後,構建模板:

err := BuildTemplate(ViewsPath)

beego.ViewsPath是模板的目錄啦,很少說。仔細來看看BuildTemplate()函數,template.goL#114

// build all template files in a directory. // it makes beego can render any template file in view directory. func BuildTemplate(dir string) error { if _, err := os.Stat(dir); err != nil { if os.IsNotExist(err) { return nil } else { return errors.New("dir open err") } } self := &templatefile{ root: dir, files: make(map[string][]string), } err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { return self.visit(path, f, err) }) if err != nil { fmt.Printf("filepath.Walk() returned %v\n", err) return err } for _, v := range self.files { for _, file := range v { t, err := getTemplate(self.root, file, v...) if err != nil { Trace("parse template err:", file, err) } else { BeeTemplates[file] = t } } } return nil } 

比較複雜。一點點來看,os.Stat(dir)判斷目錄是否存在。filepath.Walk()走一邊目錄裏的文件,記錄在self.files裏面。循環self.files中的file(map[dir][]file]),用getTemplate獲取*template.Template實例,保存在beego.BeeTemplates(map[string]*template.Template)。

爲何要預先編譯模板?想像一下,若是每次請求,都去尋找模板再編譯一遍。這顯然是個浪費的。並且若是模板複雜,嵌套衆多,編譯速度會是很大的問題。所以存下編譯好的*template.Template是必然的選擇。可是,編譯後模板的修改不能當即響應了,怎麼辦呢?先繼續看下去。

1.6 中間件

middleware包目前彷佛只有錯誤處理的功能。

middleware.RegisterErrorHandler()

只是註冊默認的錯誤處理方法 middleware.NotFound 等幾個。

1.7 beeAdminApp

if EnableAdmin { go beeAdminApp.Run() } 

beeAdminApp也是一個*beego.adminApp,負責系統監控、性能檢測、訪問統計和健康檢查等。具體的介紹和使用能夠訪問文檔

2. HTTP服務

寫了這麼多,終於要開始講核心結構beego.BeeApp的啓動:

BeeApp.Run()

Run()的實現代碼在app.go#L29。代碼較長,看看最重要的一段:

if UseFcgi { if HttpPort == 0 { l, err = net.Listen("unix", addr) } else { l, err = net.Listen("tcp", addr) } if err != nil { BeeLogger.Critical("Listen: ", err) } err = fcgi.Serve(l, app.Handlers) } else { if EnableHotUpdate { server := &http.Server{ Handler: app.Handlers, ReadTimeout: time.Duration(HttpServerTimeOut) * time.Second, WriteTimeout: time.Duration(HttpServerTimeOut) * time.Second, } laddr, err := net.ResolveTCPAddr("tcp", addr) if nil != err { BeeLogger.Critical("ResolveTCPAddr:", err) } l, err = GetInitListener(laddr) theStoppable = newStoppable(l) err = server.Serve(theStoppable) theStoppable.wg.Wait() CloseSelf() } else { s := &http.Server{ Addr: addr, Handler: app.Handlers, ReadTimeout: time.Duration(HttpServerTimeOut) * time.Second, WriteTimeout: time.Duration(HttpServerTimeOut) * time.Second, } if HttpTLS { err = s.ListenAndServeTLS(HttpCertFile, HttpKeyFile) } else { err = s.ListenAndServe() } } } 

beego.UseFcgi定義是否使用fast-cgi服務,而不是HTTP。另外一部分是啓動HTTP。裏面有個重要功能EnableHotUpdate————熱更新。對他的描述,能夠看看官方文檔

2.1 HTTP過程總覽

上面的代碼看獲得*http.Server.Handlerapp.Handlers,即*beego.ControllerRegistorServeHTTP就定義在代碼router.go#L431。很是長,咱們檢出重要的部分來講說。

首先是要建立當前請求的上下文:

// init context context := &beecontext.Context{ ResponseWriter: w, Request: r, Input: beecontext.NewInput(r), Output: beecontext.NewOutput(), } context.Output.Context = context context.Output.EnableGzip = EnableGzip 

context的類型是*context.Context,把當前的w(http.ResponseWriter)r(*http.Request)寫在context的字段中。

而後,定義了過濾器filter的調用方法,把context傳遞給過濾器操做:

do_filter := func(pos int) (started bool) { if p.enableFilter { if l, ok := p.filters[pos]; ok { for _, filterR := range l { if ok, p := filterR.ValidRouter(r.URL.Path); ok { context.Input.Params = p filterR.filterFunc(context) if w.started { return true } } } } } return false } 

而後,加載Session:

if SessionOn { context.Input.CruSession = GlobalSessions.SessionStart(w, r) defer func() { context.Input.CruSession.SessionRelease(w) }() } 

defer中的SessionRelease()是將session持久化到存儲引擎中,好比寫入文件保存。

而後,判斷請求方式是否支持:

if !utils.InSlice(strings.ToLower(r.Method), HTTPMETHOD) { http.Error(w, "Method Not Allowed", 405) goto Admin } 

這裏看一看到 goto Admin,就是執行AdminApp的監控操做,記錄此次請求的相關信息。Admin定義在整個HTTP執行的最後:

Admin:
    //admin module record QPS if EnableAdmin { timeend := time.Since(starttime) if FilterMonitorFunc(r.Method, requestPath, timeend) { if runrouter != nil { go toolbox.StatisticsMap.AddStatistics(r.Method, requestPath, runrouter.Name(), timeend) } else { go toolbox.StatisticsMap.AddStatistics(r.Method, requestPath, "", timeend) } } } 

因此goto Admin直接就跳過中間過程,走到HTTP執行的最後了。顯然,當請求方式不支持的時候,直接跳到HTTP執行最後。若是不啓用AdminApp,那就是HTTP執行過程結束。

繼續閱讀,開始處理靜態文件了:

if serverStaticRouter(context) { goto Admin } 

而後處理POST請求的內容體:

if context.Input.IsPost() { if CopyRequestBody && !context.Input.IsUpload() { context.Input.CopyBody() } context.Input.ParseFormOrMulitForm(MaxMemory) } 

執行兩個前置的過濾器:

if do_filter(BeforeRouter) { goto Admin } if do_filter(AfterStatic) { goto Admin } 

不過我以爲這倆順序怪怪的,應該先AfterStaticBeforeRouter。須要注意,過濾器若是返回false,整個執行就結束(跳到最後)。

繼續閱讀,而後判斷有沒有指定執行的控制器和方法:

if context.Input.RunController != nil && context.Input.RunMethod != "" { findrouter = true runMethod = context.Input.RunMethod runrouter = context.Input.RunController } 

若是過濾器執行後,對context指定了執行的控制器和方法,就用指定的。

繼續,路由的尋找開始,有三種路由:

if !findrouter { for _, route := range p.fixrouters { n := len(requestPath) if requestPath == route.pattern { runMethod = p.getRunMethod(r.Method, context, route) if runMethod != "" { runrouter = route.controllerType findrouter = true break } } //...... } } 

p.fixrouters就是不帶正則的路由,好比/userroute.controllerType的類型是reflect.Type,後面會用來建立控制器實例。p.getRunMethod()獲取實際請求方式。爲了知足瀏覽器沒法發送表單PUTDELETE方法,能夠用表單域_method值代替。(註明一下p就是*beego.ControllerRegistor

接下來固然是正則的路由:

if !findrouter { //find a matching Route for _, route := range p.routers { //check if Route pattern matches url if !route.regex.MatchString(requestPath) { continue } // ...... runMethod = p.getRunMethod(r.Method, context, route) if runMethod != "" { runrouter = route.controllerType context.Input.Params = params findrouter = true break } } } 

正則路由好比/user/:id:int,這種帶參數的。匹配後的參數會記錄在context.Input.Params中。

還沒找到,就看看是否須要自動路由:

if !findrouter && p.enableAuto { // ...... for cName, methodmap := range p.autoRouter { // ...... } } 

把全部路由規則走完,仍是沒有找到匹配的規則:

if !findrouter { middleware.Exception("404", rw, r, "") goto Admin } 

另外一種狀況就是找到路由規則咯,且看下文。

2.2 路由調用

上面的代碼發現路由的調用依賴runrouterrunmethod變量。他們值以爲了到底調用什麼控制器和方法。來看看具體實現:

if findrouter { //execute middleware filters if do_filter(BeforeExec) { goto Admin } //Invoke the request handler vc := reflect.New(runrouter) execController, ok := vc.Interface().(ControllerInterface) if !ok { panic("controller is not ControllerInterface") } //call the controller init function execController.Init(context, runrouter.Name(), runMethod, vc.Interface()) //if XSRF is Enable then check cookie where there has any cookie in the request's cookie _csrf if EnableXSRF { execController.XsrfToken() if r.Method == "POST" || r.Method == "DELETE" || r.Method == "PUT" || (r.Method == "POST" && (r.Form.Get("_method") == "delete" || r.Form.Get("_method") == "put")) { execController.CheckXsrfCookie() } } //call prepare function execController.Prepare() if !w.started { //exec main logic switch runMethod { case "Get": execController.Get() case "Post": execController.Post() case "Delete": execController.Delete() case "Put": execController.Put() case "Head": execController.Head() case "Patch": execController.Patch() case "Options": execController.Options() default: in := make([]reflect.Value, 0) method := vc.MethodByName(runMethod) method.Call(in) } //render template if !w.started && !context.Input.IsWebsocket() { if AutoRender { if err := execController.Render(); err != nil { panic(err) } } } } // finish all runrouter. release resource execController.Finish() //execute middleware filters if do_filter(AfterExec) { goto Admin } } 

研讀一下,最開始的又是過濾器:

if do_filter(BeforeExec) { goto Admin } 

BeforeExec執行控制器方法前的過濾。

而後,建立一個新的控制器實例:

vc := reflect.New(runrouter)
execController, ok := vc.Interface().(ControllerInterface)
if !ok { panic("controller is not ControllerInterface") } //call the controller init function execController.Init(context, runrouter.Name(), runMethod, vc.Interface()) 

reflect.New()建立新的實例,用vc.Interface().(ControllerInterface)取出,調用接口的Init方法,將請求的上下文等傳遞進去。 這裏就說明爲何不能存下控制器實例給每次請求使用,由於每次請求的上下文是不一樣的

execController.Prepare()

控制器的準備工做,這裏能夠寫用戶登陸驗證等。

而後根據runmethod執行控制器對應的方法,非接口定義的方法,用reflect.Call調用。

if !w.started && !context.Input.IsWebsocket() { if AutoRender { if err := execController.Render(); err != nil { panic(err) } } } 

若是自動渲染AutoRender,就調用Render()方法渲染頁面。

execController.Finish()

//execute middleware filters if do_filter(AfterExec) { goto Admin } 

控制器最後一刀Finish搞定,而後過濾器AfterExec使用。

總結起來,beego.ControllerInterface接口方法的Init,Prepare,RenderFinish發揮很大做用。那就來研究一下。

3. 控制器和視圖

3.1 控制器接口

控制器接口beego.ControllerInterface的定義在controller.go#L47

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 } 

官方的實現beego.Controller定義在controller.go#L29

type Controller struct { Ctx *context.Context Data map[interface{}]interface{} controllerName string actionName string TplNames string Layout string LayoutSections map[string]string // the key is the section name and the value is the template name TplExt string _xsrf_token string gotofunc string CruSession session.SessionStore XSRFExpire int AppController interface{} EnableReander bool } 

內容好多,不必所有都看看,重點在Init,Prepare,RenderFinish這四個。

3.2 控制器的實現

Init方法:

// Init generates default values of controller operations. func (c *Controller) Init(ctx *context.Context, controllerName, actionName string, app interface{}) { c.Layout = "" c.TplNames = "" c.controllerName = controllerName c.actionName = actionName c.Ctx = ctx c.TplExt = "tpl" c.AppController = app c.EnableReander = true c.Data = ctx.Input.Data } 

沒什麼話說,一堆賦值。惟一要談的是c.EnableReander,這種拼寫錯誤實在是,掉陰溝裏。實際的意思是EnableRender

PrepareFinish方法:

// Prepare runs after Init before request function execution. func (c *Controller) Prepare() { } // Finish runs after request function execution. func (c *Controller) Finish() { } 

空的!原來我要本身填內容啊。

Render方法:

// Render sends the response with rendered template bytes as text/html type. func (c *Controller) Render() error { if !c.EnableReander { return nil } rb, err := c.RenderBytes() if err != nil { return err } else { c.Ctx.Output.Header("Content-Type", "text/html; charset=utf-8") c.Ctx.Output.Body(rb) } return nil } 

3.3 視圖渲染

渲染的核心方法是c.RenderBytes():

// RenderBytes returns the bytes of rendered template string. Do not send out response. func (c *Controller) RenderBytes() ([]byte, error) { //if the controller has set layout, then first get the tplname's content set the content to the layout if c.Layout != "" { if c.TplNames == "" { c.TplNames = strings.ToLower(c.controllerName) + "/" + strings.ToLower(c.actionName) + "." + c.TplExt } if RunMode == "dev" { BuildTemplate(ViewsPath) } newbytes := bytes.NewBufferString("") if _, ok := BeeTemplates[c.TplNames]; !ok { panic("can't find templatefile in the path:" + c.TplNames) return []byte{}, errors.New("can't find templatefile in the path:" + c.TplNames) } err := BeeTemplates[c.TplNames].ExecuteTemplate(newbytes, c.TplNames, c.Data) if err != nil { Trace("template Execute err:", err) return nil, err } tplcontent, _ := ioutil.ReadAll(newbytes) c.Data["LayoutContent"] = template.HTML(string(tplcontent)) if c.LayoutSections != nil { for sectionName, sectionTpl := range c.LayoutSections { if sectionTpl == "" { c.Data[sectionName] = "" continue } sectionBytes := bytes.NewBufferString("") err = BeeTemplates[sectionTpl].ExecuteTemplate(sectionBytes, sectionTpl, c.Data) if err != nil { Trace("template Execute err:", err) return nil, err } sectionContent, _ := ioutil.ReadAll(sectionBytes) c.Data[sectionName] = template.HTML(string(sectionContent)) } } ibytes := bytes.NewBufferString("") err = BeeTemplates[c.Layout].ExecuteTemplate(ibytes, c.Layout, c.Data) if err != nil { Trace("template Execute err:", err) return nil, err } icontent, _ := ioutil.ReadAll(ibytes) return icontent, nil } else { //...... } return []byte{}, nil } 

看起來很複雜,主要是兩種狀況,有沒有Layout。若是有Layout:

err := BeeTemplates[c.TplNames].ExecuteTemplate(newbytes, c.TplNames, c.Data)
// ...... tplcontent, _ := ioutil.ReadAll(newbytes) c.Data["LayoutContent"] = template.HTML(string(tplcontent)) 

渲染模板文件,就是佈局的主內容。

for sectionName, sectionTpl := range c.LayoutSections { if sectionTpl == "" { c.Data[sectionName] = "" continue } sectionBytes := bytes.NewBufferString("") err = BeeTemplates[sectionTpl].ExecuteTemplate(sectionBytes, sectionTpl, c.Data) // ...... sectionContent, _ := ioutil.ReadAll(sectionBytes) c.Data[sectionName] = template.HTML(string(sectionContent)) } 

渲染布局裏的別的區塊c.LayoutSections

ibytes := bytes.NewBufferString("") err = BeeTemplates[c.Layout].ExecuteTemplate(ibytes, c.Layout, c.Data) // ...... icontent, _ := ioutil.ReadAll(ibytes) return icontent, nil 

最後是渲染布局文件,c.Data裏帶有全部佈局的主內容和區塊,能夠直接賦值在佈局裏。

渲染過程有趣的代碼:

if RunMode == "dev" { BuildTemplate(ViewsPath) } 

開發狀態下,每次渲染都會從新BuildTemplate()。這樣就能夠理解,最初渲染模板並存下*template.Template,生產模式下,是不會響應即時的模版修改。

總結

本文對beego的執行過程進行了分析。一個Web應用,運行的過程就是路由分發,路由執行和結果渲染三個主要過程。本文沒有很是詳細的解釋beego源碼的細節分析,可是仍是有幾個重要問題進行的說明:

  • 路由規則的分類,固定的,仍是正則,仍是自動的。不一樣的路由處理方式不一樣,須要良好設計
  • 控制器的操做其實就是上下文的處理,使用控制器類,仍是函數,須要根據應用考量。
  • 視圖的效率控制須要嚴格把關,並且如何簡單的設計就能知足複雜模板的使用,須要仔細考量。

beego自己複雜,他的不少實現其實並非很簡潔直觀。固然隨着功能愈來愈強大,beego會愈來愈好的。

相關文章
相關標籤/搜索