框架概述
mcube是一款用于构建渐进式微服务(单体-->微服务)的框架, 让应用从单体无缝过渡到微服务, 同时提供丰富的配置即用的功能配置, 只需简单配置就可拥有:
- Log: 支持文件滚动和Trace的日志打印
- Metric: 支持应用自定义指标监控
- Trace: 集成支持完整的全链路追踪(HTTP Server/GRPC Server/数据库...)以及自定义埋点
- CORS: 资源跨域共享
- Health Check: HTTP 和 GRPC 健康检查
- API DOC: 基于Swagger的 API 文档
除了上面这些功能配置,还会用到很多三方工具, 也是配置即用:
- MySQL: Grom集成
- MongoDB 官方驱动集成
- Redis: go-redis集成
- Kafka: kafka-go集成
- 分布式缓存: 当前只适配了Redis
- 分布式锁: 当前只适配了Redis
mcube的核心理念是提供一套优良的软件架构, 应对软件从简单到复杂的过程。所以关注点在架构设计, 而不是发明工具, 因此尽量选择集成社区优秀的工具。
核心概念
框架采用面向对象的设计思想, 将整个程序的对象分为4类:
- API: 接口对象, 负责对外提供接口, 现支持框架: Gin/GoRestful
- Controller: 负责编写具体的业务逻辑, 如果业务非常简单, 且没有复用场景 可以考虑直接写在API对象内部
- Config: 配置对象, 业务逻辑中共用的工具, 比如 数据库, kafka, redis之类
- Default: 通用对象, 除了上面3中对象之外的其他对象, 预留区域,暂时未使用
这4类对象分别存储在4个区域(Namespace), 这4个区域共同组成的这个容器就叫Ioc分区容器:
其中 API和Controller区域的对象 需要应用开发者开发, 而Config区域的对象 由框架和社区开发
应用开发
下面演示一个简单应用的开发, 开发一个API 接口获取DB Stats, 要获取DB Stats, 只需要获取到DB的连接池对象, 调用Stats方法即可:
db.Stats()
返回结果:
{
"data": {
"MaxOpenConnections": 0,
"OpenConnections": 1,
"InUse": 0,
"Idle": 1,
"WaitCount": 0,
"WaitDuration": 0,
"MaxIdleClosed": 0,
"MaxIdleTimeClosed": 0,
"MaxLifetimeClosed": 0
}
}
选择开发框架
Api对象支持2种框架:
- Gin
- GoRestful(v3)
Object接口
无论使用那个框架,都需要实现Object接口, 只有实现了该接口,该对象才能被ioc空间管理:
// Object接口, 需要注册到ioc空间托管的对象需要实现的方法
type Object interface {
// 对象初始化, 初始化对象的属性
Init() error
// 对象的名称, 根据名称可以从空间中取出对象
Name() string
// 对象版本, 默认v1
Version() string
// 对象优先级, 根据优先级 控制对象初始化的顺序
Priority() int
// 对象的销毁, 服务关闭时调用
Close(ctx context.Context) error
// 是否允许同名对象被替换, 默认不允许被替换
AllowOverwrite() bool
// 对象一些元数据, 对象的更多描述信息, 扩展使用
Meta() ObjectMeta
}
可用通过继承ObjectImpl对象 获取Object接口的默认实现, 然后按需进行覆写, 下面是我们设计的ApiHandler对象:
import (
"github.com/infraboard/mcube/v2/ioc"
)
type ApiHandler struct {
// 继承自Ioc对象
ioc.ObjectImpl
}
实现Object接口, 注册对象
通过继承已经实现了Object接口
type ApiHandler struct {
// 继承自Ioc对象
ioc.ObjectImpl
// db属性, gorm DB对象
db *gorm.DB
}
覆写对象的名称, 默认为对象反射名称,比如: *main.ApiHandler
// 覆写对象的名称, 该名称名称会体现在API的路径前缀里面
// 比如: /simple/api/v1/module_a/db_stats
// 其中/simple/api/v1/module_a 就是对象API前缀, 命名规则如下:
// <service_name>/<path_prefix>/<object_version>/<object_name>
func (h *ApiHandler) Name() string {
return "module_a"
}
这样ApiHandler就实现了GinApiObject接口, 此时就可以把它注册到api区域了:
import (
"github.com/infraboard/mcube/v2/ioc"
)
func main() {
// 注册HTTP接口类
ioc.Api().Registry(&ApiHandler{})
...
}
对象初始化
现在ApiHandler的db属性还没初始化, 可以直接通过引入Datasource组件, 获取Gorm的db对象
type ApiHandler struct {
// 继承自Ioc对象
ioc.ObjectImpl
// db属性, gorm DB对象
db *gorm.DB
}
要完成对象属性的初始化需要覆写Init()方法, mcube框架在启动时,会自动调用该方法来初始化对象
// 初始化db属性, 从ioc的配置区域获取共用工具 gorm db对象
func (h *ApiHandler) Init() error {
h.db = datasource.DB()
// 进行业务暴露, router 通过ioc
router := ioc_gin.RootRouter()
router.GET("/db_stats", h.GetDbStats)
return nil
}
// 业务功能
func (h *ApiHandler) GetDbStats(ctx *gin.Context) {
db, _ := h.db.DB()
ctx.JSON(http.StatusOK, gin.H{
"data": db.Stats(),
})
}
然后通过配置文件进行组件配置: etc/application.toml (后面启动时会制定配置文件读取路径)
[datasource]
host = "127.0.0.1"
port = 3306
username = "root"
password = "123456"
database = "test"
启动服务器
通过server.Run()启动服务:
import (
"github.com/infraboard/mcube/v2/ioc/server"
)
func main() {
// 注册HTTP接口类
ioc.Api().Registry(&ApiHandler{})
// 开启配置文件读取配置
server.DefaultConfig.ConfigFile.Enabled = true
server.DefaultConfig.ConfigFile.Path = "etc/application.toml"
// 启动应用
err := server.Run(context.Background())
if err != nil {
panic(err)
}
}
最后启动服务 可以看到api已经可以访问:
$ mcube/examples/simple ‹master*› » go run main.go
2024-01-04T21:48:48+08:00 INFO ioc/server/server.go:74 > loaded configs: [app.v1 trace.v1 log.v1 datasource.v1 grpc.v1 http.v1] component:SERVER
2024-01-04T21:48:48+08:00 INFO ioc/server/server.go:75 > loaded controllers: [] component:SERVER
2024-01-04T21:48:48+08:00 INFO ioc/server/server.go:76 > loaded apis: [module_a.v1] component:SERVER
[GIN-debug] GET /simple/api/v1/module_a/db_stats --> main.(*ApiHandler).Registry.func1 (3 handlers)
2024-01-04T21:48:48+08:00 INFO config/http/http.go:211 > HTTP服务启动成功, 监听地址: 127.0.0.1:8020 component:HTTP
完整代码
完整样例代码
package main
import (
"context"
"net/http"
"github.com/gin-gonic/gin"
"github.com/infraboard/mcube/v2/ioc"
"github.com/infraboard/mcube/v2/ioc/config/datasource"
ioc_gin "github.com/infraboard/mcube/v2/ioc/config/gin"
"github.com/infraboard/mcube/v2/ioc/server"
"gorm.io/gorm"
// 开启Health健康检查
_ "github.com/infraboard/mcube/v2/ioc/apps/health/gin"
// 开启Metric
_ "github.com/infraboard/mcube/v2/ioc/apps/metric/gin"
)
func main() {
// 注册HTTP接口类
ioc.Api().Registry(&ApiHandler{})
// 开启配置文件读取配置
server.DefaultConfig.ConfigFile.Enabled = true
server.DefaultConfig.ConfigFile.Path = "etc/application.toml"
// 启动应用
err := server.Run(context.Background())
if err != nil {
panic(err)
}
}
type ApiHandler struct {
// 继承自Ioc对象
ioc.ObjectImpl
// mysql db依赖
db *gorm.DB
}
// 覆写对象的名称, 该名称名称会体现在API的路径前缀里面
// 比如: /simple/api/v1/module_a/db_stats
// 其中/simple/api/v1/module_a 就是对象API前缀, 命名规则如下:
// <service_name>/<path_prefix>/<object_version>/<object_name>
func (h *ApiHandler) Name() string {
return "module_a"
}
// 初始化db属性, 从ioc的配置区域获取共用工具 gorm db对象
func (h *ApiHandler) Init() error {
h.db = datasource.DB()
// 进行业务暴露, router 通过ioc
router := ioc_gin.RootRouter()
router.GET("/db_stats", h.GetDbStats)
return nil
}
// 业务功能
func (h *ApiHandler) GetDbStats(ctx *gin.Context) {
db, _ := h.db.DB()
ctx.JSON(http.StatusOK, gin.H{
"data": db.Stats(),
})
}
工程化
有同学看到这里可能会十分不解, 上面这样简单的功能, 我几行代码就能搞定, 为啥要搞那么复杂:
func main() {
r := gin.Default()
dsn := "user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{})
if err != nil {
panic(err)
}
r.GET("/simple/api/v1/module_a/db_stats", func(ctx *gin.Context) {
db, _ := db.DB()
ctx.JSON(http.StatusOK, gin.H{
"data": db.Stats(),
})
})
}
针对简单的功能, 上面这样写固然没问题, 甚至更易读, 但是随着业务发展, 功能越来越多, 业务越来越复杂, 如果这样写, 到了后期基本无法维护。
想要业务从简单到复杂, 代码维护的复杂度不增加, 我们就需要对工程进行统一的设计, 提供统一的标准, 我们通过下面3个纬度体验mcube 工程化带来的好处:
- 标准化工程配置
- 即插即用的组件
- 灵活的模块组合
标准化工程配置
统一了项目的配置加载方式:
- 环境变量
- 配置文件
- TOML
- YAML
- JSON
下面是项目配置文件(etc/application.toml)内容:
[app]
name = "simple"
key = "this is your app key"
[http]
host = "127.0.0.1"
port = 8020
[datasource]
host = "127.0.0.1"
port = 3306
username = "root"
password = "123456"
database = "test"
[log]
level = "debug"
[log.file]
enable = true
file_path = "logs/app.log"
更多配置见左侧应用配置板块
即插即用的组件
通过简单的配置就能为项目添加:
- 检查检查(Health Chcek)
- 应用指标监控(Metric)
import (
// 开启Health健康检查
_ "github.com/infraboard/mcube/v2/ioc/apps/health/gin"
// 开启Metric
_ "github.com/infraboard/mcube/v2/ioc/apps/metric/gin"
)
启动过后, 在日志里就能看到这2个功能开启了:
2024-01-05T11:30:00+08:00 INFO health/gin/check.go:52 > Get the Health using http://127.0.0.1:8020/healthz component:HEALTH_CHECK
2024-01-05T11:30:00+08:00 INFO metric/gin/metric.go:51 > Get the Metric using http://127.0.0.1:8020/metrics component:METRIC
当然你也可以通过配置来修改功能的URL路径:
[health]
path = "/healthz"
[metric]
enable = true
provider = "prometheus"
endpoint = "/metrics"
功能强大的模块
在服务开发的过程中,我们将一些通用功能打包成一个模块, 在项目中引入, 这样可以减少重复代码, 提高开发效率。
下面是一个IAM(身份与权限管理)模块:
- 管理管理接口
- 令牌管理接口
- 权限中间件
- 初始化的CLI
在程序启动时引入模块:
import (
// 引入IAM模块组件
_ "github.com/infraboard/modules/iam"
// 引入IAM模块CLI工具
_ "github.com/infraboard/modules/iam/cmd"
)
然后初始化:
# 初始化管理员用户
$ modules/iam/example ‹main*› » go run main.go init 1 ↵
? 请输入管理员用户名称: admin
? 请输入管理员密码: ******
? 再次输入管理员密码: ******
在业务接 口开发中,使用中间件 就引入的身份认证和RBAC鉴权
// API路由
func (h *ApiHandler) Registry(r gin.IRouter) {
r.Use(middleware.Auth())
r.GET("/db_stats", middleware.Perm(user.ROLE_ADMIN), h.DBStats)
}
模块的具体使用文档可以参考: IAM模块