597 lines
24 KiB
Markdown
597 lines
24 KiB
Markdown
|
# 16|配置和环境(下):配置服务中的设计思路
|
|||
|
|
|||
|
你好,我是轩脉刃。
|
|||
|
|
|||
|
上一节课,我们已经定义好了配置文件服务的接口,这节课就来实现这些接口。先来规划配置文件服务目录,按照上一节课分析的,多个配置文件按类别放在不同配置文件夹中,在框架文件夹中,我们将配置文件接口代码写在框架文件夹下的contract/config.go文件中,将具体实现放在provider/config/目录中。
|
|||
|
|
|||
|
## 配置服务的设计
|
|||
|
|
|||
|
不过设计优于实现,动手之前我们先思考下实现这个接口要如何设计。
|
|||
|
|
|||
|
首先,要读取一下配置文件夹中的文件。上节课说了,最终的配置文件夹地址为,应用服务的 ConfigFolder 下的环境变量对应的文件夹,比如 ConfigFolder/development。但是还有一个问题,就是配置文件的格式的选择。
|
|||
|
|
|||
|
**目前市面上的配置文件格式非常多,但是很难说哪种配置文件比较好,完全是不同平台、不同时代下的产物**。比如Windows开发的配置常用INI、Java开发配置常用Properties,我这里选择了使用YAML格式。
|
|||
|
|
|||
|
### 配置文件的读取
|
|||
|
|
|||
|
YAML格式是在Golang的项目中比较通用的一种格式,比如Kubernetes、Docker、Swagger等项目,都是使用YAML作为其配置文件的。YAML配置文件除了能表达基础类型比如string、int、float 之外,也能表达复杂的数组、结构等数据类型。
|
|||
|
|
|||
|
目前最新的YAML版本为1.2版本,配置的说明文档在[官网](https://yaml.org/)上。它提供多种语言的解析库,其中[go-yaml](https://github.com/go-yaml/yaml) 就是非常通用的一个Go解析库,这个库的封装性非常好。
|
|||
|
|
|||
|
我们通过第一节课讲的快速阅读一个库的命令 `go doc github.com/go-yaml/yaml |grep '^func'`,可以看出来这个库对外提供的方法非常明确,一共三个方法:
|
|||
|
|
|||
|
* Marshal 表示序列化一个结构成为YAML格式;
|
|||
|
* Unmarshal表示反序列化一个YAML格式文本成为一个结构;
|
|||
|
* 还有一个UnmarshalStrict 函数,表示严格反序列化,比如如果YAML格式文件中包含重复key的字段,那么使用UnmarshalStrict 函数反序列化会出现错误。
|
|||
|
|
|||
|
```plain
|
|||
|
// 序列化
|
|||
|
func Marshal(in interface{}) (out []byte, err error)
|
|||
|
// 反序列化
|
|||
|
func Unmarshal(in []byte, out interface{}) (err error)
|
|||
|
// 严格反序列化
|
|||
|
func UnmarshalStrict(in []byte, out interface{}) (err error)
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
我们选择Unmarshal的函数进行反序列化,因为这样能提高框架对配置文件的容错性和易用性。好,读取配置文件的格式和对应工具搞定,下一步就是想清楚怎么替换了。
|
|||
|
|
|||
|
### 配置文件的替换
|
|||
|
|
|||
|
在上一节课说的环境变量服务中,存放了包括.env中设置的环境变量,那么我们自然会希望使用上这些环境变量,把配置文件中有的字段使用环境变量替换掉。那么这里在配置文件中就需要有一个“占位符”。这个占位符表示当前这个字段去环境变量中进行阅读。
|
|||
|
|
|||
|
这个占位符的设计只有一个要求:够特别。只要这个占位符能和其他配置文件字符区分开就行,所以这里设计占位符为比较有语义的“env(XXXX)”。比如app/config/development/database.yaml 文件中的数据库密码,使用占位符表示如下:
|
|||
|
|
|||
|
```yaml
|
|||
|
mysql:
|
|||
|
hostname: 127.0.0.1
|
|||
|
username: yejianfeng
|
|||
|
password: env(DB_PASSWORD)
|
|||
|
timeout: 1
|
|||
|
readtime: 2.3
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
要实现这个功能,其实也很简单,可以在读取YAML配置文件内容之后,进行完整的文本匹配,将所有环境变量env(xxx) 的字符替换为环境变量。我们应该能设计出替换文本的函数。
|
|||
|
|
|||
|
在框架目录的provider/config/service.go中,可以先实现这个方法。
|
|||
|
|
|||
|
```go
|
|||
|
// replace 表示使用环境变量maps替换context中的env(xxx)的环境变量
|
|||
|
func replace(content []byte, maps map[string]string) []byte {
|
|||
|
if maps == nil {
|
|||
|
return content
|
|||
|
}
|
|||
|
// 直接使用ReplaceAll替换。这个性能可能不是最优,但是配置文件加载,频率是比较低的,可以接受
|
|||
|
for key, val := range maps {
|
|||
|
reKey := "env(" + key + ")"
|
|||
|
content = bytes.ReplaceAll(content, []byte(reKey), []byte(val))
|
|||
|
}
|
|||
|
return content
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
### 配置项的解析
|
|||
|
|
|||
|
读取并解析完配置文件内容,接下来就要根据path来解析某个配置项了。上一节课说,我们使用点号分割的路径读取方式,比如database.mysql.password 表示在配置文件夹中的database.yaml文件,其中的mysql配置,对应的是数据结构中的password字段。
|
|||
|
|
|||
|
那这种根据path来读取字段应该怎么实现呢?
|
|||
|
|
|||
|
在获取配置项的时候,我们已经通过go-yaml库将配置文件解析到一个map数据结构中了,而这个map数据结构的子项,明显也有可能是一个map数据结构。所以按照path路径查找,这明显应该是一个**函数递归逻辑**。
|
|||
|
|
|||
|
还是用刚才的database.mysql.password举例,可以拆分为3个结构。database 去根map中寻找;如果有这个key,就拿着mysql.password的path,去 database这个key对应的value中进行寻找;而递归寻找到了最后一级path为password,发现这个path没有下一级了,就停止递归。
|
|||
|
|
|||
|
详细的代码方法如下,同样存放在框架目录的provider/config/service.go中。
|
|||
|
|
|||
|
```go
|
|||
|
// 查找某个路径的配置项
|
|||
|
func searchMap(source map[string]interface{}, path []string) interface{} {
|
|||
|
if len(path) == 0 {
|
|||
|
return source
|
|||
|
}
|
|||
|
|
|||
|
// 判断是否有下个路径
|
|||
|
next, ok := source[path[0]]
|
|||
|
if ok {
|
|||
|
// 判断这个路径是否为1
|
|||
|
if len(path) == 1 {
|
|||
|
return next
|
|||
|
}
|
|||
|
|
|||
|
// 判断下一个路径的类型
|
|||
|
switch next.(type) {
|
|||
|
case map[interface{}]interface{}:
|
|||
|
// 如果是interface的map,使用cast进行下value转换
|
|||
|
return searchMap(cast.ToStringMap(next), path[1:])
|
|||
|
case map[string]interface{}:
|
|||
|
// 如果是map[string],直接循环调用
|
|||
|
return searchMap(next.(map[string]interface{}), path[1:])
|
|||
|
default:
|
|||
|
// 否则的话,返回nil
|
|||
|
return nil
|
|||
|
}
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
// 通过path获取某个元素
|
|||
|
func (conf *HadeConfig) find(key string) interface{} {
|
|||
|
...
|
|||
|
return searchMap(conf.confMaps, strings.Split(key, conf.keyDelim))
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
想通了以上三个核心实现难点,我们就可以着手整体代码实现了。
|
|||
|
|
|||
|
## 配置服务的代码实现
|
|||
|
|
|||
|
首先,在框架文件夹的provider/config/service.go 中,创建一个配置文件服务HadeConfig。它有几个属性:folder代表配置本地配置文件所在的文件夹;keyDelim代表路径中的分割符号,也就是点;envMaps存放所有的环境变量;而confMaps存放每个配置解析后的结构,confRaws存放每个配置的原始文件信息。
|
|||
|
|
|||
|
```go
|
|||
|
// HadeConfig 表示hade框架的配置文件服务
|
|||
|
type HadeConfig struct {
|
|||
|
c framework.Container // 容器
|
|||
|
folder string // 文件夹
|
|||
|
keyDelim string // 路径的分隔符,默认为点
|
|||
|
...
|
|||
|
envMaps map[string]string // 所有的环境变量
|
|||
|
confMaps map[string]interface{} // 配置文件结构,key为文件名
|
|||
|
confRaws map[string][]byte // 配置文件的原始信息
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
我们初始化这个HadeConfig的函数,它从服务提供者provider/config/provider.go中获取到三个参数,除了容器之外,另外两个是文件夹地址和所有的环境变量。
|
|||
|
|
|||
|
我们这里对provider.go 只列一下参数函数,其他的四个服务提供者函数(Register、Boot、IsDefer、Name) 可以参考[GitHub上的代码](https://github.com/gohade/coredemo/blob/geekbang/16/framework/provider/config/provider.go)。
|
|||
|
|
|||
|
```go
|
|||
|
// Paramas 服务提供者实例化的时候参数
|
|||
|
func (provider *HadeConfigProvider) Params(c framework.Container) []interface{} {
|
|||
|
appService := c.MustMake(contract.AppKey).(contract.App)
|
|||
|
envService := c.MustMake(contract.EnvKey).(contract.Env)
|
|||
|
env := envService.AppEnv()
|
|||
|
// 配置文件夹地址
|
|||
|
configFolder := appService.ConfigFolder()
|
|||
|
envFolder := filepath.Join(configFolder, env)
|
|||
|
// 传递容器,配置文件夹地址,所有环境变量
|
|||
|
return []interface{}{c, envFolder, envService.All()}
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
那么在provider/config/service.go中,实例化的函数逻辑如下:
|
|||
|
|
|||
|
```go
|
|||
|
// NewHadeConfig 初始化Config方法
|
|||
|
func NewHadeConfig(params ...interface{}) (interface{}, error) {
|
|||
|
container := params[0].(framework.Container)
|
|||
|
envFolder := params[1].(string)
|
|||
|
envMaps := params[2].(map[string]string)
|
|||
|
|
|||
|
// 检查文件夹是否存在
|
|||
|
if _, err := os.Stat(envFolder); os.IsNotExist(err) {
|
|||
|
return nil, errors.New("folder " + envFolder + " not exist: " + err.Error())
|
|||
|
}
|
|||
|
// 实例化
|
|||
|
hadeConf := &HadeConfig{
|
|||
|
c: container,
|
|||
|
folder: envFolder,
|
|||
|
envMaps: envMaps,
|
|||
|
confMaps: map[string]interface{}{},
|
|||
|
confRaws: map[string][]byte{},
|
|||
|
keyDelim: ".",
|
|||
|
lock: sync.RWMutex{},
|
|||
|
}
|
|||
|
// 读取每个文件
|
|||
|
files, err := ioutil.ReadDir(envFolder)
|
|||
|
if err != nil {
|
|||
|
return nil, errors.WithStack(err)
|
|||
|
}
|
|||
|
for _, file := range files {
|
|||
|
fileName := file.Name()
|
|||
|
err := hadeConf.loadConfigFile(envFolder, fileName)
|
|||
|
if err != nil {
|
|||
|
log.Println(err)
|
|||
|
continue
|
|||
|
}
|
|||
|
}
|
|||
|
...
|
|||
|
return hadeConf, nil
|
|||
|
}
|
|||
|
|
|||
|
// 读取某个配置文件
|
|||
|
func (conf *HadeConfig) loadConfigFile(folder string, file string) error {
|
|||
|
conf.lock.Lock()
|
|||
|
defer conf.lock.Unlock()
|
|||
|
// 判断文件是否以yaml或者yml作为后缀
|
|||
|
s := strings.Split(file, ".")
|
|||
|
if len(s) == 2 && (s[1] == "yaml" || s[1] == "yml") {
|
|||
|
name := s[0]
|
|||
|
// 读取文件内容
|
|||
|
bf, err := ioutil.ReadFile(filepath.Join(folder, file))
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
// 直接针对文本做环境变量的替换
|
|||
|
bf = replace(bf, conf.envMaps)
|
|||
|
// 解析对应的文件
|
|||
|
c := map[string]interface{}{}
|
|||
|
if err := yaml.Unmarshal(bf, &c); err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
conf.confMaps[name] = c
|
|||
|
conf.confRaws[name] = bf
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
逻辑非常清晰。先检查配置文件夹是否存在,然后读取文件夹中的每个以yaml或者yml后缀的文件;读取之后,先用replace对环境变量进行一次替换;替换之后使用 go-yaml,对文件进行解析。
|
|||
|
|
|||
|
初始化实例就是一个完整的 解析文件的过程,解析结束之后,confMaps里存放的就是解析之后的结果。
|
|||
|
|
|||
|
配置文件的获取接口上节课已经写好了,定义了接口的系列方法,这里我们就详细实现Get/GetBool/GetInt,其他方法大同小异,就不贴出来了,你可以直接参考[GitHub上的代码](https://github.com/gohade/coredemo/blob/geekbang/16/framework/provider/config/service.go)。
|
|||
|
|
|||
|
前面已经想好了,用方法find,通过path,从一个嵌套map confMaps中获取数据。所以Get方法就是调用一下find方法而已,同样也在service.go中:
|
|||
|
|
|||
|
```go
|
|||
|
// Get 获取某个配置项
|
|||
|
func (conf *HadeConfig) Get(key string) interface{} {
|
|||
|
return conf.find(key)
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
而对应的Get系列的方法我们使用cast库进行类型转换,比如:
|
|||
|
|
|||
|
```go
|
|||
|
// GetBool 获取bool类型配置
|
|||
|
func (conf *HadeConfig) GetBool(key string) bool {
|
|||
|
return cast.ToBool(conf.find(key))
|
|||
|
}
|
|||
|
// GetInt 获取int类型配置
|
|||
|
func (conf *HadeConfig) GetInt(key string) int {
|
|||
|
return cast.ToInt(conf.find(key))
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
到这里,配置服务的代码已经基本成型了。但是实际上还有两个细节我们需要认真思考。
|
|||
|
|
|||
|
首先,因为之前我们设置过App服务,将一个App服务的目录都安排好了,但是如果之后有需求要改变这些目录的配置呢?如果有的话,是否可以通过配置来进行修改呢?所以第一个问题就是,我们要思考配置文件更新App服务的操作。
|
|||
|
|
|||
|
其次,假设现在配置服务能从文件中获取配置了,但是如果文件修改了,我们是否需要重新启动应用呢?是否有能不启动应用的方法呢?
|
|||
|
|
|||
|
下面我们来一一解决这两个问题。
|
|||
|
|
|||
|
## 配置文件更新App服务
|
|||
|
|
|||
|
现在有了配置文件服务,但在没有配置文件服务之前,我们启动服务的appService,也是有可能要修改这个服务的配置的。回忆[第十二](https://time.geekbang.org/column/article/423982)[课](https://time.geekbang.org/column/article/423982),appService中存放了启动这个业务实例默认设置的文件夹目录和地址。
|
|||
|
|
|||
|
```go
|
|||
|
//BaseFolder 定义项目基础地址
|
|||
|
BaseFolder() string
|
|||
|
// ConfigFolder 定义了配置文件的路径
|
|||
|
ConfigFolder() string
|
|||
|
// LogFolder 定义了日志所在路径
|
|||
|
LogFolder() string
|
|||
|
// ProviderFolder 定义业务自己的服务提供者地址
|
|||
|
ProviderFolder() string
|
|||
|
// MiddlewareFolder 定义业务自己定义的中间件
|
|||
|
MiddlewareFolder() string
|
|||
|
// CommandFolder 定义业务定义的命令
|
|||
|
CommandFolder() string
|
|||
|
// RuntimeFolder 定义业务的运行中间态信息
|
|||
|
RuntimeFolder() string
|
|||
|
// TestFolder 存放测试所需要的信息
|
|||
|
TestFolder() string
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
现在有需求将这些文件夹目录,在配置文件中进行配置并修改。所以应该在加载到配置服务时,再更新下appService。加载逻辑如下:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/f5/ee/f5141333501ce140314fb985b75c6eee.jpg?wh=1920x1080)
|
|||
|
|
|||
|
可以把设定App的这些配置文件,存放在配置文件夹的app.yaml文件的path设置项下,其中每个配置项的key,对应appService中每个对应的服务。比如log\_folder对应LogFolder目录:
|
|||
|
|
|||
|
```go
|
|||
|
path:
|
|||
|
log_folder: "/home/jianfengye/hade/log/"
|
|||
|
runtime_folder: "/home/jianfengye/hade/runtime/"
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
现在加载配置服务的时候,当读取到配置服务app.path下有内容,就需要更新appService的配置。首先需要修改appService,修改框架目录下的provider/app/service.go文件。
|
|||
|
|
|||
|
将HadeApp增加一个configMap字段:
|
|||
|
|
|||
|
```go
|
|||
|
// HadeApp 代表hade框架的App实现
|
|||
|
type HadeApp struct {
|
|||
|
...
|
|||
|
configMap map[string]string // 配置加载
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
同时为HadeApp增加LoadAppConfig方法,用于读取配置文件中的信息:
|
|||
|
|
|||
|
```go
|
|||
|
// LoadAppConfig 加载配置map
|
|||
|
func (app *HadeApp) LoadAppConfig(kv map[string]string) {
|
|||
|
for key, val := range kv {
|
|||
|
app.configMap[key] = val
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
再修改对应的LogFolder等一系列XXXFolder的方法,先读取configMap中的值,如果有的话,先用configMap中的值:
|
|||
|
|
|||
|
```go
|
|||
|
// LogFolder 表示日志存放地址
|
|||
|
func (app HadeApp) LogFolder() string {
|
|||
|
if val, ok := app.configMap["log_folder"]; ok {
|
|||
|
return val
|
|||
|
}
|
|||
|
return filepath.Join(app.StorageFolder(), "log")
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
这样,对appService的修改就完成了。
|
|||
|
|
|||
|
在configService,读取配置文件loadConfigFile的时候,要注意,如果当前的配置文件是app.yaml, 我们需要调用appService的LoadAppConfig方法:
|
|||
|
|
|||
|
```go
|
|||
|
// 读取某个配置文件
|
|||
|
func (conf *HadeConfig) loadConfigFile(folder string, file string) error {
|
|||
|
...
|
|||
|
|
|||
|
// 判断文件是否以yaml或者yml作为后缀
|
|||
|
s := strings.Split(file, ".")
|
|||
|
if len(s) == 2 && (s[1] == "yaml" || s[1] == "yml") {
|
|||
|
name := s[0]
|
|||
|
|
|||
|
...
|
|||
|
|
|||
|
// 读取app.path中的信息,更新app对应的folder
|
|||
|
if name == "app" && conf.c.IsBind(contract.AppKey) {
|
|||
|
if p, ok := c["path"]; ok {
|
|||
|
appService := conf.c.MustMake(contract.AppKey).(contract.App)
|
|||
|
appService.LoadAppConfig(cast.ToStringMapString(p))
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
这样在加载app.yaml的配置文件的时候,就同时更新了appService 里面的配置。
|
|||
|
|
|||
|
## 配置文件热更新
|
|||
|
|
|||
|
正常来说,在程序启动的时候会读取一次配置文件,但是在程序运行过程中,我们难免会遇到需要修改配置文件的操作。也就是之前思考的第二个问题。
|
|||
|
|
|||
|
这个时候,是否需要重新启动一次程序再加载一次配置文件呢?这当然是没有问题的,但是更为强大的是,**我们可以自动监控配置文件目录下的所有文件,当配置文件有修改和更新的时候,能自动更新程序中的配置文件信息,也就是实现配置文件热更新**。
|
|||
|
|
|||
|
这个热更新看起来很麻烦,其实在Golang中是非常简单的事情。我们使用 [fsnotify](https://github.com/fsnotify/fsnotify) 库能很方便对一个文件夹进行监控,当文件夹中有文件增/删/改的时候,会通过channel进行事件回调。
|
|||
|
|
|||
|
这个库的使用方式很简单。大致思路就是先使用NewWatcher创建一个监控器watcher,然后使用Add来监控某个文件夹,通过watcher设置的events来判断文件是否有变化,如果有变化,就进行对应的操作,比如更新内存中配置服务存储的map结构。
|
|||
|
|
|||
|
```go
|
|||
|
// NewHadeConfig 初始化Config方法
|
|||
|
func NewHadeConfig(params ...interface{}) (interface{}, error) {
|
|||
|
...
|
|||
|
|
|||
|
// 监控文件夹文件
|
|||
|
watch, err := fsnotify.NewWatcher()
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
err = watch.Add(envFolder)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
go func() {
|
|||
|
defer func() {
|
|||
|
if err := recover(); err != nil {
|
|||
|
fmt.Println(err)
|
|||
|
}
|
|||
|
}()
|
|||
|
|
|||
|
for {
|
|||
|
select {
|
|||
|
case ev := <-watch.Events:
|
|||
|
{
|
|||
|
//判断事件发生的类型,如下5种
|
|||
|
// Create 创建
|
|||
|
// Write 写入
|
|||
|
// Remove 删除
|
|||
|
path, _ := filepath.Abs(ev.Name)
|
|||
|
index := strings.LastIndex(path, string(os.PathSeparator))
|
|||
|
folder := path[:index]
|
|||
|
fileName := path[index+1:]
|
|||
|
|
|||
|
if ev.Op&fsnotify.Create == fsnotify.Create {
|
|||
|
log.Println("创建文件 : ", ev.Name)
|
|||
|
hadeConf.loadConfigFile(folder, fileName)
|
|||
|
}
|
|||
|
if ev.Op&fsnotify.Write == fsnotify.Write {
|
|||
|
log.Println("写入文件 : ", ev.Name)
|
|||
|
hadeConf.loadConfigFile(folder, fileName)
|
|||
|
}
|
|||
|
if ev.Op&fsnotify.Remove == fsnotify.Remove {
|
|||
|
log.Println("删除文件 : ", ev.Name)
|
|||
|
hadeConf.removeConfigFile(folder, fileName)
|
|||
|
}
|
|||
|
}
|
|||
|
case err := <-watch.Errors:
|
|||
|
{
|
|||
|
log.Println("error : ", err)
|
|||
|
return
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}()
|
|||
|
|
|||
|
return hadeConf, nil
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
代码如上,我们使用NewWatcher创建一个监听器,监听配置文件目录,接着启动一个新的Goroutine作为监听协程。在监听协程中,监听配置文件的创建、更新、删除操作。创建和更新对应 LoadConfigFile 操作。
|
|||
|
|
|||
|
而删除,对应的是 removeConfigFile操作,这个操作的内容就是删除配置服务中的confMaps中对应的key。
|
|||
|
|
|||
|
```go
|
|||
|
// 删除文件的操作
|
|||
|
func (conf *HadeConfig) removeConfigFile(folder string, file string) error {
|
|||
|
conf.lock.Lock()
|
|||
|
defer conf.lock.Unlock()
|
|||
|
s := strings.Split(file, ".")
|
|||
|
// 只有yaml或者yml后缀才执行
|
|||
|
if len(s) == 2 && (s[1] == "yaml" || s[1] == "yml") {
|
|||
|
name := s[0]
|
|||
|
// 删除内存中对应的key
|
|||
|
delete(conf.confRaws, name)
|
|||
|
delete(conf.confMaps, name)
|
|||
|
}
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
这里注意下,由于在运行时增加了对confMaps的写操作,所以需要对confMaps进行锁设置,以防止在写confMaps的时候,读操作进入读取了错误信息。
|
|||
|
|
|||
|
分析目前的这个场景,读明显多于写。所以我们的锁应该是一个读写锁,读写锁可以让多个读并发读,但是只要有一个写操作,读和写都需要等待。这个很符合当前这个场景。
|
|||
|
|
|||
|
所以在框架目录的provider/config/service.go中的HadeConfig,我们增加了一个读写锁lock。
|
|||
|
|
|||
|
```go
|
|||
|
// HadeConfig 表示hade框架的配置文件服务
|
|||
|
type HadeConfig struct {
|
|||
|
...
|
|||
|
lock sync.RWMutex // 配置文件读写锁
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
而在loadConfigFile和removeConfigFile这两个对配置有修改的情况,使用写锁锁住HadeConfig。
|
|||
|
|
|||
|
```go
|
|||
|
// 读取某个配置文件
|
|||
|
func (conf *HadeConfig) loadConfigFile(folder string, file string) error {
|
|||
|
conf.lock.Lock()
|
|||
|
defer conf.lock.Unlock()
|
|||
|
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
在Get系列方法调用的find函数中,使用读锁来进行读操作。
|
|||
|
|
|||
|
```go
|
|||
|
// 通过path来获取某个配置项
|
|||
|
func (conf *HadeConfig) find(key string) interface{} {
|
|||
|
conf.lock.RLock()
|
|||
|
defer conf.lock.RUnlock()
|
|||
|
...
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
这样,配置服务就开发完成了。
|
|||
|
|
|||
|
## 验证
|
|||
|
|
|||
|
我们先测试环境变量注入配置文件的功能。将业务目录下的config/development/database.yaml 中的mysql.password,使用环境变量进行替换。
|
|||
|
|
|||
|
```yaml
|
|||
|
mysql:
|
|||
|
hostname: 127.0.0.1
|
|||
|
username: yejianfeng
|
|||
|
password: env(DB_PASSWORD)
|
|||
|
timeout: 1
|
|||
|
readtime: 2.3
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
然后修改业务目录下的module/demo/api.go,替换其中/demo/demo对应的路由方法。
|
|||
|
|
|||
|
```go
|
|||
|
func (api *DemoApi) Demo(c *gin.Context) {
|
|||
|
// 获取password
|
|||
|
configService := c.MustMake(contract.ConfigKey).(contract.Config)
|
|||
|
password := configService.GetString("database.mysql.password")
|
|||
|
// 打印出来
|
|||
|
c.JSON(200, password)
|
|||
|
}
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
最后使用命令行 `./hade app start` 启动服务。打开浏览器,看到输出:
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/27/f1/275ddfb18f04549dd62e6b45fc3cccf1.png?wh=478x86)
|
|||
|
|
|||
|
说明此时还没注入环境变量。下面使用命令行:
|
|||
|
|
|||
|
```plain
|
|||
|
DB_PASSWORD=123 ./hade app start
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
启动服务。这个命令注入了DB\_PASSWORD这个环境变量。
|
|||
|
重启打开浏览器看到输出。
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/33/b6/33f14f5277ea8294458141d5393269b6.png?wh=405x90)
|
|||
|
|
|||
|
环境变量注入成功!
|
|||
|
|
|||
|
这个时候我们不停止进程,直接修改配置文件database.yaml中的mysql.password:
|
|||
|
|
|||
|
```yaml
|
|||
|
mysql:
|
|||
|
hostname: 127.0.0.1
|
|||
|
username: yejianfeng
|
|||
|
password: 456789
|
|||
|
timeout: 1
|
|||
|
readtime: 2.3
|
|||
|
|
|||
|
```
|
|||
|
|
|||
|
打开浏览器,输出已经变化了。
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/6c/b7/6c86bf1f4f055d94fb17c756415a94b7.png?wh=377x82)
|
|||
|
|
|||
|
说明热更新已经生效了,测试成功。
|
|||
|
|
|||
|
今天所有代码的目录结构截图,也贴在这里供你对比检查,代码放在GitHub上的 [16分支](https://github.com/gohade/coredemo/tree/geekbang/16) 里。
|
|||
|
|
|||
|
![图片](https://static001.geekbang.org/resource/image/6c/38/6c9b9046f41e4b5c0719fe205540c438.png?wh=351x1140)
|
|||
|
|
|||
|
## 小结
|
|||
|
|
|||
|
配置服务在框架中是一个非常基础且重要的服务。
|
|||
|
|
|||
|
我们考虑了整个配置服务的实现,先读取配置文件,再替换环境变量,最后再根据路径获取配置项,这样三步走完成了基本的配置服务。在配置服务的基础上,我们又补充了配置服务加载时对App服务的更新,并且为配置服务增加了热更新的机制。
|
|||
|
|
|||
|
我个人认为,配置服务是一个App中最常用到的服务了,有非常方便的配置服务接口,能为业务代码节省不少的代码量。**提供多种设置配置的方式,是真实从业务需求出发的**。
|
|||
|
|
|||
|
比如在实际工作中,有的需求要求数据库密码不能进入git库,必须通过环境变量获取,我们就可以通过环境变量获取配置;而有的需求要求在一个服务器上调试测试和预发布环境,我们可以通过.env切换不同环境。所以,有个多层次的环境配置机制,对于一个框架来说是非常必要的。
|
|||
|
|
|||
|
## 思考题
|
|||
|
|
|||
|
现在有配置文件服务了,但是根据路径、获取某个配置却只能在代码中获取。这里我们希望有一个命令行工具 `./hade config get "database.mysql"` 能获取到这个path路径对应的配置。你可以尝试实现么?
|
|||
|
|
|||
|
欢迎在留言区分享你的思考。感谢你的收听,如果觉得有收获,也欢迎把今天的内容分享给你身边的朋友,邀他一起学习。我们下节课见~
|
|||
|
|