用 Go 微框架 aah 为 android app 写个 REST API 服务
更新
- 20220213 - 新增 Web 前端——Svelte web app。详见《用 Svelte 为 Go aah 后端写个 Web App》
- 20220219 - 增加 SQLBoiler 驱动、补充存储库及基本使用示例。详见《Go aah 框架集成数据库 ORM》。这部分的代码目前在 database 分支。
- 20220222 - 增加 《Go aah 框架的安全设计理解及应用实例》。
前言
学习和工作中有时会碰到这样的情况:一个有着漂亮 “颜值” 和功能的前端开源项目,因为缺失了后端服务而不能呈现效果,彼时那种 “猴急” 的心情难以言表。那么,是狠下心来果断地放弃,还是厚着脸皮跟开发者讨个后台测试 URL?还是兀自搭个后台继续地折腾?不管结果如何,这总归是一桩憾事。
“何以解忧,唯有...Mock”!
吃够了苦头的开发者,拿起来手中的武器——键盘和鼠标,于是,无数的 “Mock” 应用诞生了!不是啦,当然这是在讲笑话了,Mock 因测试而生。缺失测试后端的问题,搭建一个支持 REST API 的服务就可以了,就能解决你的遗憾。
最简单的解决方案应该是用 Node.js 来实现一个 Mock Server 了!近年来,JS 及 Node.js 的生态之繁荣实在令人惊叹!但在这里,我想介绍另一种实现思路:用 Go aah 框架来搭建 REST API 服务。这个方案基于 aah 框架,仅需编辑 3~4 个文件,即可实现一个完整的 CRUD 应用服务。
需求分析
以一个 GitHub 上存在的 Android app(https://github.com/dvinfosys/Android-RecyclerView-...)为例,它使用 Apache HttpClient 从远程 API 端点获取一个分类列表,然后用 Android RecyclerView 对该分类列表按照升序或降序排序的方式进行展示。开发者没有提供后端服务的测试接口。需要为该 app 搭建一个 REST API 后端服务,目标是实现该 app 正常操作演示 。
1、app 现有功能需求
主要是实现分类列表显示。需要一个 REST API 服务器,至少支持 HTTP GET。为增加趣味性,可设置属性 Count 为访问计数器,每次通过 API 访问条目(Item)时 Count 加 1)。
通过代码,可知 app 现有功能实现细节如下:
1)网络请求
请求 URL:"http://192.168.1.12/StoreAPI/api/Industry/GetIndustry"
请求方式:GET。
2)分类属性
基本属性包括 IndustryID、IndustryName、Nature、Count。
2、app 扩展功能需求
现 app 实现了一个列表,只用到了 GET,。要实现一个较为完整的演示,至少还需要增加 "CUD" 功能:增加1个 industry 分类(CREATE)、更新 industry 分类(UPDATE)、删除分类(DELETE)。
这里就用 aah 框架来实现一下第 1 点中的功能。第 2 点的后端功能在代码中已经实现,可以通过 Postman 进行验证。代码仓库(repo)是:https://github.com/vulcangz/aah-recycleview-backen...。Postman 测试 Collection 在 repo 的 docs\postman 目录下。
前端 app 不是本文关注的重点,"CUD" 功能,若以后有时间再补充完善吧。
先决条件
- go >= 1.11:这是 aah v0.12.0 版本的要求。
- Visual Studio Code(可选):Go IDE。
- Android Studio Arctic Fox | 2020.3.1 Patch 4:这是作者所使用的版本,用于安卓代码编辑,编译、测试 APK。您也可选择自己熟悉的 IDE。
- 安卓模拟器(可选):在 AS 内置模拟器和夜神上测试通过。
aah 框架简介
aah(https://github.com/go-aah/aah)是一个具有微框架性质的、特性丰富的、OOTB(开箱即用)的框架。可为构建现代 Web、API 和 WebSocket 应用程序提供必要的组件。如果想了解详细特性,可访问官网;或者可访问 https://www.worldlink.com.cn/osdir/aah.html 了解下。
(aah框架近2个月星走势图)
从近2个月的星走势图来看,aah 一直处于不温不火的、比较平稳的状态。与其他流行的 Go 框架如 Gin、Iris、chi、Echo、Fiber 等相比,aah 低调得就好像不存在一样。与定位类似的 Buffalo 相比,其 CLI 功能没有那么多,Buffalo 的 Soda 可以直接在命令行创建 model($ soda g model)、写 Migrations($ soda generate fizz)等,而 aah 没有提供这些。但个人认为,无论从 OOTB 功能、安全性设计及实现,还是从事件处理机制、i18n 国际化支持等方面来看,aah 都做得相当出色,当然,这是个人观点。aah 为开发者提供了很多 hack 的空间,要玩转 aah 这个框架需要开发者具备一定的技术能力,也许这是影响 aah 流行的一个主要因素。
aah 支持生成、构建 Web、API 和 WebSocket 应用。这里用 aah 为上述 app 实现一个 REST API 后端服务器。
REST API 服务实施(STEP BY STEP)
1、安装 aah CLI 客户端工具
1)macOS、Linux、BSD 系统和带有 Cygwin 的 Windows:
# 安装最新版本的 aah CLI $ curl -s https://aahframework.org/install-cli | bash # 或者 $ wget -qO- https://aahframework.org/install-cli | bash
2)对于 Windows 用户,建议以源代码方式进行安装
λ git clone -b master --single-branch --depth 1 https://github.com/go-aah/tools.git aah-cli λ cd aah-cli\aah λ go install go: aahframe.work@v0.12.5: missing go.sum entry; to add it: go mod download aahframe.work D:\working\golang\src\github.com\go-aah\tools\aah-cli\aah (master -> origin) λ go mod tidy go: finding module for package gopkg.in/go-playground/assert.v1 go: finding module for package google.golang.org/appengine go: finding module for package google.golang.org/appengine/urlfetch go: finding module for package golang.org/x/sys/unix go: found google.golang.org/appengine in google.golang.org/appengine v1.6.7 go: found gopkg.in/go-playground/assert.v1 in gopkg.in/go-playground/assert.v1 v1.2.1 go: found google.golang.org/appengine/urlfetch in google.golang.org/appengine v1.6.7 go: found golang.org/x/sys/unix in golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 λ go install λ aah -v aah v0.12.5 cli v0.13.6 go v1.16.4
2、生成 api 类型的骨架代码
D:\working\vulcangz λ aah new --------------------------------------------------------------- aah framework v0.13.0-edge (cli v0.13.4) --------------------------------------------------------------- # Report improvements/bugs at https://aahframework.org/issues # Welcome to interactive way to create your aah application, press ^C to exit :) Based on your inputs, aah CLI generates the aah application structure for you. Enter your application import path: aah-recycleview-backend Enter your application location: Choose your application type (web, api or websocket), default is 'web': api Choose your application Auth Scheme (basic, generic), default is 'none': Would you like to enable CORS? [y/N]: Downloading aah quick start app templates from https://github.com/go-aah/app-templates.git Your aah api application was created successfully at 'aah-recycleview-backend' You shall run your application via the command 'aah run' from application base directory. Go to https://docs.aahframework.org to learn more and customize your aah application.
生成的骨架代码目录结构如下:
D:. | .gitignore | aah.project | go.mod | go.sum | aah-recycleview-backend.pid | +---app | | aah.go | | init.go | | | +---controllers | | | app.go | | | | | \---v1 | | value.go | | | +---generated | | add_controllers.go | | | \---models | greet.go | value.go | +---build | \---bin | aah-recycleview-backend.exe | \---config | aah.conf | routes.conf | security.conf | \---env dev.conf prod.conf
3、进入应用目录,运行项目:
D:\working\vulcangz\aah\aah-recycleview-backend λ aah run --------------------------------------------------------------- aah framework v0.13.0-edge (cli v0.13.4) --------------------------------------------------------------- # Report improvements/bugs at https://aahframework.org/issues # Loaded aah project file: D:\working\vulcangz\aah\aah-recycleview-backend\aah.project Hot-Reload enabled for environment profile: dev Compile starts for 'aah-recycleview-backend' [aah-recycleview-backend] Compile successful for 'aah-recycleview-backend' [aah-recycleview-backend] 2022-01-25 20:46:24.434 INFO aah framework v0.13.0-edge, requires >= go1.11 2022-01-25 20:46:24.435 DEBUG aah-recycleview-backend Domain count: 1 2022-01-25 20:46:24.435 DEBUG aah-recycleview-backend Domain: localhost:8080, routes found: 6 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Base Directory: D:\working\vulcangz\aah\aah-recycleview-backend 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Virtual Base Directory: /app 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Name: aah-recycleview-backend 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Build Version: 0.0.1 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Build Timestamp: 2022-01-25T20:46:19+08:00 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Single Binary Mode: false 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Profile: dev 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App TLS/SSL Enabled: false 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Session Mode: stateless 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Route Domains: localhost:8080 2022-01-25 20:46:24.436 INFO aah-recycleview-backend App Shutdown Grace Timeout: 60s 2022-01-25 20:46:24.436 DEBUG aah-recycleview-backend Subscribed event callbacks 2022-01-25 20:46:24.436 DEBUG aah-recycleview-backend Event: OnStart (callback=aah-recycleview-backend/app/generated.AddControllers priority=1) 2022-01-25 20:46:24.436 DEBUG aah-recycleview-backend Event: OnStart (callback=main.SubscribeHTTPEvents priority=1) 2022-01-25 20:46:24.436 DEBUG aah-recycleview-backend Publishing event 'OnStart' in synchronous mode 2022-01-25 20:46:24.436 INFO aah-recycleview-backend Add 2 Controllers from 'app/controllers/**' 2022-01-25 20:46:24.436 DEBUG aah-recycleview-backend Adding aah-recycleview-backend/app/controllers.AppController 2022-01-25 20:46:24.436 DEBUG aah-recycleview-backend Adding aah-recycleview-backend/app/controllers/v1.ValueController 2022-01-25 20:46:24.468 INFO aah-recycleview-backend aah go server running on :8080
如果 aah run 出现错误,可运行 go mod tidy 解决依赖问题。
D:\working\vulcangz\aah-recycleview-backend λ go mod tidy
4、在浏览器访问:http://localhost:8080
(aah 默认样板 web 访问信息)
5、创建实体模型(model)和控制器(controller)
为简化示例,数据存储采用 “IN-MEMORY” store 存储方式,具体可参考官方示例项目:https://github.com/go-aah/examples/tree/v0.12.x/re...
1)在 app\models 目录下创建 industy.go,键入代码代码:
package models import ( "errors" "sync" "time" ) var ( industryStore = make(map[int64]*Industry) lastIndustryID int64 idMx = &sync.Mutex{} industryMx = &sync.RWMutex{} ) type Industry struct { IndustryID int64 `json:"IndustryID,omitempty"` IndustryName string `json:"IndustryName,omitempty"` Nature string `json:"Nature,omitempty"` Count int64 `json:"Count,omitempty"` CreatedAt time.Time `json:"-"` UpdatedAt time.Time `json:"-"` } // IndustryAlias is used for time format to ISO 8061 type IndustryAlias Industry // AllIndustries returns all the industries. func AllIndustries() []interface{} { industryMx.RLock() defer industryMx.RUnlock() industries := make([]interface{}, 0) for _, industry := range industryStore { industries = append(industries, ToIndustryAlias(industry)) } return industries } // CreateIndustry method creates the new industry entry. func CreateIndustry(industry *Industry) int64 { industryMx.Lock() defer industryMx.Unlock() id := createIndustryID() industry.IndustryID = id industry.CreatedAt = time.Now() industry.UpdatedAt = industry.CreatedAt industryStore[id] = industry return id } // GetIndustry method return the industry for given ID. func GetIndustry(id int64) (*Industry, error) { industryMx.RLock() defer industryMx.RUnlock() if _, found := industryStore[id]; !found { return nil, errors.New("industry not found") } industryStore[id].Count++ return industryStore[id], nil } // UpdateIndustry method updates the given info with industry store. func UpdateIndustry(industry *Industry) error { industryMx.Lock() defer industryMx.Unlock() if _, found := industryStore[industry.IndustryID]; !found { return errors.New("industry not found") } industryStore[industry.IndustryID].IndustryName = industry.IndustryName industryStore[industry.IndustryID].Nature = industry.Nature industryStore[industry.IndustryID].Count = industry.Count industryStore[industry.IndustryID].UpdatedAt = time.Now() return nil } // DeleteIndustry method deletes the industry for given ID. func DeleteIndustry(id int64) error { industryMx.Lock() defer industryMx.Unlock() if _, found := industryStore[id]; !found { return errors.New("industry not found") } delete(industryStore, id) return nil } // ToIndustryAlias method formats the time to RFC3339 using alias. func ToIndustryAlias(industry *Industry) interface{} { return &struct { *IndustryAlias CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` }{ IndustryAlias: (*IndustryAlias)(industry), CreatedAt: industry.CreatedAt.Format(time.RFC3339), UpdatedAt: industry.UpdatedAt.Format(time.RFC3339), } } func createIndustryID() int64 { idMx.Lock() defer idMx.Unlock() lastIndustryID++ return lastIndustryID } func init() { industryStore[1] = &Industry{IndustryID: 1, IndustryName: "Electronics", Nature: "1", Count: 4} industryStore[2] = &Industry{IndustryID: 2, IndustryName: "Mobile & Tablets", Nature: "1", Count: 3} // and so on... industryStore[20] = &Industry{IndustryID: 20, IndustryName: "Cultery Crockery", Nature: "1", Count: 3} lastIndustryID = 20 }
2)在 app\controllers\v1 目录下创建 industy.go, 键入代码如下:
package v1 import ( "fmt" "aah-recycleview-backend/app/models" aah "aahframe.work" ) // IndustryController implements the industry APi endpoints. type IndustryController struct { *aah.Context } // List method returns all the industries available. func (c *IndustryController) List() { industries := models.AllIndustries() c.Log().Infof("%v industries found", len(industries)) c.Reply().JSON(aah.Data{ "industries": industries, }) } // Create method to create a industry via JSON. func (c *IndustryController) Create(industry *models.Industry) { c.Log().Infof("Industry Info: %+v", *industry) id := models.CreateIndustry(industry) newResourceURL := fmt.Sprintf("%s:%v", c.Req.Scheme, c.RouteURL("retrieve_industry", id)) c.Reply().Created(). Header("Location", newResourceURL). JSON(aah.Data{ "id": id, }) } // Retrieve method retunrs single industry details for given industry ID. func (c *IndustryController) Retrieve(id int64) { c.Log().Infof("Retrieving industry, ID: %v", id) industry, err := models.GetIndustry(id) if err != nil { c.Log().Errorf("Industry ID %v, %v", id, err) c.Reply().NotFound().JSON(aah.Data{ "error": err.Error(), }) return } c.Reply().JSON(models.ToIndustryAlias(industry)) } // Update method updates the industry with given content. func (c *IndustryController) Update(id int64, industry *models.Industry) { c.Log().Infof("Updating industry: %v", id) if industry.IndustryID == 0 { industry.IndustryID = id } if err := models.UpdateIndustry(industry); err != nil { c.Log().Errorf("Industry ID %v, %v", id, err) c.Reply().NotFound().JSON(aah.Data{ "error": err.Error(), }) return } c.Reply().NoContent() } // Delete method deletes the industry of given industry ID. func (c *IndustryController) Delete(id int64) { c.Log().Infof("Deleting industry, ID: %v", id) if err := models.DeleteIndustry(id); err != nil { c.Log().Errorf("Industry ID %v, %v", id, err) c.Reply().NotFound().JSON(aah.Data{ "error": err.Error(), }) return } c.Reply().NoContent() }
6、配置路由
1)配置服务器监听端口为 80
打开 config\aah.conf 文件,在 “server {” 块内添加:
port = "80"
2)配置 industry 路由
打开 config\routes.conf,在 “api_v1” 块内的 “routes” 子块内 “all_values” 子块结束处的下方(L63 行之后),添加 industry 相关路由如下:
64 industry { 65 path = "/industry" 66 controller = "IndustryController" 67 action = "List" 68 69 routes { 70 create_industry { 71 method = "POST" 72 } 73 retrieve_industry { 74 path = "/:id" 75 action = "Retrieve" 76 77 routes { 78 update_industry { 79 method = "PUT" 80 } 81 delete_industry { 82 method = "DELETE" 83 } 84 } 85 } 86 } 87 } # end - industry
这里的 L73~85:retrieve_industry 区块,74~75 行表示该区块中各路由首先需要从 path 中 "Retrieve"(取出)industry id,也就是说:HTTP request URL 中的 PATH 部分必须包含有 "id",这样后续的 "update" 或 "delete" 等操作才能正常往下进行 。
到这里代码和配置的部分就完成了。aah 支持热重载(Hot-Reload),代码和配置文件更改时,aah 会自动停止服务器,构建它并使用更新的代码库启动服务器。
这时候在浏览器访问 http://localhost/v1/industry 可以列出全部 industries 条目的 json 数据;访问 http://localhost/v1/industry/1 显示第1条 industry 的 json 数据,依此类推。
7、Android app 的调试
1)克隆 Android app repo
git clone git@github.com:vulcangz/Android-RecyclerView-Sort-Ascending-Descending.git
2)导入到 Android Studio 并编译、运行
然后在 Android Studio 选择 “File”——"New"——"Import Project" 导入即可,需要修改的只是 1.1 中所提到的 “请求 URL”。在这里我把它改为了 "http://192.168.100.16/v1/industry" 这样的形式,请把它改为自己的服务器 IP。
按照规范要求:URL 中的 “industry” 应为复数 "industries" 形式,这里为了与原 app 中的网络请求 URL 对应保持了单数形式。希望不要误导大家。
测试
后端 API 服务的功能测试可以结合 POSTMAN 和浏览器来完成。我在 repo 中的 docs\postman 目录中提供了 POSTMAN 测试集文件,读者可导入到 POSTMAN 进行测试。您需要自行修改一下后端服务端口为 80,测试集中设置的是 8080。
App 的测试可以通过 Android Studio 和 安卓模拟器来完成。
总结
这一篇教程中基于 aah 框架实现了一个 REST API 形式的 CRUD 应用服务,采取 “IN-MEMORY” store 存储而不是数据库存储方式,简化了程序结构和数据处理的复杂度,真正地让你在几分钟之内就可以搭建好一个后端服务。
当然,aah 框架所内置的功能远不止于此。aah 在安全性部分的设计颇具特色,它借鉴了 Apache Shiro 安全库,“Subject”、"Principal"的概念容易让人迷惑,但这些是安全领域的专业术语,如果您了解 Apache Shiro,接受起来应该不难。
“原生多租户(域和子域)” 、“虚拟文件系统(VFS)”、以及使用简单的 “i18n 国际化” 都是让我印象深刻的特性。
与 Echo、Gin 等其他框架不同,aah 把路由这一部分由常规的代码实现改为由配置来实现,根据配置来自动生成对应的处理程序——我们可以看一下 app\generated 目录下生成的文件 add_controllers.go,它实现的就是类似于路由的功能。
如有兴趣可以看一下 app\init.go 文件。aah 框架在其请求生命周期中提供了
“OnRequest”、“OnPreReply”、“OnHeaderReply”、“OnPostReply”、“OnPreAuth” 和 “OnPostAuth” 等事件扩展点,这赋予给开发者 hack 空间。
参考
关注开源技术发展,发掘实用开源项目。秀项目,秀 code!这就是 WorldLink 资源网。关注公号 “Worldlink资源网”,可及时收到文章推送。技术文章是会修改和更新的,也欢迎您有空时过来看看。