目录
文章目录
目录
一个 RESTful API 框架需要什么?
go-restful
核心概念
Route
WebService
Container
过滤器(Filter)
响应编码(Response Encoding)
代码示例一
代码示例二
一个 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 请求的处理即响应。
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 文档。
- 支持可配置的日志跟踪。
核心概念
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)等。
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。
Container
Container 表示一个 Web Server(服务器),由多个 WebServices 组成,此外还包含了若干个 Filters(过滤器)、一个 http.ServeMux 多路复用器以及一个 dispatch。go-restful 如何在从 Container 开始将路由分发给各个 WebService,再由 WebService 分发给具体的 Handler 函数,这些都在 dispatch 中实现。
开发者根据需要创建 Container 实例之后,将 Container 加载到一个 http.Server 上运行。示例:
// 构建一个 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}
过滤器(Filter)
go-restful 支持服务级、路由级的请求或响应过滤。开发者可以使用 Filter 来执行常规的日志记录、计量、验证、重定向、设置响应头部等工作。go-restful 提供了 3 个针对请求、响应的钩子(Hooks),此外,还可以实现自定义的 Filter。
每个 Filter 必须实现一个 FilterFunction:
func(req *restful.Request, resp *restful.Response, chain *restful.FilterChain)
并使用如下语句传递请求、响应对到下一个 Filter 或 RouteFunction:
chain.ProcessFilter(req, resp)
Container Filter:在注册 WebService 之前处理。
// 安装一个全局的 Filter 到 Default Container restful.Filter(globalLogging)
WebService Filter:路由 WebService 之前处理。
// 安装一个 WebService Filter ws.Filter(webserviceLogging).Filter(measureTime)
Route Filter:在调用 Router 相关的函数之前处理。
// 安装 2 个链式的 Route Filter ws.Route(ws.GET("/{user-id}").Filter(routeLogging).Filter(NewCountFilter().routeCounter))
OPTIONS Filter:使 WebService 可以响应 HTTP OPTIONS 请求。
Filter(OPTIONSFilter())
CORS Filter:是 WebService 可以响应 CORS 请求。
cors := CrossOriginResourceSharing{ ExposeHeaders: []string{"X-My-Header"}, CookiesAllowed: false, Container: DefaultContainer }
Filter(cors.Filter)
响应编码(Response Encoding)
如果 HTTP Request 包含了 Accept-Encoding Header,那么 HTTP Response 就必须使用指定的编码格式进行压缩。go-restful 目前支持 zip 、deflate 这两种响应编码格式。
如果要为所有的响应启用它们:
restful.DefaultContainer.EnableContentEncoding(true)
同时,也可以通过创建一个 Filter 来实现自定义的响应编码过滤器,并将其安装到每一个 WebService 和 Route 上。
代码示例一
下述示例实现了对 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。
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 //
// // GET http://localhost:8080/users/1 // // PUT http://localhost:8080/users/1 //1 Melissa Raspberry // // DELETE http://localhost:8080/users/1 //1 Melissa 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 //
// 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()) } }1 Melissa Raspberry // PUT http://localhost:8080/users/1 //
// 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()) } }1 Melissa // 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()) }
执行:
$ ./restful
客户端调用:
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
代码示例二
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/doublegao/experiment/restful/dist"}
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())
}