摘要html
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
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
加載配置的代碼是: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,具體加載什麼就不說明了。須要說明的是AppPath
和workPath
這倆變量。找到定義config.go#72:app
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") } }
workPath
是os.Getwd()
,即當前的目錄;AppPath
是os.Args[0]
,即二進制文件所在目錄。有些狀況下這兩個是不一樣的。好比把命令加到PATH
中,而後cd到別的目錄執行。beego
以二進制文件所在目錄爲優先。若是二進制文件所在目錄沒有發現conf/app.conf
,再去workPath
裏找。框架
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
使用的是單實例的模式。
beego
的核心結構是beego.APP
,保存路由調度結構*beego.ControllerRegistor
。從beego.Run()
方法的代碼BeeApp.Run()
發現,beego
有一個全局變量BeeApp
是實際調用的*beego.APP
實例。也就是說整個beego
就是一個實例,不須要相似NewApp()
這樣的寫法。
所以,不少結構都做爲全局變量如beego.BeeApp
暴露在外。詳細的定義在 config.go#L18,特別注意一下SessionProvider(string)
,立刻就要提到。
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
接口的)。
繼續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
是必然的選擇。可是,編譯後模板的修改不能當即響應了,怎麼辦呢?先繼續看下去。
middleware
包目前彷佛只有錯誤處理的功能。
middleware.RegisterErrorHandler()
只是註冊默認的錯誤處理方法 middleware.NotFound
等幾個。
if EnableAdmin { go beeAdminApp.Run() }
beeAdminApp
也是一個*beego.adminApp
,負責系統監控、性能檢測、訪問統計和健康檢查等。具體的介紹和使用能夠訪問文檔。
寫了這麼多,終於要開始講核心結構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
————熱更新。對他的描述,能夠看看官方文檔。
上面的代碼看獲得*http.Server.Handler
是app.Handlers
,即*beego.ControllerRegistor
,ServeHTTP
就定義在代碼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 }
不過我以爲這倆順序怪怪的,應該先AfterStatic
後BeforeRouter
。須要注意,過濾器若是返回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
就是不帶正則的路由,好比/user
。route.controllerType
的類型是reflect.Type
,後面會用來建立控制器實例。p.getRunMethod()
獲取實際請求方式。爲了知足瀏覽器沒法發送表單PUT
和DELETE
方法,能夠用表單域_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 }
另外一種狀況就是找到路由規則咯,且看下文。
上面的代碼發現路由的調用依賴runrouter
和runmethod
變量。他們值以爲了到底調用什麼控制器和方法。來看看具體實現:
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
,Render
和Finish
發揮很大做用。那就來研究一下。
控制器接口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
,Render
和Finish
這四個。
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
。
Prepare
和Finish
方法:
// 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 }
渲染的核心方法是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
會愈來愈好的。