前一段时间用 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 可能要等我考完期末考试(逃