ZEROMAKE | keep codeing and thinking!
2019-12-21 | go

go-spring 使用学习

前言

  • 最近发现了 go-spring 并且发布了 v1.0.0-beta 版。
  • 看了一下感觉挺不错的,最近离职在家学习就花了一天时间学习这边记录一下

一、安装

1
# 拉取 go spring
2
$ go get github.com/go-spring/[email protected]
3
4
# 如果需要使用 go-spring 做 web 服务需要以下包
5
# go-spring-boot-starter 是使用 spring-boot 包装的支持 web 以及其它的启动器
6
$ go get github.com/go-spring/[email protected]
7
# go-spring-web 则是配合 go-spring-boot-starter 使用的各种 web 框架的封装
8
$ go get github.com/go-spring/[email protected]

由于 go-spring 现在还是 beta 版,每天都有可能有一些重要更新建议拉取最新的 master

不过到了后面 go-spring 正式版也许就不需要直接手动拉取 @master 了,请自行判断。

二、go-spring 项目包结构介绍

1
$ tree . -L 1
2
.
3
├── CONTRIBUTING.md
4
├── LICENSE
5
├── README.md
6
├── RunAllTests.sh
7
├── RunCodeCheck.sh
8
├── RunGoDoc.sh
9
├── boot-starter
10
├── go.mod
11
├── go.sum
12
├── package-info.go
13
├── spring-boot
14
├── spring-core
15
├── starter-echo
16
├── starter-gin
17
└── starter-web
18
19
6 directories, 9 files

其中 starter 本来在 go-spring-boot-starter 仓库里,作者为减少引入包已经把这些 starter 移动到了 go-spring 仓库里。

starter 部分的暂时无视,这样一看就只剩下 spring-corespring-bootboot-starter

  • spring-core 是用于 IoC 容器注入的核心库。
  • spring-boot 是使用了 spring-core 构建的配置自动载入,还有注入的对象的启动和关闭的统一管理。
  • boot-starter 简单启动和监听信号包装器。

三、一个简单 gin web 服务

1
package main
2
3
import (
4
SpringWeb "github.com/go-spring/go-spring-web/spring-web"
5
SpringBoot "github.com/go-spring/go-spring/spring-boot"
6
"net/http"
7
8
_ "github.com/go-spring/go-spring/starter-gin"
9
_ "github.com/go-spring/go-spring/starter-web"
10
)
11
12
func init() {
13
SpringBoot.RegisterBean(new(Controller)).InitFunc(func(c *Controller) {
14
SpringBoot.GetMapping("/", c.Home)
15
})
16
}
17
18
type Controller struct{}
19
20
func (c *Controller) Home(ctx SpringWeb.WebContext) {
21
ctx.String(http.StatusOK, "OK!")
22
}
23
24
func main() {
25
SpringBoot.RunApplication("config/")
26
}
  • 其中 init 方法里我们注册了一个 Controller 的空实例,这个不一定要在 init 中注册,可以在 SpringBoot.RunApplication 调用前的任意地方注册,使用 init 的原因是可以不依赖包内部方法只需要导入即可注入。
  • 然后通过 InitFunc 注册路由,SpringBoot.GetMapping 是统一封装的路由挂载器
  • Home(ctx SpringWeb.WebContext) 里的 SpringWeb.WebContext 则封装了请求响应操作。
  • github.com/go-spring/go-spring/starter-gin 导入替换为 github.com/go-spring/go-spring/starter-echo 可以直接替换为 echo 框架。

执行该文件会打出大量的注册初始化日志,正式版应该会能够关闭。

1
$ go run main.go
2
register bean "github.com/go-spring/go-spring/starter-web/WebStarter.WebServerStarter:*WebStarter.WebServerStarter"
3
register bean "main/main.Controller:*main.Controller"
4
register bean "github.com/go-spring/go-spring/spring-boot/SpringBoot.DefaultApplicationContext:*SpringBoot.DefaultApplicationContext"
5
register bean "github.com/go-spring/go-spring/starter-web/WebStarter.WebServerConfig:*WebStarter.WebServerConfig"
6
wire bean github.com/go-spring/go-spring/spring-boot/SpringBoot.DefaultApplicationContext:*SpringBoot.DefaultApplicationContext
7
success wire bean "github.com/go-spring/go-spring/spring-boot/SpringBoot.DefaultApplicationContext:*SpringBoot.DefaultApplicationContext"
8
wire bean github.com/go-spring/go-spring/starter-web/WebStarter.WebServerConfig:*WebStarter.WebServerConfig
9
success wire bean "github.com/go-spring/go-spring/starter-web/WebStarter.WebServerConfig:*WebStarter.WebServerConfig"
10
wire bean github.com/go-spring/go-spring/starter-web/WebStarter.WebServerStarter:*WebStarter.WebServerStarter
11
success wire bean "github.com/go-spring/go-spring/starter-web/WebStarter.WebServerStarter:*WebStarter.WebServerStarter"
12
wire bean main/main.Controller:*main.Controller
13
success wire bean "main/main.Controller:*main.Controller"
14
spring boot started
15
⇨ http server started on :8080

访问 http://127.0.0.1 可以看到上面的代码效果。

该章节代码见 post-1 分支。

四、拆分 controller 并自动注册路由

现代项目都是 controller + service 外加一个实体层,这里我们试着把 controller 拆分出去。

新建一个 controllers 目录下面创建一个 controllers.go 来导入各个独立的 controller

controllers/home/home.go

1
package home
2
3
import (
4
SpringWeb "github.com/go-spring/go-spring-web/spring-web"
5
SpringBoot "github.com/go-spring/go-spring/spring-boot"
6
"net/http"
7
)
8
9
type Controller struct {}
10
11
func init() {
12
SpringBoot.RegisterBean(new(Controller)).InitFunc(func(c *Controller) {
13
SpringBoot.GetMapping("/", c.Home)
14
})
15
}
16
17
func (c *Controller) Home(ctx SpringWeb.WebContext) {
18
ctx.String(http.StatusOK, "OK!")
19
}

controllers/controllers.go

1
package controllers
2
3
// 导入各个 controller 即可实现路由挂载
4
import (
5
_ "github.com/zeromake/spring-web-demo/controllers/home"
6
)
7

main.go

1
package main
2
3
import (
4
_ "github.com/go-spring/go-spring/starter-gin"
5
_ "github.com/go-spring/go-spring/starter-web"
6
SpringBoot "github.com/go-spring/go-spring/spring-boot"
7
_ "github.com/zeromake/spring-web-demo/controllers"
8
)
9
10
func main() {
11
SpringBoot.RunApplication("config/")
12
}
13

重新运行 go run main.go 访问浏览器能获得相同的效果,这样我们就把 controller 拆分出去了。

该章节代码见 post-2 分支。

五、构建 service 的自动注入到 controller

上面说到 controller 的主要的能力为路由注册,参数处理复杂的逻辑应当拆分到 service 当中。

在我使用 go-spring 之前都是手动的构建一个 map[string]interface{} 然后把 service 按照自定义名字挂进去。

然后在 controller 构建时从这个 map 中取出并强制转换为 service 类型或者抽象的接口。

这个方案问题蛮大的,手动的 service 名称容易出错,而且注册和在 controller 注入都是非常麻烦的,而且错误处理也都没做。

但是这一切有了 go-spring 就不一样了,我只需要在 service 注册,在 controller 里的结构体里声明这个 service 类型实例就可以使用。

为了不作为一个示例而太简单让学习者觉得没有什么意义,我决定做一个上传的能力,先看未拆分 service 的情况

controllers/upload/upload.go

1
package upload
2
3
import (
4
// ……
5
)
6
7
type Controller struct{}
8
9
func init() {
10
SpringBoot.RegisterBean(new(Controller))InitFunc(func(c *Controller) {
11
SpringBoot.GetMapping("/upload", c.Upload)
12
})
13
}
14
15
func (c *Controller) Upload(ctx SpringWeb.WebContext) {
16
file, err := ctx.FormFile("file")
17
if err != nil {
18
// ……
19
return
20
}
21
w, err := file.Open()
22
if err != nil {
23
// ……
24
return
25
}
26
defer func() {
27
_ = w.Close()
28
}()
29
out := path.Join("temp", file.Filename)
30
if !PathExists(out) {
31
dir := path.Dir(out)
32
if !PathExists(dir) {
33
err = os.MkdirAll(dir, DIR_MARK)
34
if err != nil {
35
// ……
36
return
37
}
38
}
39
dst, err := os.OpenFile(out, FILE_FLAG, FILE_MAEK)
40
if err != nil {
41
// ……
42
return
43
}
44
defer func() {
45
_ = dst.Close()
46
}()
47
_, err = io.Copy(dst, w)
48
if err != nil {
49
// ……
50
return
51
}
52
} else {
53
// ……
54
return
55
}
56
ctx.JSON(http.StatusOK, gin.H{
57
"code": 0,
58
"message": http.StatusText(http.StatusOK),
59
"data": map[string]string{
60
"url": out,
61
},
62
})
63
}
64
65
func PathExists(path string) bool {
66
// ……
67
}
68

运行 go run main.go 然后用 curl 上传测试。

1
$ curl -F "[email protected]/README.md" http://127.0.0.1:8080/upload
2
{"code":0,"data":{"url":"temp/README.md"},"message":"OK"}
3
# 重复上传会发现文件已存在
4
$ curl -F "[email protected]/README.md" http://127.0.0.1:8080/upload
5
{"code":1,"message":"该文件已存在"}

在项目下的 temp 文件夹中能够找到上传后的文件。

以上能正常运行但是 controller 中包含了大量的逻辑而且均为文件操作 api 耦合性过高。

我们需要把上面的的文件操作拆分到 service 当中。

services/file/file.go

将文件操作逻辑抽取为 PutObject(name string, r io.Reader, size int64) (err error)ExistsObject(name string) bool

1
package file
2
3
type Service struct{}
4
5
func init() {
6
SpringBoot.RegisterBean(new(Service))
7
}
8
9
func (s *Service) PutObject(name string, r io.Reader, size int64) (err error) {
10
// ……
11
}
12
13
func (s *Service) ExistsObject(name string) bool {
14
// ……
15
}
16

services/services.go

1
package services
2
3
import (
4
_ "github.com/zeromake/spring-web-demo/services/file"
5
)
6

main.go

增加 services 的导入。

1
package main
2
3
import (
4
// ……
5
_ "github.com/zeromake/spring-web-demo/services"
6
)
7
8
func main() {
9
SpringBoot.RunApplication("config/")
10
}
11

controllers/upload/upload.go

Controller 上声明 File 并设置 tag autowire,这样 spring-boot 会自动注入 service 那边注册的实例。

1
package upload
2
3
import (
4
"github.com/gin-gonic/gin"
5
SpringWeb "github.com/go-spring/go-spring-web/spring-web"
6
SpringBoot "github.com/go-spring/go-spring/spring-boot"
7
"github.com/zeromake/spring-web-demo/services/file"
8
"net/http"
9
"path"
10
)
11
12
type Controller struct {
13
File *file.Service `autowire:""`
14
}
15
16
func (c *Controller) Upload(ctx SpringWeb.WebContext) {
17
// ……
18
if !c.File.ExistsObject(out) {
19
err = c.File.PutObject(out, w, f.Size)
20
if err != nil {
21
ctx.JSON(http.StatusInternalServerError, gin.H{
22
"code": 1,
23
"message": "保存失败",
24
"error": err.Error(),
25
})
26
return
27
}
28
} else {
29
ctx.JSON(http.StatusBadRequest, gin.H{
30
"code": 1,
31
"message": "该文件已存在",
32
})
33
return
34
}
35
// ……
36
}
37

重新运行 go run main.go 并测试,功能正常

1
$ rm temp/README.md
2
$ curl -F "[email protected]/README.md" http://127.0.0.1:8080/upload
3
{"code":0,"data":{"url":"temp/README.md"},"message":"OK"}
4
5
$ curl -F "[email protected]/README.md" http://127.0.0.1:8080/upload
6
{"code":1,"message":"该文件已存在"}

未拆分 service 的完整代码在 post-3 拆分了 service 的完整代码在 post-4

六、spring-boot 加载配置注入对象

我们启动服务时有传入一个 config/ 这个实际上是配置文件搜索路径。

SpringBoot.RunApplication("config/")

spring-boot 支持不少格式的配置和命名方式,这些都不介绍了。

只介绍一下怎么使用这些文件

config/application.toml

1
[spring.application]
2
name = "demo-config"
3
4
[file]
5
dir = "temp"

controllers/upload/upload.gocontroller 使用配置替换硬编码的保存文件夹路径, value:"${file.dir}" 对应配置文件的路径绑定。

1
type Controller struct {
2
File *file.Service `autowire:""`
3
Dir string `value:"${file.dir}"`
4
}
5
6
func (c *Controller) Upload(ctx SpringWeb.WebContext) {
7
// ……
8
// 替换为注入的配置
9
out := path.Join(c.Dir, f.Filename)
10
// ……
11
}

当然 spring-boot 也支持对结构体实例化配置数据还有默认值。

1
2
type Config struct {
3
Dir string `value:"${file.dir=tmp}"`
4
}
5
6
type Controller struct {
7
File *file.Service `autowire:""`
8
Config Config
9
}
10
11
func (c *Controller) Upload(ctx SpringWeb.WebContext) {
12
// ……
13
// 替换为注入的配置
14
out := path.Join(c.Config.Dir, f.Filename)
15
// ……
16
}

该章完整代码在 post-5

七、通过接口类型解除 controller 对 service 的依赖

以上代码已经很完整了,但是 controller 直接导入 service 造成对逻辑的直接依赖,这样会照成很高的代码耦合,而且导入 service 包也比较麻烦。

这里我们可以使用 interface 来做到解除依赖,这样不仅解决的导入的问题也能够快速的替换 serivce 的实现。

types/services.go

之前抽取的抽象方法派上用处了。

1
package types
2
3
import (
4
"io"
5
)
6
7
type FileProvider interface {
8
PutObject(name string, r io.Reader, size int64) error
9
ExistsObject(name string) bool
10
}

controllers/upload/upload.go

然后把 *file.Service 类型替换为 types.FileProvider 即可,spring-boot 会自动匹配接口对应的实例。

1
type Controller struct {
2
File types.FileProvider `autowire:""`
3
Dir string `value:"${file.dir}"`
4
}

该章完整代码在 post-6

八、通过 Condition 来限制 Bean 的注册来做到不同的 service 切换

上面我们说到用 interface 结构后是可以替换不同的逻辑实现的,这里我们就来一个对象存储和本地文件存储能力的更换,可以通过配置文件替换文件操作逻辑实现。

这里使用 minio 作为远端对象存储服务。

docker-compose 这里我们用 docker 快速创建一个本地的 minio 服务。

1
version: "3"
2
services:
3
minio:
4
image: "minio/minio:RELEASE.2019-10-12T01-39-57Z"
5
volumes:
6
- "./minio:/data"
7
ports:
8
- "9000:9000"
9
environment:
10
MINIO_ACCESS_KEY: minio
11
MINIO_SECRET_KEY: minio123
12
command:
13
- "server"
14
- "/data"

config/application.toml 添加 minio 配置

1
[minio]
2
enable = true
3
host = "127.0.0.1"
4
port = 9000
5
access = "minio"
6
secret = "minio123"
7
secure = false
8
bucket = "demo"

modules/minio/minio.go 单独的用 module 来做 minio 的客户端初始化。

1
package minio
2
3
type MinioConfig struct {
4
Enable bool `value:"${minio.enable:=true}"` // 是否启用 HTTP
5
Host string `value:"${minio.host:=127.0.0.1}"` // HTTP host
6
Port int `value:"${minio.port:=9000}"` // HTTP 端口
7
Access string `value:"${minio.access:=}"` // Access
8
Secret string `value:"${minio.secret:=}"` // Secret
9
Secure bool `value:"${minio.secure:=true}"` // Secure
10
Bucket string `value:"${minio.bucket:=}"`
11
}
12
13
func init() {
14
SpringBoot.RegisterNameBeanFn(
15
// 给这个实例起个名字
16
"minioClient",
17
// 自动注入 minio 配置
18
func(config MinioConfig) *minio.Client {
19
// ……
20
},
21
// 前面的 0 代表参数位置,后面则是配置前缀
22
"0:${}",
23
// ConditionOnPropertyValue 会检查配置文件来确认是否注册
24
).ConditionOnPropertyValue(
25
"minio.enable",
26
true,
27
)
28
}

记得收集导入到 main.go

services/file/file.go

本地存储 service 需要在没有注册 minioClient 的情况才注册。

1
func init() {
2
SpringBoot.RegisterBean(new(Service)).ConditionOnMissingBean("minioClient")
3
}

services/minio/minio.go

1
package minio
2
3
type Service struct {
4
// 自动注入 minio client
5
Client *minio.Client `autowire:""`
6
Bucket string `value:"${minio.bucket:=}"`
7
}
8
9
func init() {
10
// 在已注册了 minioClient 才注册
11
SpringBoot.RegisterBean(new(Service)).ConditionOnBean("minioClient")
12
}
13
14
func (s *Service) PutObject(name string, r io.Reader, size int64) error {
15
// ……
16
}
17
18
func (s *Service) ExistsObject(name string) bool {
19
// ……
20
}
21

然后启动 docker-compose up -d minio 启动 minio 服务。

修改 config/application.tomlminio.enable 可以切换存储能力。

本章完整代码在 post-7

求职

我是 zeromake 现在我离职中。

希望能够找到一个合适的新工作。

目标:Golang 开发,厦门优先

我的在线简历: zeromake的简历

顺便推广一下: docker-debug