文档章节

Martini源码剖析

傅小黑
 傅小黑
发布于 2014/04/24 01:25
字数 1641
阅读 1497
收藏 30

martini是非常优雅的Go Web框架。他基于依赖注入的思想,仿照Sinatra的路由设计,参考Express的中间件设计,而且核心微小,扩展方便,非常值得学习。但是由于本身API设计简洁,使很多细节无法从代码理解。所以,我写一点笔记记录martini的工作方式。

<!--more-->

Martini核心

我们从最简单的官方实例入手:

package main

import "github.com/go-martini/martini"

func main() {
  m := martini.Classic()
  m.Get("/", func() string {
    return "Hello world!"
  })
  m.Run()
}

martini.Martini是自带的核心结构,负责完成依赖注入和调用的过程。martini.ClassicMartini是路由martini.Routermartini.Martini的组合,实现路由分发和逻辑调用的过程。m := martini.Classic()返回的就是martini.ClassicMartini。具体在martini.go#L104:

func Classic() *ClassicMartini {
	r := NewRouter()
	m := New()
	m.Use(Logger())
	m.Use(Recovery())
	m.Use(Static("public"))
	m.MapTo(r, (*Routes)(nil))
	m.Action(r.Handle)
	return &ClassicMartini{m, r}
}

里面的m := New()定义在martini.go#L38:

func New() *Martini {
	m := &Martini{Injector: inject.New(), action: func() {}, logger: log.New(os.Stdout, "[martini] ", 0)}
	m.Map(m.logger)
	m.Map(defaultReturnHandler())
	return m
}

依赖注入

上面很明显的看到两个奇特方法:m.Map()m.MapTo()。这里,需要注意martini的一个最重要原则,注入的任何类型的结构,都是唯一的。即如:

type User struct{
    Id int
}

m.Map(&User{Id:1})
m.Map(&User{Id:2})

martini在寻找*User类型的时候,只能获取到&User{Id:2}结构(最后注册的)。Map的作用,就是向内部注册对应类型的具体对象或者值。类型索引是reflect.Type。从而我们可以理解到m.New()的代码中将m.Logger(*log.Logger)defaultReturnHandler(martini.ReturnHandler)(括号中是类型索引)注入到内部。

这里出现了一个问题。接口这种类型,是无法用reflect.Type直接获取的(因为传来的都是已经实现接口的具体结构)。解决方法就是m.MapTo

m.MapTo(r, (*Routes)(nil))即将r(martini.router)按照martini.Router接口(注意大小写)类型注入到内部。(*Routes)(nil)也是高明的构造。接口的默认值不是nil,无法直接new。但是指针的默认值是nil,可以直接赋值,比如var user *User; user = nil。因此他注册一个接口指针类型的空指针,用reflect.Type.Elem()方法就可以获取到指针的内部类型,即接口类型,并以接口类型索引注入到内部。

路由过程

HTTP处理

martini.Martini实现了http.Handler方法,实际的HTTP执行过程在代码martini.go#L68:

func (m *Martini) ServeHTTP(res http.ResponseWriter, req *http.Request) {
	m.createContext(res, req).run()
}

这里需要我们关注m.createContext,它返回*martini.context类型,代码martini.go#L87

func (m *Martini) createContext(res http.ResponseWriter, req *http.Request) *context {
	c := &context{inject.New(), m.handlers, m.action, NewResponseWriter(res), 0}
	c.SetParent(m)
	c.MapTo(c, (*Context)(nil))
	c.MapTo(c.rw, (*http.ResponseWriter)(nil))
	c.Map(req)
	return c
}

创建*martini.context类型;然后SetParent设置寻找注入对象的时候同时从m(*martini.Martini)中寻找(*martini.context*martini.Martini两个独立的inject),这样就可以获取m.Map注入的数据。

这里叉出来说:从代码看出实际上注入的数据有两层,分别在*martini.context*martini.Martini*martini.context中的是当前请求可以获取的(每个请求都会m.createContext(),都是新的对象);martini.Martini是全局的,任何请求都可以获取到。

回到上一段,c.MapTo*martini.contextmartini.Context接口,将martini.ResponseWriterhttp.ResponseWriter接口,把req(*http.Request)注入到当前上下文。

context.run方法定义在martini.go#L163:

func (c *context) run() {
	for c.index <= len(c.handlers) {
		_, err := c.Invoke(c.handler())
		if err != nil {
			panic(err)
		}
		c.index += 1

		if c.Written() {
			return
		}
	}
}

它在循环c.handlers(来自m.handlers,createContext代码中)。这里想解释三个细节。

c.Invokeinject.Invoke方法,内部就是获取c.hanlder()返回的martini.Handler(func)类型的传入参数reflect.Type.In(),根据参数个数和类型去内部找对应的结构,然后拼装成[]reflect.Value给函数的reflect.Value(func).Call()

c.handler()的返回来自两个方面,c.hanldersc.actionc.handlers来自m.Use()添加,c.action来自r.Handle(*martini.router.Handle)(见上文martini.ClassicMartini.New中的m.Action(r.Handle))。因此,可以发现实际上handlers是有两个列表,一个是c.handlers([]martini.handler)r.handlers(martini.routerContext.handlers)。而且前者先执行。也就是说无论m.Use写在哪儿,都要比router添加的func先执行。

c.Written判断请求是否已经发送。他实际上是判断martini.ResponseWriter.status是否大于0。因此只要发送了response status,handlers过程就会停止。

路由调用

从上面可以知道,路由调用过程有两个方面:一是m.Use()添加的handlers,二是路由添加比如m.Get("/",handlers...)中的handlers。m.Use的handlers调用就是上文的*martini.context.run方法,不再赘述。路由中的handlers执行是在router.go#L218:

func (r *route) Handle(c Context, res http.ResponseWriter) {
	context := &routeContext{c, 0, r.handlers}
	c.MapTo(context, (*Context)(nil))
	context.run()
}

router.go#L315:

func (r *routeContext) run() {
	for r.index < len(r.handlers) {
		handler := r.handlers[r.index]
		vals, err := r.Invoke(handler)
		if err != nil {
			panic(err)
		}
		r.index += 1

		// if the handler returned something, write it to the http response
		if len(vals) > 0 {
			ev := r.Get(reflect.TypeOf(ReturnHandler(nil)))
			handleReturn := ev.Interface().(ReturnHandler)
			handleReturn(r, vals)
		}

		if r.Written() {
			return
		}
	}
}

如果你已经理解上文中说明,这个过程和martini.context.run是一样的。唯一这里要解释的是martini.ReturnHandler。它与很上文中的m.Map(defaultReturnHandler())遥相呼应。

中间件

从上文不难理解,中间件其实就是martini.Handlerm.Use添加到m.handlers中。这里我们来说明官方的一个中间件martini.Logger(),实现代码在logger.go:

func Logger() Handler {
	return func(res http.ResponseWriter, req *http.Request, c Context, log *log.Logger) {
		start := time.Now()
		log.Printf("Started %s %s", req.Method, req.URL.Path)

		rw := res.(ResponseWriter)
		c.Next()

		log.Printf("Completed %v %s in %v\n", rw.Status(), http.StatusText(rw.Status()), time.Since(start))
	}
}

首先看func的传入参数,http.ResponseWriter*http.Request来自:

c := &context{inject.New(), m.handlers, m.action, NewResponseWriter(res), 0}
// ...
c.MapTo(c.rw, (*http.ResponseWriter)(nil))
c.Map(req)

Context来自:

context := &routeContext{c, 0, r.handlers}
c.MapTo(context, (*Context)(nil))

*log.Logger来自:

m := &Martini{Injector: inject.New(), action: func() {}, logger: log.New(os.Stdout, "[martini] ", 0)}
m.Map(m.logger)

然后看rw := res.(ResponseWriter)。实际上c.rwNewReponseWriter(res)返回的martini.ResponseWriter类型,一次可以在这里直接转换(注意在外部调用,不是martini包中,要import并写res.(martini.ResponseWriter))。

最后是c.Next()方法,源码在martini.go#L154:

func (c *context) Next() {
	c.index += 1
	c.run()
}

意思就是index自增,指向下一个handler,c.run走完所有handler,然后继续中间件里的log.Printf...

总结

martini的对外API很简单,但是内部实现其实比较复杂的。需要仔细的阅读,并且有一定标准库的基础,才能很好的理解他代码的用意。

我这里只是按照自己的理解说明,如果有错误请在评论中指正。


小站地址 http://fuxiaohei.me/article/25/martini-source-study.html

© 著作权归作者所有

上一篇: Beego源码分析
下一篇: Go语言的Web框架
傅小黑
粉丝 155
博文 7
码字总数 13850
作品 1
厦门
高级程序员
私信 提问
加载中

评论(7)

ricktian1226
ricktian1226

引用来自“喻恒春”的评论

小黑文章写的很清晰, 排版也清爽. 好文.
里面有个不确切的用词 "注入的任何类型的结构,都是唯一的"
应该是 "注入的任何类型都是唯一的"
结构一样不一样无所谓, 用类型区分的.
如果遇到 handler 参数有同样的类型比如

Foo( int, int )

改写成别名形式

type myint int
Foo(int, myint)

然后 Map 的时候注意下类型转换 比如用:

Map(1)
Map(myint(2))

就可以了, 使用技巧而已.
喻恒春
喻恒春
小黑文章写的很清晰, 排版也清爽. 好文.
里面有个不确切的用词 "注入的任何类型的结构,都是唯一的"
应该是 "注入的任何类型都是唯一的"
结构一样不一样无所谓, 用类型区分的.
如果遇到 handler 参数有同样的类型比如

Foo( int, int )

改写成别名形式

type myint int
Foo(int, myint)

然后 Map 的时候注意下类型转换 比如用:

Map(1)
Map(myint(2))

就可以了, 使用技巧而已.
chapin
chapin
20144225443[79]
Deja-Vu
Deja-Vu
顶起来79
铂金小猪
铂金小猪
看不懂,纯支持。
铂金小狼
铂金小狼
顶小黑一个
老盖
老盖
很好的分享,支持一下
技术晨读_20160217

技术导读 Build a RESTful API with Martini 使用martini搭建一个Restful API,使用的是简易的内存database,搭建了一套支持json和xml的RESTFUL的API http://0value.com/build-a-restful-API...

王二狗子11
2018/01/07
0
0
Martini 极好的 Go WEB 框架

已知的其他框架看到的是传统OOP的影子, 到处充蚀 Class 风格的 OOP 方法. 而我们知道GoLang中是没有Class的. 笔者也曾努力用Go 的风格做WEB开发, 总感到力不从心. 写出的代码不能完全称之为框...

喻恒春
2014/01/07
18.8K
16
cordova-hcp server 报错

Running server Could not create tunnel: Error: panic: runtime error: invalid memory address or nil pointer dereference github.com/inconshreveable/olive/recover.go:40 runtime/asm......

mamie42
2016/10/17
646
0
Python/Ruby/Go/Node 之四国大战

Python Flask vs Ruby Sinatra vs Go Martini vs Node Express 本文授权转载自 zybuluo 博客。 题外话一:最近一段时间,Cloud Insight 接连发布了三种语言(Python, Node, Ruby)的SDK,Clo...

OneAPM蓝海讯通
2016/03/17
190
0
Martini 中的 Handler

前文参见 Martini 的工作方式 Handler 在Martini中是这样定义的 事实上 Handler 就是一个函数. Handler 贯穿于多个对象中. Martini Martini 对象设置Handler的方 Use 一次设置一个 handler ...

喻恒春
2014/02/07
2.1K
1

没有更多内容

加载失败,请刷新页面

加载更多

Handler简解

Handler 这里简化一下代码 以便理解 Handler不一定要在主线程建 但如Handler handler = new Handler(); 会使用当前的Looper的, 由于要更新UI 所以最好在主线程 new Handler() { mLooper = Lo...

shzwork
27分钟前
3
0
h5获取摄像头拍照功能

完整代码展示: <!DOCTYPE html> <head> <title>HTML5 GetUserMedia Demo</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum......

诗书易经
29分钟前
3
0
正向代理和反向代理

文章来源 运维公会:正向代理和反向代理 1、正向代理 (1)服务对象不同 正向代理服务器的服务对象是客户端,可以将客户端和代理服务器看作一个整体。 (2)配置方法不同 需要在客户端配置代...

运维团
46分钟前
4
0
5个避免意外论文重复率高的方法

即使你不是故意抄袭,但你可能在无意中抄袭了别人的论文, 这个叫做意外抄袭,它可能正发生在你身上,如果你不熟悉学术 道德规范,这里将告诉你5个基本的方法来避免意外抄袭。 Tip1 熟悉其他...

论文辅导员
47分钟前
4
0
Maven通过profiles标签读取不同的配置

<profiles> <profile> <id>dev</id> <properties> <profiles.active>dev</profiles.active> </properties> ......

时刻在奔跑
53分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部