给 Go 写一个类 Spring Boot 框架

·  阅读 1867
原文链接: zhuanlan.zhihu.com

前一段时间用 Kotlin -- spring-boot 写了一个项目

开发体验非常棒,颠覆了我对 “Java 那一套” 的刻板印象

Spring 的核心思想就是 DI 和 AOP

那在 Go 里面实现起来会是怎么样的呢

先给个 example 吧(完整文档看 godoc Rhapsody

type UserController struct {
	Controller 	`prefix:"api/v1"`
	GET 		`path:":id" method:"GetUserInfo"`
}

func (u *UserController) GetUserInfo(ctx echo.Context) error {
	// do something
}

type Root struct {
	*UserController
}

func main() {
	CreateApplication(new(Root)).Run()
}
复制代码
(注:这个项目的 Web 模块基于 echo

这个 example 比较简单,就是注册一个 prefix = api/v1 路由组,然后在这个路由组下注册一个 path = :id 的 controller

Controller 的作用是标记,相当于 spring 中的 @Controller() ,GET 也同理
你也可以在 上一层 中标记,如
type UserController struct {
	Controller 	
	GET 		`path:":id" method:"GetUserInfo"`
}

type Root struct {
	*UserController `prefix:"api/v1"`
}
复制代码
type UserController struct {	
	GET 		`path:":id" method:"GetUserInfo"`
}

type Root struct {
	*UserController `type:"controller" prefix:"api/v1"`
}
复制代码
(注:如果两层都有标注,那较高层 (Root) 会覆盖较低层 (UserController) 的注释)

这样似乎看不出太大优势,因为依赖注入没发挥太大作用

如果我们再写一个 ORM 的扩展呢

比如我要写一个 GORM 的拓展叫 GormConfig

之后我只需要

type Root struct {
	*UserController `prefix:"api/v1"`
        *GormConfig
        *Entities
}

type Entities struct {
        *User
        *Score
        // Your other models ...
}
复制代码

就可以

type UserController struct {
	Controller 	`prefix:"api/v1"`
	GET 		`path:":id" method:"GetUserInfo"`
        db *gorm.DB
}

func (u *UserController) GetUserInfo(ctx echo.Context) error {
	db.Create(&User{//balabala})
        // do something
}
复制代码

那 db 要怎么初始化呢,数据库连接怎么配置呢?

配置当然是写在配置文件里了

配置文件默认路径是 "./resources/application.conf"

默认类型是 json

如果想用 yaml ,直接把文件后缀改成 .yaml 或 .yml 就好了

那自定义路径怎么设置呢?

type Root struct {
        CONF            `path:"./conf/config.json" type:"yaml"`
	*UserController `prefix:"api/v1"`
        *GormConfig
        *Entities
}
复制代码

这样就可以了

注:约定优于配置,配置先于约定的原则,上面代码中的 config.json 会被当做 yaml 解析
所以请尽可能地减少配置

那把配置文件读入之后,我们要怎么拿到想要的值呢?

比如配置文件写着:

rhapsody:
  db:
    type: mysql
    database: rhapsody_demo
    username: gopher
    password: gopherLOVErhapsody
  redis:
    host: 127.0.0.1
    port: 6937
复制代码

那我们的 GORM 拓展要怎么写呢?

我们只需要

type SqlParameter struct {
        Parameter
        Type *string      `value:"rhapsody.db.type"`
        Database *string  `value:"rhapsody.db.database"`
        Username *string  `value:"rhapsody.db.username"`
        Password *string  `value:"rhapsody.db.password"`
}

type GormConfig struct {
        Configuration
        App *Application
}

func (g *GormConfig) GetDB(params *SqlParameter) *gorm.DB {
        db, err := gorm.Open(*params.Type, fmt.Sprintf("%s:%s@/%s", *params.Username, *params.Password, *params.Database))
        if err != nil {
                g.App.Logger.Error(//balabala)   
        }
        for _, value := range g.App.Entities {
                db.AutoMigrate(value.interface())
        }
        return db
}
复制代码
注:为什么配置文件字符串注入要用指针?
因为 spring-boot 这一套启动一次太慢了,需要分析依赖、加载、注入
所以 rhapsody 将支持配置文件热更新,用指针比较方便(什么,要完全热更?那还是等我研究一下 qlang 再看看吧)

这里有个 *Application 是什么呢?

其实就是 CreateApplication 函数返回的值,全局容器的指针

整个应用程序的加载、装配都是在这个容器中完成的

你在任何一个有效的 Bean (标记了 Configuration / Service/ Repository / Component/ Controller / Middlware / Router / Parameter 的结构体)里面声明一个类型为 *Application 的 public 成员 ,全局容器都会被自动注入。

同理, 上面的 GetDB 无效的,因为 *gorm.DB 不是一个有效的 Bean

我们需要代理一下

type UserRepository struct {
        Repository
        Db *gorm.DB
}

func (g *GormConfig) GetDB(params *SqlParameter) *UserRepository {
        db, err := gorm.Open(*params.Type, fmt.Sprintf("%s:%s@/%s", *params.Username, *params.Password, *params.Database))
        if err != nil {
                g.App.Logger.Error(//balabala)   
        }
        for _, value := range g.App.Entities {
                db.AutoMigrate(value.interface())
        }
        return &UserRepository{ Db: db }
}
复制代码

到现在我们把基本使用都说完了,再探讨一个比较深层的问题,如何指定注入的 Bean?

比如我在 Config 中注册了两个 *gorm.DB

func (g *GormConfig) GetDB(params *SqlParameter) *UserRepository {
        db, err := gorm.Open(*params.Type, fmt.Sprintf("%s:%s@/%s", *params.Username, *params.Password, *params.Database))
        if err != nil {
                g.App.Logger.Error(//balabala)   
        }
        return &UserRepository{ Db: db }
}

func (g *GormConfig) GetAutoMigrateDB(params *SqlParameter) *UserRepository {
        db, err := gorm.Open(*params.Type, fmt.Sprintf("%s:%s@/%s", *params.Username, *params.Password, *params.Database))
        if err != nil {
                g.App.Logger.Error(//balabala)   
        }
        for _, value := range g.App.Entities {
                db.AutoMigrate(value.interface())
        }
        return &UserRepository{ Db: db }
}
复制代码

其实,Bean 都是有自己名字的

像上面工厂函数(方法)产出的 Bean 名字默认是 函数(方法)名,直接注册的 Bean 名是类的完全限定名。

当存在多个 Bean 时,被注入的 Field 需要制定 Bean 的名字,如

type UserController struct {
	Controller 	       `prefix:"api/v1"`
	GET 		        `path:":id" method:"GetUserInfo"`
        db *UserRepository  `name:"GetAutoMigrateDB"`
}
复制代码

如果同时我们想要指定默认的 *UserRepository

我们需要用完全限定名

type UserController struct {
	Controller 	       `prefix:"api/v1"`
	GET 		       `path:":id" method:"GetUserInfo"`
        db *UserRepository     `name:"*rhapsody.UserRepository"`
}
复制代码

去指定它

我们也可以给它改名字

在 Config 中注册

type GormConfig struct {
        Configuration
        App *Application
        *UserRepository     `name:"*UserRepo"`
}
复制代码

之后就可以

type UserController struct {
	Controller 	       `prefix:"api/v1"`
	GET 		       `path:":id" method:"GetUserInfo"`
        db *UserRepository     `name:"*UserRepo"`
}
复制代码

深入:Bean 的分类

上文也提到过,Bean 分为

Configuration / Service/ Repository / Component/ Controller / Middlware / Router / Parameter

其中可以直接注册在 Root 里的有 Configuration, Controller, Router 和 Middleware

Controller 用于注册 路由组 和 handler

当 需要 prefix 或 Controller 比较多的时候可以在外层再套个 Router ,可以创建一个全局路由组

Middleware 用于注册 middleware ,可以指定 Controller 或 prefix

最特殊的就是 Configuration

Configuration 用于 注册 “Components”(Service/ Repository / Component)

工厂函数的注册,Bean 默认名注册,都在 Configuration 里面

Configuration 内的 Bean 称为 "PrimeBean"

容器一共会加载两次 Bean

第一次是 PrimeBean

第二次是 NormalBean

PrimeBean 的载入逻辑为

如过不存在已加载的同类型同名 Bean ,则载入; 如已存在,Crash

NormalBean 的载入逻辑为

如过不存在已加载的同类型 Bean ,则载入该类型的默认 Bean;若存在且只存在一个,跳过;若存在一个以上,则必须指定名称加载

-- 关于 AOP

短期没有考虑写 AOP 模块,大部分情况下 Middleware 就可以解决问题

AOP 的语法可以还是会用 spring 那一套,如果有更好的设计,欢迎提 pr / issue

--- 总结

项目刚刚起步,还未实现最小可用性

本文只是推广,并非正式文档(还没有正式文档)

如果对这个项目有兴趣,或者对整个项目设计有意见 / 建议

欢迎来 Github 提 pr / issue

下次 Commit 可能要等我考完期末考试(逃

分类:
后端
标签:
分类:
后端
标签: