beego源码分析——配置文件

关于配置,beego声明了如下的全局变量:

var (
    // BConfig is the default config for Application
    BConfig *Config
    // AppConfig is the instance of Config, store the config information from file
    AppConfig *beegoAppConfig
    // AppPath is the absolute path to the app
    AppPath string
    // GlobalSessions is the instance for the session manager
    GlobalSessions *session.Manager

    // appConfigPath is the path to the config files
    appConfigPath string
    // appConfigProvider is the provider for the config, default is ini
    appConfigProvider = "ini"
)

从上面的声明我们可以看到,beego的配置包括了BConfigAppConfig,分别表示beego自带的内部属性配置和应用的配置(即用户的自定义配置),那么接下去就是对这个两个变量的初始化。初始化函数在beego\config.go文件里面:

func init() {
    BConfig = newBConfig()
    var err error
    if AppPath, err = filepath.Abs(filepath.Dir(os.Args[0])); err != nil {
        panic(err)
    }
    workPath, err := os.Getwd()
    if err != nil {
        panic(err)
    }
    appConfigPath = filepath.Join(workPath, "conf", "app.conf")
    if !utils.FileExists(appConfigPath) {
        appConfigPath = filepath.Join(AppPath, "conf", "app.conf")
        if !utils.FileExists(appConfigPath) {
            AppConfig = &beegoAppConfig{innerConfig: config.NewFakeConfig()}
            return
        }
    }
    if err = parseConfig(appConfigPath); err != nil {
        panic(err)
    }
}

init()中首先调用newBConfig()对BConfig进行了初始化:

func newBConfig() *Config {
    return &Config{
    ...
        Listen: Listen{
        ...
        },
        WebConfig: WebConfig{
        ...
            Session: SessionConfig{
            ...
            },
        },
        Log: LogConfig{
        ...
        },
    }
}

可以看到该函数内对BConfig结构内的所有变量进行了初始化,也就是做默认配置,这些配置用户都可以自由调整。接着判断配置文件在可执行文件(即编译后的可执行文件)目录下是否存在,如果存在,那么就直接解析可执行文件目录下的配置文件;如果可执行文件目录下不存在配置文件,那么去工作目录下查找配置文件是否存在,如果在该目录下存在的话,那么解析该目录下的配置文件;如果这两个目录下的配置文件都不存在,那么init()中第15行会被执行,我们跟踪一下NewFakeConfig()函数:

// NewFakeConfig return a fake Congiger
func NewFakeConfig() Configer {
    return &fakeConfigContainer{
        data: make(map[string]string),
    }
}

这个函数返回了一个fakeConfigContainer类型的结构,但是init()函数中第15行beegoAppConfig结构中的变量innerConfig的定义如下:

type beegoAppConfig struct {
    innerConfig config.Configer
}

可以看到innerConfig是一个config.Configer类型的变量,那么我们在看下config.Configer是一个什么类型的结构:

// Configer defines how to get and set value from configuration raw data.
type Configer interface {
    Set(key, val string) error   //support section::key type in given key when using ini type.
    String(key string) string    //support section::key type in key string when using ini and json type; Int,Int64,Bool,Float,DIY are same.
    Strings(key string) []string //get string slice
    Int(key string) (int, error)
    Int64(key string) (int64, error)
    Bool(key string) (bool, error)
    Float(key string) (float64, error)
    DefaultString(key string, defaultVal string) string      // support section::key type in key string when using ini and json type; Int,Int64,Bool,Float,DIY are same.
    DefaultStrings(key string, defaultVal []string) []string //get string slice
    DefaultInt(key string, defaultVal int) int
    DefaultInt64(key string, defaultVal int64) int64
    DefaultBool(key string, defaultVal bool) bool
    DefaultFloat(key string, defaultVal float64) float64
    DIY(key string) (interface{}, error)
    GetSection(section string) (map[string]string, error)
    SaveConfigFile(filename string) error
}

可以看到config.Configer是一个接口,我们获取配置文件中的相关配置或是设置配置时都是调用这个接口内的相关函数来完成的。回到NewFakeConfig()这个函数,既然返回的是一个fakeConfigContainer类型的结构,那么fakeConfigContainer一定是一个Configer类型的接口变量,跟踪一下这个结构发现在beego\config\fake.go中,该结构实现了Configer这个接口的所有方法,那么这样返回就没问题了。

言归正传,现在假设存在配置文件,那么init()函数就会执行第19行,看一下parseConfig这个函数:

// now only support ini, next will support json.
func parseConfig(appConfigPath string) (err error) {
    AppConfig, err = newAppConfig(appConfigProvider, appConfigPath)
    if err != nil {
        return err
    }
    return assignConfig(AppConfig)
}

在这个函数内首先调用newAppConfig()这个函数来初始化AppConfig,那么跟踪newAppConfig()这个函数:

func newAppConfig(appConfigProvider, appConfigPath string) (*beegoAppConfig, error) {
    ac, err := config.NewConfig(appConfigProvider, appConfigPath)
    if err != nil {
        return nil, err
    }
    return &beegoAppConfig{ac}, nil
}

该函数调用了NewConfig来创建一个Configer类型的结构,因为该结构就是AppConfig中唯一的成员,只要构建好改成员就好了,那么继续看一下该函数内创建Configer接口变量的函数NewConfig

func NewConfig(adapterName, filename string) (Configer, error) {
    adapter, ok := adapters[adapterName]
    if !ok {
        return nil, fmt.Errorf("config: unknown adaptername %q (forgotten import?)", adapterName)
    }
    return adapter.Parse(filename)
}

那么该函数内的adapters又是什么?可以找到该变量的声明在beego\config\config.go下:

var adapters = make(map[string]Config)

这是map类型一个全局变量,键是字符串类型,值是Config类型的结构,那么看一下Config的定义:

// Config is the adapter interface for parsing config file to get raw data to Configer.
type Config interface {
    Parse(key string) (Configer, error)
    ParseData(data []byte) (Configer, error)
}

这也是一个接口,从源码的注释我们就可以得知这个结构内的方法就是用来解析配置文件来获取原始数据的。那么,adapters在使用前肯定被初始化过,在beego\config\config.go中看到了adapters的初始化:

func Register(name string, adapter Config) {
    if adapter == nil {
        panic("config: Register adapter is nil")
    }
    if _, ok := adapters[name]; ok {
        panic("config: Register called twice for adapter " + name)
    }
    adapters[name] = adapter
}

那么Register()函数肯定在什么地方被调用过,查找Register在beego\config\ini.go和beego\config\json.go中发现被调用,看一下ini.go中的初始化:

func init() {
    Register("ini", &IniConfig{})
}

从这个函数就可以看出来,这里注册的是ini类型的配置文件的解析方式。IniConfig是一个Config类型的接口变量,因为在ini.go文件内,该接口实现了Config接口的所有方法;同理json.go就是注册了json类型的配置文件的解释方式。从最前面的全局变量appConfigProvider = "ini"可以得知系统初始化的时候默认配置文件的格式是ini类型。

现在回到NewConfig中,第6行:

adapter.Parse(filename)

这个语句中的adapter是由

adapter, ok := adapters[adapterName]

这个语句来获取的,之前说过beego默认是以ini的方式来解析配置文件,所以这里的adapterName就是字符串ini,那么adapter就是IniConfig类型的变量,所以现在就使用ini的方式去解析配置文件,所以adapter.Parse(filename)中的Parse函数就是ini.go中实现的那个Parse函数,那么看一下该文件内的实现:

// Parse creates a new Config and parses the file configuration from the named file.
func (ini *IniConfig) Parse(name string) (Configer, error) {
    return ini.parseFile(name)
}

Parse函数调用了parseFile:

func (ini *IniConfig) parseFile(name string) (*IniConfigContainer, error) {
    data, err := ioutil.ReadFile(name)
    if err != nil {
        return nil, err
    }

    return ini.parseData(filepath.Dir(name), data)
}

parseFile调用了parseData

func (ini *IniConfig) parseData(dir string, data []byte) (*IniConfigContainer, error) {
    ......
}

从源码来看,parseData函数就是最底层的配置文件解析函数,这里不深究如何解析。

这里再看下Parse的返回值的类型:Configer,该类型正是变量AppConfig的那个唯一成员的类型。前面已经讨论过Configer是一个接口类型,而parseDataparseFile的返回值类型都是IniConfigContainer类型的,那么我们看一下IniConfigContainer的定义:

type IniConfigContainer struct {
    data           map[string]map[string]string // section=> key:val
    sectionComment map[string]string            // section : comment
    keyComment     map[string]string            // id: []{comment, key...}; id 1 is for main comment.
    sync.RWMutex
}

可以清楚的看到这里面定义的是几个map类型的成员,这几个变量就是用来存放配置文件内的配置的。而且可以肯定的是使用IniConfigContainer声明的变量必须是Configer类型接口的变量,所以用IniConfigContainer肯定实现了接口是Configer,这一点从ini.go文件内可以得到证明。

现在已经达到最底层,我们一步一步往回退,
Parse返回到NewConfig:返回一个Configer类型的变量,该变量存储着所有配置信息,并且有相应的方法获取或是设置配置。
NewConfig返回到newAppConfigNewConfig把得到的Configer类型变量返回给newAppConfig,在newAppConfig中,使用返回回来的Configer类型的变量创建了一个beegoAppConfig变量。
newAppConfig返回到parseConfignewAppConfig中创建的beegoAppConfig类型将被返回回来,并且赋值给全局变量APPConfig,终于APPConfig被初始化完毕。

parseConfig函数内还有最后一步:

assignConfig(AppConfig)

从字面上理解这个函数就是分配配置的意思,通过分析assignConfigassignSingleConfig就可以发现,这一步其实做的是重新给前面BConfig的部分变量设置值,因为某些变量可能在配置文件内用户做了改变,比如:AppName在BConfig初始化是被默认设置为”beego”,但是改变了一般都会设置为项目名,所以就有必要在真正获取到该变量后改变BConfig中的值。

最后,从parseConfig函数返回到init中,配置初始化就此结束。

还有一点需要说明的是,在之前的项目中也遇到的一个问题:生成的可执行文件所在目录。
在Go语言中有三种生成可执行文件的方式:go build, go install, go get。如果要是使用IntelliJ或是Goglang这样的IDE的话,还可以直接点击运行按钮就可以。
go build:使用这种方式生成可执行文件的话,必须cd到该项目main函数所在文件的目录,注意这里main函数所在文件不一定非得以main.go命名。这样生成的可执行文件所在的目录就是main函数所在文件的目录。一般main函数所在的文件会放到项目的根目录,那么可执行文件就在项目的根目录。你是否还记得前面的在判断配置文件所在目录的时候进行了两层判断,一层是可执行文件所在目录配置文件存在性的判断,下一层就是工作目录配置问文件存在性的判断。针对于go build方式生成可执行文件的话,第一层和第二层判断都是在同一个目录。
go install:使用这种方式生成的可执行文件会被放到GOPATHbin目录下。这种方式在判断配置文件所在目录的时候,第一层判断就会失败,所以会在第二层判断中找到。
go get:这种方式首先会在GOPATH目录下寻找需要被安装的包,如果在GOPATH\src目录下能够找到这样的包,那么就直接调用go install把可执行文件安装到GOPATHbin目录下。如果需要被安装的包不存在那么go get命令会调用相应的工具从远程获取package,如果package在github那么,go get就会调用git clone把源代码下载到GOPATH\src目录下,那么系统中就必须存在git工具且在可执行文件必须在PATH中。然后使用go install生成可执行文件。这样判断判断配置文件存在性也是在第二层才能成功。
最后就是使用IDE点击按钮运行的情况,在Windows中,以这种方式生成的可执行文件会被放到一个临时文件夹下:

这样在判断配置文件的存在性时就会在第二层的时候成功。

那么在加入在某个目录下运行go test的时候需要配置文件的读取,这时候我们测试一下可执行文件目录,工作目录分别在什么地方:

从图中可以看到,可执行文件目录是之前提到过的一个临时目录,工作目录变成了go test运行所在的目录,那么根据之前那两层判断都无法找到配置文件。那么要是在本地测试的话,我的解决办法就是把配置文件拷贝到go test运行所在的目录。

你可能感兴趣的:(源码)