1. 一个 RESTful API 框架需要什么?
从应用程序开发的角度来看,RESTful API 的本质是一个 Web Application,而 RESTful API 框架就是实现这个 Web Application 所封装的一些列工具库,使开发者可以忽略底层实现的复杂度,专注以自身 Application 的逻辑设计。
一个 RESTful API 框架应该具备以下几个元素:
- Resources:资源的定义,即 HTTP URI(或称之为 HTTP URL Path)的定义。RESTful API 的设计围绕着 - - - Resource 进行建模。
- Handlers:资源处理器,是资源业务逻辑处理的具体实现。
- Request Routers:资源请求路由器,完成 HTTP URIs、HTTP Request Methods 和 Handlers 三者之间的映射与路由。
- Request Verification Schemas:HTTP Request Body 校验器,验证请求实体的合法性。
- Response View Builder:HTTP Response Body 生成器,生成合法的响应实体。
- Controllers:资源表现层状态转移控制器,每个 Resource 都有着各自的 Controller,将 Resource 自身及其所拥有的 Handlers、Request Verification Schemas 以及 Response View Builder 进行封装,配合 Request Routers 完成 RESTful 请求的处理即响应。
2. go-restful
go-restful 是一个 Golang 第三方库,是一个轻量的 RESTful API 框架,基于 Golang Build-in 的 http/net 库。适用于构建灵活多变的 Web Application,Kubernetes 的 ApiServer 也使用了 go-restful。
go-restful 具有以下特性:
- 支持可配置的请求路由,默认使用 CurlyRouter 快速路由算法,也支持 RouterJSR311。
- 支持在 URL path 上定义正则表达式,例如:/static/{subpath:*}。
- 提供 Request API 用于从 JSON、XML 读取路径参数、查询参数、头部参数,并转换为 Struct。
- 提供 Response API 用于将 Struct 写入到 JSON、XML 以及 Header。
- 支持在服务级、或路由级对请求、响应流进行过滤和拦截。
- 支持使用过滤器自动响应 OPTIONS 请求和 CORS(跨域)请求。
- 支持使用 RecoverHandler 自定义处理 HTTP 500 错误。
- 支持使用 ServiceErrorHandler 自定义处理路由错误产生 HTTP 404/405/406/415 等错误。
- 支持对请求、响应的有效负载进行编码(例如:gzip、deflate)。
- 支持使用 CompressorProvider 注册自定义的 gzip、deflate 的读入器和输出器。
- 支持使用 EntityReaderWriter 注册的自定义编码实现。
- 支持 Swagger UI 编写的 API 文档。
- 支持可配置的日志跟踪。
3. 核心概念
在这里插入图片描述
3.1. Route
Route 表示一条请求路由记录,即:Resource 的 URL Path(URI),从编程的角度可细分为 RootPath 和 SubPath。Route 包含了 Resource 的 URL Path、HTTP Method、Handler 三者之间的组合映射关系。go-restful 内置的 RouteSelector(请求路由分发器)根据 Route 将客户端发出的 HTTP 请求路由到相应的 Handler 进行处理。
go-restful 支持两种路由分发器:快速路由 CurlyRouter 和 RouterJSR311。实际上,CurlyRoute 也是基于 RouterJSR311 的,相比 RouterJSR11,还支持了正则表达式和动态参数,也更加轻量级,Kubernetes ApiServer 中使用的就是这种路由。
CurlyRouter 的元素包括:请求路径(URL Path),请求参数(Parameter),输入、输出类型(Writes、Reads Model),处理函数(Handler),响应内容类型(Accept)等。
3.2. WebService
一个 WebService 由若干个 Routes 组成,并且 WebService 内的 Routes 拥有同一个 RootPath、输入输出格式、基本一致的请求数据类型等等一系列的通用属性。通常的,我们会根据需要将一组相关性非常强的 API 封装成为一个 WebServiice,继而将 Web Application 所拥有的全部 APIs 划分若干个 Group。
所以,WebService 至少会有一个 Root Path,通过 ws.Path() 方法设置,例如:/user_group,作为 Group 的 “根”。Group 下属的 APIs 都是 RootRoute(RootPath)下属的 SubRoute(SubPath)。
每个 Group 就是提供一项服务的 API 集合,每个 Group 会维护一个 Version。Group 的抽象是为了能够安全隔离的对各项服务进行敏捷迭代,当我们对一项服务进行升级时,只需要通过对特定版本号的更新来升级相关的 APIs,而不会影响到整个 Web Server。视实际情况而定,可能是若干个 APIs 分为一个 Group,也有可能一个 API 就是一个 Group。
3.3. Container
Container 表示一个 Web Server(服务器),由多个 WebServices 组成,此外还包含了若干个 Filters(过滤器)、一个 http.ServeMux 多路复用器以及一个 dispatch。go-restful 如何在从 Container 开始将路由分发给各个 WebService,再由 WebService 分发给具体的 Handler 函数,这些都在 dispatch 中实现。
开发者根据需要创建 Container 实例之后,将 Container 加载到一个 http.Server 上运行。示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// 构建一个 WebService 实例。 ws := new(restful.WebService) // 定义 Root Path。 ws.Path("/users") // 定义一个 WebService 下属的 Route。 ws.Route(ws.GET("/users").To(u.findAllUsers). Doc("get all users"). Metadata(restfulspec.KeyOpenAPITags, tags). Writes([]User{}). Returns(200, "OK", []User{})) // 构建一个 Container 实例。 container := restful.NewContainer() // 将 Container 加载到 http.Server 运行。 server := &http.Server{Addr: ":8081", Handler: container} |
3.4. 过滤器(Filter)
go-restful 支持服务级、路由级的请求或响应过滤。开发者可以使用 Filter 来执行常规的日志记录、计量、验证、重定向、设置响应头部等工作。go-restful 提供了 3 个针对请求、响应的钩子(Hooks),此外,还可以实现自定义的 Filter。
每个 Filter 必须实现一个 FilterFunction:
1 |
func(req *restful.Request, resp *restful.Response, chain *restful.FilterChain) |
并使用如下语句传递请求、响应对到下一个 Filter 或 RouteFunction:
1 |
chain.ProcessFilter(req, resp) |
Container Filter:在注册 WebService 之前处理。
1 2 |
// 安装一个全局的 Filter 到 Default Container restful.Filter(globalLogging) |
WebService Filter:路由 WebService 之前处理。
1 2 |
// 安装一个 WebService Filter ws.Filter(webserviceLogging).Filter(measureTime) |
Route Filter:在调用 Router 相关的函数之前处理。
1 2 |
// 安装 2 个链式的 Route Filter ws.Route(ws.GET("/{user-id}").Filter(routeLogging).Filter(NewCountFilter().routeCounter)) |
OPTIONS Filter:使 WebService 可以响应 HTTP OPTIONS 请求。
1 |
Filter(OPTIONSFilter()) |
CORS Filter:是 WebService 可以响应 CORS 请求。
1 2 3 4 5 6 |
cors := CrossOriginResourceSharing{ ExposeHeaders: []string{"X-My-Header"}, CookiesAllowed: false, Container: DefaultContainer } Filter(cors.Filter) |
3.5. 响应编码(Response Encoding)
如果 HTTP Request 包含了 Accept-Encoding Header,那么 HTTP Response 就必须使用指定的编码格式进行压缩。go-restful 目前支持 zip 、deflate 这两种响应编码格式。
如果要为所有的响应启用它们:
1 |
restful.DefaultContainer.EnableContentEncoding(true) |
同时,也可以通过创建一个 Filter 来实现自定义的响应编码过滤器,并将其安装到每一个 WebService 和 Route 上。
4. 代码示例一
下述示例实现了对 users 资源的 CURD API。实现的过程如下:
- 定义 User resource。
- 定义 User 的 Handlers。
- 定义一个 User resource Register(资源注册器)
- 在 User resource Register 内构造了 WebService 实例、定义了 User 的 URL RootPath /users、以及多个 SubPath 的 Routes。并且 HTTP Method、User Path(RootPath + SubPath)、Handlers 在 Routes 内建立映射关系。最后将 Routes 关联到 WebService、将 WebServices 关联到 Container。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
package main import ( "log" "net/http" "github.com/emicklei/go-restful" ) // This example has the same service definition as restful-user-resource // but uses a different router (CurlyRouter) that does not use regular expressions // // POST http://localhost:8080/users // <User><Id>1</Id><Name>Melissa Raspberry</Name></User> // // GET http://localhost:8080/users/1 // // PUT http://localhost:8080/users/1 // <User><Id>1</Id><Name>Melissa</Name></User> // // DELETE http://localhost:8080/users/1 // type User struct { Id, Name string } type UserResource struct { // normally one would use DAO (data access object) users map[string]User } func (u UserResource) Register(container *restful.Container) { ws := new(restful.WebService) ws. Path("/users"). Consumes(restful.MIME_XML, restful.MIME_JSON). Produces(restful.MIME_JSON, restful.MIME_XML) // you can specify this per route as well ws.Route(ws.GET("/{user-id}").To(u.findUser)) ws.Route(ws.POST("").To(u.updateUser)) ws.Route(ws.PUT("/{user-id}").To(u.createUser)) ws.Route(ws.DELETE("/{user-id}").To(u.removeUser)) container.Add(ws) } // GET http://localhost:8080/users/1 // func (u UserResource) findUser(request *restful.Request, response *restful.Response) { id := request.PathParameter("user-id") usr, ok := u.users[id] if !ok { response.AddHeader("Content-Type", "text/plain") response.WriteErrorString(http.StatusNotFound, "User could not be found.") } else { response.WriteEntity(usr) } } // POST http://localhost:8080/users // <User><Id>1</Id><Name>Melissa Raspberry</Name></User> // func (u *UserResource) updateUser(request *restful.Request, response *restful.Response) { usr := new(User) err := request.ReadEntity(&usr) if err == nil { u.users[usr.Id] = *usr response.WriteEntity(usr) } else { response.AddHeader("Content-Type", "text/plain") response.WriteErrorString(http.StatusInternalServerError, err.Error()) } } // PUT http://localhost:8080/users/1 // <User><Id>1</Id><Name>Melissa</Name></User> // func (u *UserResource) createUser(request *restful.Request, response *restful.Response) { usr := User{Id: request.PathParameter("user-id")} err := request.ReadEntity(&usr) if err == nil { u.users[usr.Id] = usr response.WriteHeaderAndEntity(http.StatusCreated, usr) } else { response.AddHeader("Content-Type", "text/plain") response.WriteErrorString(http.StatusInternalServerError, err.Error()) } } // DELETE http://localhost:8080/users/1 // func (u *UserResource) removeUser(request *restful.Request, response *restful.Response) { id := request.PathParameter("user-id") delete(u.users, id) } func main() { wsContainer := restful.NewContainer() wsContainer.Router(restful.CurlyRouter{}) u := UserResource{map[string]User{}} u.Register(wsContainer) log.Printf("start listening on localhost:8080") server := &http.Server{Addr: ":8080", Handler: wsContainer} log.Fatal(server.ListenAndServe()) } |
执行:
1 |
$ ./restful |
客户端调用:
1 2 3 4 5 6 |
curl -X POST -v -i http://127.0.0.1:8080/users \ -H 'Content-type: application/json' \ -H 'Accept: application/xml' \ -d '{"Id": "1", "Name": "fanguiju"}' curl -X GET -v -i http://127.0.0.1:8080/users/1 |
5. 代码示例二
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
package main import ( "io" "log" "net/http" "github.com/emicklei/go-restful" swagger "github.com/emicklei/go-restful-swagger12" ) type UserResource struct{} func (u UserResource) result(request *restful.Request, response *restful.Response) { io.WriteString(response.ResponseWriter, "this would be a normal response") } func (UserResource) SwaggerDoc() map[string]string { return map[string]string{ "": "Address doc", "country": "Country doc", "postcode": "PostCode doc", } } func (u UserResource) RegisterTo(container *restful.Container) { ws := new(restful.WebService) ws.Path("/user").Consumes("*/*").Produces("*/*") ws.Route(ws.GET("/{id}"). To(u.result). Doc("方法描述:获取用户"). Param(ws.PathParameter("id", "参数描述:用户ID").DataType("string")). Param(ws.QueryParameter("name", "用户名称").DataType("string")). Param(ws.HeaderParameter("token", "访问令牌").DataType("string")). Do(returns200, returns500)) ws.Route(ws.POST("").To(u.result)) ws.Route(ws.PUT("/{id}").To(u.result)) ws.Route(ws.DELETE("/{id}").To(u.result)) container.Add(ws) } func returns200(b *restful.RouteBuilder) { b.Returns(http.StatusOK, "OK", "success") } func returns500(b *restful.RouteBuilder) { b.Returns(http.StatusInternalServerError, "Bummer, something went wrong", nil) } func main() { wsContainer := restful.NewContainer() // 跨域过滤器 cors := restful.CrossOriginResourceSharing{ ExposeHeaders: []string{"X-My-Header"}, AllowedHeaders: []string{"Content-Type", "Accept"}, AllowedMethods: []string{"GET", "POST"}, CookiesAllowed: false, Container: wsContainer} wsContainer.Filter(cors.Filter) // Add container filter to respond to OPTIONS wsContainer.Filter(wsContainer.OPTIONSFilter) config := swagger.Config{ WebServices: restful.DefaultContainer.RegisteredWebServices(), // you control what services are visible WebServicesUrl: "http://localhost:8080", ApiPath: "/apidocs.json", ApiVersion: "V1.0", // Optionally, specify where the UI is located SwaggerPath: "/apidocs/", SwaggerFilePath: "D:/gowork/src/restful/"} swagger.RegisterSwaggerService(config, wsContainer) swagger.InstallSwaggerService(config) u := UserResource{} u.RegisterTo(wsContainer) log.Print("start listening on localhost:8080") server := &http.Server{Addr: ":8080", Handler: wsContainer} defer server.Close() log.Fatal(server.ListenAndServe()) } |
微信赞赏
支付宝赞赏