控制反转
简述
控制反转是一种设计思想,适用于任何编程语言。包含了服务定位器(Service Locator),事件(Events),委托(Delegates)和依赖注入(Dependency Injection)。到目前为止,Hiboot实现了依赖注入部分,所以我们主要来讲解依赖注入部分。如何编写出松耦合,易维护的应用是Hiboot框架的设计初衷。根据这个设计思想,我们不应该在结构体,函数或方法中静态的配置其依赖,而应该将依赖配置在外部。
依赖注入设计模式允许我们解除应用组件间的强依赖关系,是应用程序松耦合,可扩展和可维护。
Go语言的一个结构体A依赖于另外一个结构体B,如果A用到了B的实例,我们说A依赖B。例如,userController 依赖 userService.
type userController struct {
at.RestController
userService service.UserService
}
func newUserController(userService service.UserService) *userController {
return &userController{
userService: userService,
}
}
理想的状况是一个Go语言的结构体应该尽量独立以其它结构体。这样可增加结构体的复用性以及可测试性。
依赖注入
和前面章节一样,我们还是通过示例来说明Hiboot的依赖注入是如何工作的。
⚠️ 注意:
- 如果你想要注入当是一个实例,依赖注入定义的相关字段或参数,其数据类型必须是结构体指针或interface{}. 如果你尝试定义其它类型将不会成功。
- 依赖注入实例的来源主要有两个,一个是通过app.Register()注册结构体构造器,另外一个是通过自动配置的方法注册。
正例:
type Foo struct {
}
func newFoo() *Foo {
return &Foo{}
}
type Bar struct {
foo *Foo
}
func newBar(foo *Foo) *Bar {
return &Bar{
foo: foo,
}
}
func init() {
app.Register(newFoo, newBar)
}
反例:
在这个反例中,foo
不是一个指针,将不会被注入。
type Foo struct {
}
func newFoo() *Foo {
return &Foo{}
}
type Bar struct {
foo Foo
}
func newBar(foo Foo) *Bar {
return &Bar{
foo: foo,
}
}
func init() {
app.Register(newFoo, newBar)
}
构造器注入
Hiboot建议使用构造器来注入,因为构造器注入能够保证注入的组件不可变,并且确保需要的依赖不为空。并且,构造器注入的依赖总数能够保证组件完全初始化的状态。
- 使用构造器注入的字段可以是私有的,不像字段注入,Go语言要求使用标签时,字段必须大写开头,这样就暴露了该字段,这就有被更改的风险。
通过构造器注入Hiboot可以保证依赖不会为空。
package main import ( "hidevops.io/hiboot/pkg/app/web" "hidevops.io/hiboot/pkg/model" "hidevops.io/hiboot/pkg/starter/jwt" "time" ) // This example shows that jwtToken is injected through the constructor, // once you imported "hidevops.io/hiboot/pkg/starter/jwt", // jwtToken jwt.Token will be injectable. func main() { // the web application entry web.NewApplication().Run() } // PATH: /login type loginController struct { at.RestController token jwt.Token } type userRequest struct { // embedded field model.RequestBody mark that userRequest is request body model.RequestBody Username string `json:"username" validate:"required"` Password string `json:"password" validate:"required"` } func init() { // Register Rest Controller through constructor newLoginController app.Register(newLoginController) } // newLoginController inject jwtToken through the argument jwtToken jwt.Token on constructor // the dependency jwtToken is auto configured in jwt starter, see https://hidevops.io/hiboot/pkg/starter/jwt func newLoginController(token jwt.Token) *loginController { return &loginController{ token: token, } } // Post / // The first word of method is the http method POST, the rest is the context mapping func (c *loginController) Post(request *userRequest) (response model.Response, err error) { jwtToken, _ := c.token.Generate(jwt.Map{ "username": request.Username, "password": request.Password, }, 30, time.Minute) response = new(model.BaseResponse) response.SetData(jwtToken) return }
字段注入
我们来将上面这段代码做个些的改动(⚠️ 我们将 token 改成了 Token, 这是因为Go语言定义了标签的字段必须是大写字母开头的,也就是对外公开),不用构造器也是可以注入的。Hiboot通过标签注入实例到字段 Token
中, 尽管使用的效果和构造器差不多,但是我们还是建议使用构造器来注入,原因在上面有说明。
package main
import (
"hidevops.io/hiboot/pkg/app/web"
"hidevops.io/hiboot/pkg/model"
"hidevops.io/hiboot/pkg/starter/jwt"
"time"
)
// This example shows that jwtToken is injected through the constructor,
// once you imported "hidevops.io/hiboot/pkg/starter/jwt",
// jwtToken jwt.Token will be injectable.
func main() {
// the web application entry
web.NewApplication().Run()
}
// PATH: /login
type loginController struct {
at.RestController
// Inject Token by tag `inject:""`
Token jwt.Token `inject:""`
}
type userRequest struct {
// embedded field model.RequestBody mark that userRequest is request body
model.RequestBody
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
}
func init() {
// Register Rest Controller through constructor newLoginController
app.Register(newLoginController)
}
// newLoginController inject jwtToken through the argument jwtToken jwt.Token on constructor
// the dependency jwtToken is auto configured in jwt starter, see https://hidevops.io/hiboot/pkg/starter/jwt
func newLoginController() *loginController {
return &loginController{}
}
// Post /
// The first word of method is the http method POST, the rest is the context mapping
func (c *loginController) Post(request *userRequest) (response model.Response, err error) {
// c.Token 是通过 inject 标签注入的,可以直接使用
jwtToken, _ := c.Token.Generate(jwt.Map{
"username": request.Username,
"password": request.Password,
}, 30, time.Minute)
response = new(model.BaseResponse)
response.SetData(jwtToken)
return
}
Hiboot字段注入是通过标签`inject:“”`来实现到。当在某个字段标注了`inject:“”`,Hiboot会尝试找到相应的实例并且注入到字段中,如果你尝试让Hiboot注入一个不存在的依赖项,Hiboot会在应用启动时报告错误。
如果需要注入的依赖有多个实现,则可以指定名称,比如`inject:“buzService”`, Hiboot会先寻找标签当名字,如果该实例不存在则会寻找是否有以该字段名命名当实例,最后才寻找以该字段数据类型命名当实例。
type BxzService interface {
GetNickname() string
}
type bazService struct {
Name string
nickname string
}
func (s *bazService) GetNickname() string {
return s.nickname
}
type buzService struct {
Name string
nickname string
}
func (s *buzService) GetNickname() string {
return s.nickname
}
type bxzServiceImpl struct {
BxzService
Name string
nickname string
}
func (s *bxzServiceImpl) GetNickname() string {
return s.nickname
}
type dependencyInjectionTestService struct {
BxzSvc BxzService `inject:""`
BazService BxzService `inject:""`
BuzService BxzService `inject:""`
BozService BxzService `inject:"buzService"`
name string
}
方法注入
方法注入是Hiboot的一大特点,下面是Hiboot starter jwt的实际代码。在方法Middleware()
中依赖了Token
,Hiboot会自动将Token
实例注入到Middleware()
方法的参数jwtToken Token
。
关于Hiboot starter会在后面章节详细讲解。
// Package jwt provides the hiboot starter for injectable jwt dependency
package jwt
import (
"github.com/dgrijalva/jwt-go"
"hidevops.io/hiboot/pkg/app"
mw "github.com/iris-contrib/middleware/jwt"
)
type configuration struct {
app.Configuration
Properties Properties `mapstructure:"jwt"`
middleware *Middleware
token Token
}
func init() {
app.Register(newConfiguration)
}
func newConfiguration() *configuration {
return &configuration{}
}
// Middleware is the jwt handler
func (c *configuration) Middleware(jwtToken Token) *Middleware {
return NewJwtMiddleware(mw.Config{
ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
//log.Debug(token)
return jwtToken.VerifyKey(), nil
},
// When set, the middleware verifies that tokens are signed with the specific signing algorithm
// If the signing method is not constant the ValidationKeyGetter callback can be used to implement additional checks
// Important to avoid security issues described here: https://auth0.com/blog/2015/03/31/critical-vulnerabilities-in-json-web-token-libraries/
SigningMethod: jwt.SigningMethodRS256,
})
}
// Token
func (c *configuration) Token() Token {
t := new(jwtToken)
t.Initialize(&c.Properties)
return t
}