用 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资源网”,可及时收到文章推送。技术文章是会修改和更新的,也欢迎您有空时过来看看。
