mirror of
https://gitee.com/johng/gf
synced 2026-06-06 02:25:47 +08:00
# Loader 配置加载器
Loader 是一个通用的配置管理器,提供了类似于 Spring Boot
的`@ConfigurationProperties`的配置加载、监控、更新和管理功能。
## 功能特性
- **泛型支持**:使用 Go 泛型,类型安全的配置绑定
- **配置加载**:从配置源加载数据并绑定到结构体
- **配置监控**:自动监控配置变化并更新
- **自定义转换器**:支持自定义数据转换函数
- **回调处理**:配置变更时的回调函数
- **错误处理**:灵活的错误处理机制
## 安装
```bash
go get github.com/gogf/gf/v2
```
## 使用示例
### 1. 基本用法
#### 用法一
```go
package main
import (
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
)
type AppConfig struct {
Name string `json:"name"`
Age int `json:"age"`
Enabled bool `json:"enabled"`
Features []string `json:"features"`
Server ServerConfig `json:"server"`
}
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
}
func main() {
ctx := gctx.New()
// 创建配置器实例
loader := gcfg.NewLoader[AppConfig](g.Cfg("test"), "")
// 加载和监听配置
loader.MustLoadAndWatch(ctx, "test-watcher")
// 获取配置
config := loader.Get()
fmt.Println(config.Name)
}
```
#### 用法二
```go
package main
import (
"fmt"
"github.com/gogf/gf/v2/os/gcfg"
"github.com/gogf/gf/v2/os/gctx"
)
type AppConfig struct {
Name string `json:"name"`
Age int `json:"age"`
Enabled bool `json:"enabled"`
Features []string `json:"features"`
Server ServerConfig `json:"server"`
}
type ServerConfig struct {
Host string `json:"host"`
Port int `json:"port"`
}
func main() {
ctx := gctx.New()
// 使用单独的适配器创建
// 创建配置管理器
cfg, _ := gcfg.NewAdapterFile("test.yaml")
// 创建配置器实例
loader := gcfg.NewLoaderWithAdapter[AppConfig](cfg, "")
// 加载和监听配置
loader.MustLoadAndWatch(ctx, "test-watcher")
// 获取配置
config := loader.Get()
fmt.Println(config.Name)
}
```
### 2. 配置监控
```go
// 仅加载App配置
loader := gcfg.NewLoaderWithAdapter[AppConfig](cfg, "app")
// 设置配置变更回调
loader.OnChange(func (updated AppConfig) error {
// 配置变更时的处理逻辑
println("配置已更新:", updated.Name)
return nil
})
// 加载数据
err := loader.Load(ctx)
if err != nil {
panic(err)
}
// 开始监控配置变化
err := loader.Watch(context.Background(), "my-watcher")
if err != nil {
panic(err)
}
```
### 3. 自定义转换器
```go
// 设置自定义转换器
loader.SetConverter(func (data any, target *AppConfig) error {
// 自定义数据转换逻辑
return nil
})
```
### 4. 便捷方法
```go
// 一步完成加载和监控
loader.MustLoadAndWatch(context.Background(), "my-app")
```
## API 参考
### `NewLoader`
创建一个新的 Loader 实例。
```go
func NewLoader[T any](config *Config, propertyKey string, targetStruct ...*T) *Loader[T]
```
参数:
- `config`: 配置实例,用于监控变化
- `propertyKey`: 监控的属性键模式(使用 "" 或 "." 监控所有配置)
- `targetStruct`: 接收配置值的结构体指针(可选)
### `NewLoaderWithAdapter`
使用适配器创建一个新的 Loader 实例。
```go
func NewLoaderWithAdapter[T any](adapter Adapter, propertyKey string, targetStruct ...*T) *Loader[T]
```
### `Load`
从配置实例加载数据并绑定到目标结构体。
```go
func (l *Loader[T]) Load(ctx context.Context) error
```
### `MustLoad`
与 Load 类似,但出错时会 panic。
```go
func (l *Loader[T]) MustLoad(ctx context.Context)
```
### `Watch`
开始监控配置变化并自动更新目标结构体。
```go
func (l *Loader[T]) Watch(ctx context.Context, name string) error
```
### `MustWatch`
与 Watch 类似,但出错时会 panic。
```go
func (l *Loader[T]) MustWatch(ctx context.Context, name string)
```
### `MustLoadAndWatch`
便捷方法,调用 MustLoad 和 MustWatch。
```go
func (l *Loader[T]) MustLoadAndWatch(ctx context.Context, name string)
```
### `Get`
返回当前配置结构体。
```go
func (l *Loader[T]) Get() T
```
### `GetPointer() *T`
返回指向当前配置结构体的指针。
```go
func (l *Loader[T]) GetPointer() *T
```
### `OnChange`
设置配置变化时调用的回调函数。
```go
func (l *Loader[T]) OnChange(fn func (updated T) error)
```
### `SetConverter`
设置在 Load 操作期间使用的自定义转换函数。
```go
func (l *Loader[T]) SetConverter(converter func (data any, target *T) error)
```
### `SetWatchErrorHandler`
设置在 Watch 过程中 Load 操作失败时调用的错误处理函数。
```go
func (l *Loader[T]) SetWatchErrorHandler(errorFunc func(ctx context.Context, err error))
```
### `SetReuseTargetStruct`
设置是否在更新时重用相同的目标结构体或创建新结构体。
```go
func (l *Loader[T]) SetReuseTargetStruct(reuse bool)
```
### `StopWatch`
停止监控配置变化并移除关联的监控器。
```go
func (l *Loader[T]) StopWatch(ctx context.Context) (bool, error)
```
### `IsWatching`
返回 Loader 是否正在监控配置变化。
```go
func (l *Loader[T]) IsWatching() bool
```
## 高级用法
### 监控特定配置键
```go
// 只监控特定配置键
loader := gcfg.NewLoaderWithAdapter[ServerConfig](cfg, "server")
```
### 使用默认值
```go
// 创建带默认值的目标结构体
var targetConfig AppConfig
targetConfig.Name = "default-app" // 设置默认值
loader := gcfg.NewLoaderWithAdapter(cfg, "", &targetConfig)
```
## 错误处理
Loader 提供了灵活的错误处理机制:
```go
loader.SetWatchErrorHandler(func(ctx context.Context, err error) {
// 处理加载错误
log.Printf("配置加载失败: %v", err)
})
```
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
346 lines
8.9 KiB
Go
346 lines
8.9 KiB
Go
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
|
|
//
|
|
// This Source Code Form is subject to the terms of the MIT License.
|
|
// If a copy of the MIT was not distributed with this file,
|
|
// You can obtain one at https://github.com/gogf/gf.
|
|
|
|
package gcfg_test
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/container/gtype"
|
|
"github.com/gogf/gf/v2/os/gcfg"
|
|
"github.com/gogf/gf/v2/os/gfile"
|
|
"github.com/gogf/gf/v2/test/gtest"
|
|
"github.com/gogf/gf/v2/util/gconv"
|
|
"github.com/gogf/gf/v2/util/guid"
|
|
)
|
|
|
|
// TestConfig is a test struct for configuration binding
|
|
type TestConfig struct {
|
|
Name string `json:"name" yaml:"name"`
|
|
Age int `json:"age" yaml:"age"`
|
|
Enabled bool `json:"enabled" yaml:"enabled"`
|
|
Features []string `json:"features" yaml:"features"`
|
|
Server ServerConfig `json:"server" yaml:"server"`
|
|
}
|
|
|
|
// TestConfig2 is a test struct for configuration binding
|
|
type TestConfig2 struct {
|
|
Name string `json:"name" yaml:"name"`
|
|
Age int `json:"age" yaml:"age"`
|
|
Enabled bool `json:"enabled" yaml:"enabled"`
|
|
Features string `json:"features" yaml:"features"`
|
|
Server ServerConfig `json:"server" yaml:"server"`
|
|
}
|
|
|
|
// TestConfig3 is a test struct for configuration binding
|
|
type TestConfig3 struct {
|
|
Name string `json:"name" yaml:"name"`
|
|
Age int `json:"age" yaml:"age"`
|
|
Enabled bool `json:"enabled" yaml:"enabled"`
|
|
Features []string `json:"features" yaml:"features"`
|
|
Server ServerConfig `json:"server" yaml:"server"`
|
|
Other string `json:"other" yaml:"other"`
|
|
}
|
|
|
|
type ServerConfig struct {
|
|
Host string `json:"host" yaml:"host"`
|
|
Port int `json:"port" yaml:"port"`
|
|
}
|
|
|
|
var configContent = `
|
|
name: "test-app"
|
|
age: 25
|
|
enabled: true
|
|
features: ["feature1", "feature2", "feature3"]
|
|
server:
|
|
host: "localhost"
|
|
port: 8080
|
|
`
|
|
|
|
func TestLoader_Load(t *testing.T) {
|
|
gtest.C(t, func(t *gtest.T) {
|
|
var (
|
|
configFile = "./" + guid.S() + ".yaml"
|
|
err = gfile.PutContents(configFile, configContent)
|
|
)
|
|
t.AssertNil(err)
|
|
defer gfile.RemoveFile(configFile)
|
|
|
|
// Create a new config instance
|
|
cfg, err := gcfg.NewAdapterFile(configFile)
|
|
t.AssertNil(err)
|
|
|
|
// Create loader
|
|
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
|
|
|
|
// Load configuration
|
|
err = loader.Load(context.Background())
|
|
t.AssertNil(err)
|
|
v := loader.Get()
|
|
|
|
// Check loaded values
|
|
t.Assert(v.Name, "test-app")
|
|
t.Assert(v.Age, 25)
|
|
t.Assert(v.Enabled, true)
|
|
t.Assert(v.Server.Host, "localhost")
|
|
t.Assert(v.Server.Port, 8080)
|
|
t.Assert(len(v.Features), 3)
|
|
})
|
|
}
|
|
|
|
func TestLoader_LoadWithDefaultValues(t *testing.T) {
|
|
gtest.C(t, func(t *gtest.T) {
|
|
var (
|
|
configFile = "./" + guid.S() + ".yaml"
|
|
err = gfile.PutContents(configFile, configContent)
|
|
)
|
|
t.AssertNil(err)
|
|
defer gfile.RemoveFile(configFile)
|
|
|
|
// Create a new config instance
|
|
cfg, err := gcfg.NewAdapterFile(configFile)
|
|
t.AssertNil(err)
|
|
|
|
// Create target struct
|
|
var targetConfig TestConfig3
|
|
targetConfig.Other = "other"
|
|
|
|
// Create loader
|
|
loader := gcfg.NewLoaderWithAdapter(cfg, "", &targetConfig)
|
|
loader.SetReuseTargetStruct(true)
|
|
|
|
// Load configuration
|
|
err = loader.Load(context.Background())
|
|
t.AssertNil(err)
|
|
v := loader.Get()
|
|
|
|
// Check loaded values
|
|
t.Assert(v.Name, "test-app")
|
|
t.Assert(v.Age, 25)
|
|
t.Assert(v.Enabled, true)
|
|
t.Assert(v.Server.Host, "localhost")
|
|
t.Assert(v.Server.Port, 8080)
|
|
t.Assert(len(v.Features), 3)
|
|
t.Assert(v.Other, "other")
|
|
})
|
|
}
|
|
|
|
func TestLoader_LoadWithPropertyKey(t *testing.T) {
|
|
gtest.C(t, func(t *gtest.T) {
|
|
var (
|
|
configFile = "./" + guid.S() + ".yaml"
|
|
err = gfile.PutContents(configFile, configContent)
|
|
)
|
|
t.AssertNil(err)
|
|
defer gfile.RemoveFile(configFile)
|
|
|
|
// Create a new config instance
|
|
cfg, err := gcfg.NewAdapterFile(configFile)
|
|
t.AssertNil(err)
|
|
|
|
// Create loader with specific property key
|
|
loader := gcfg.NewLoaderWithAdapter[ServerConfig](cfg, "server")
|
|
|
|
// Load configuration
|
|
err = loader.Load(context.Background())
|
|
t.AssertNil(err)
|
|
v := loader.Get()
|
|
|
|
// Check loaded values - only the app section should be loaded
|
|
t.Assert(v.Host, "localhost")
|
|
t.Assert(v.Port, 8080)
|
|
})
|
|
}
|
|
|
|
func TestLoader_WatchAndOnChange(t *testing.T) {
|
|
var configContent2 = `
|
|
name: test-app-2
|
|
age: 200
|
|
enabled: true
|
|
features: ["feature1", "feature2", "feature3"]
|
|
server:
|
|
host: localhost
|
|
port: 8080
|
|
`
|
|
|
|
gtest.C(t, func(t *gtest.T) {
|
|
// Create a new config instance
|
|
cfg, err := gcfg.NewAdapterContent(configContent)
|
|
t.AssertNil(err)
|
|
|
|
// Variable to track if callback was called
|
|
callbackCalled := gtype.NewBool(false)
|
|
|
|
// Create loader
|
|
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
|
|
|
|
// Set change callback
|
|
loader.OnChange(func(updated TestConfig) error {
|
|
callbackCalled.Set(true)
|
|
return nil
|
|
})
|
|
|
|
// Load configuration
|
|
err = loader.Load(context.Background())
|
|
t.AssertNil(err)
|
|
err = loader.Watch(context.Background(), "test-watcher")
|
|
t.AssertNil(err)
|
|
v := loader.Get()
|
|
t.Assert(v.Name, "test-app")
|
|
t.Assert(v.Age, 25)
|
|
err = cfg.SetContent(configContent2)
|
|
t.AssertNil(err)
|
|
time.Sleep(2 * time.Second)
|
|
v2 := loader.Get()
|
|
t.Assert(v2.Name, "test-app-2")
|
|
t.Assert(v2.Age, 200)
|
|
t.Assert(callbackCalled.Val(), true)
|
|
})
|
|
}
|
|
|
|
func TestLoader_SetConverter(t *testing.T) {
|
|
var configContent2 = `
|
|
name: test-app-2
|
|
age: 200
|
|
enabled: true
|
|
features: ["feature", "feature", "feature"]
|
|
server:
|
|
host: localhost
|
|
port: 8080
|
|
`
|
|
gtest.C(t, func(t *gtest.T) {
|
|
var (
|
|
configFile = "./" + guid.S() + ".yaml"
|
|
err = gfile.PutContents(configFile, configContent2)
|
|
)
|
|
t.AssertNil(err)
|
|
defer gfile.RemoveFile(configFile)
|
|
|
|
// Create a new config instance
|
|
cfg, err := gcfg.NewAdapterFile(configFile)
|
|
t.AssertNil(err)
|
|
|
|
// Create loader
|
|
loader := gcfg.NewLoaderWithAdapter[TestConfig2](cfg, "features")
|
|
|
|
// Set custom converter
|
|
loader.SetConverter(func(data any, target *TestConfig2) error {
|
|
s := gconv.Strings(data)
|
|
target.Features = strings.Join(s, ",")
|
|
return nil
|
|
})
|
|
|
|
// Load configuration
|
|
err = loader.Load(context.Background())
|
|
t.AssertNil(err)
|
|
v := loader.Get()
|
|
|
|
// Check converted values
|
|
t.Assert(v.Features, "feature,feature,feature")
|
|
})
|
|
}
|
|
|
|
func TestLoader_SetWatchErrorHandler(t *testing.T) {
|
|
gtest.C(t, func(t *gtest.T) {
|
|
// Create a new config instance with content that will cause converter error
|
|
cfg, err := gcfg.NewAdapterContent(configContent)
|
|
t.AssertNil(err)
|
|
|
|
// Create loader
|
|
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
|
|
|
|
// Set error handler for watch operations
|
|
errorHandled := gtype.NewBool(false)
|
|
loader.SetWatchErrorHandler(func(ctx context.Context, err error) {
|
|
errorHandled.Set(true)
|
|
})
|
|
|
|
// Set a converter that will fail
|
|
loader.SetConverter(func(data any, target *TestConfig) error {
|
|
return errors.New("converter error")
|
|
})
|
|
|
|
// Load initially - this should return error without calling error handler
|
|
err = loader.Load(context.Background())
|
|
t.AssertNE(err, nil)
|
|
t.Assert(err.Error(), "converter error")
|
|
// Error handler should NOT be called during direct Load
|
|
t.Assert(errorHandled.Val(), false)
|
|
|
|
// Start watching - now errors during Load should trigger the error handler
|
|
err = loader.Watch(context.Background(), "test-error-handler")
|
|
t.AssertNil(err)
|
|
// Reset
|
|
errorHandled.Set(false)
|
|
// Trigger a config change - this will call Load internally and should trigger error handler
|
|
err = cfg.SetContent(configContent)
|
|
t.AssertNil(err)
|
|
|
|
// Wait for watcher to process the change
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Error handler should be called during Watch's Load
|
|
t.Assert(errorHandled.Val(), true)
|
|
})
|
|
}
|
|
|
|
func TestLoader_IsWatchingAndStopWatch(t *testing.T) {
|
|
gtest.C(t, func(t *gtest.T) {
|
|
// Create a new config instance
|
|
cfg, err := gcfg.NewAdapterContent(configContent)
|
|
t.AssertNil(err)
|
|
|
|
// Create loader
|
|
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
|
|
|
|
// Initially, should not be watching
|
|
t.Assert(loader.IsWatching(), false)
|
|
|
|
// Load configuration
|
|
err = loader.Load(context.Background())
|
|
t.AssertNil(err)
|
|
|
|
// Start watching
|
|
err = loader.Watch(context.Background(), "test-stopwatch-watcher")
|
|
t.AssertNil(err)
|
|
|
|
// Now should be watching
|
|
t.Assert(loader.IsWatching(), true)
|
|
|
|
// Stop watching
|
|
stopped, err := loader.StopWatch(context.Background())
|
|
t.AssertNil(err)
|
|
t.Assert(stopped, true)
|
|
|
|
// Should not be watching anymore
|
|
t.Assert(loader.IsWatching(), false)
|
|
})
|
|
}
|
|
|
|
func TestLoader_StopWatchWithoutWatcher(t *testing.T) {
|
|
gtest.C(t, func(t *gtest.T) {
|
|
// Create a new config instance
|
|
cfg, err := gcfg.NewAdapterContent(configContent)
|
|
t.AssertNil(err)
|
|
|
|
// Create loader without starting to watch
|
|
loader := gcfg.NewLoaderWithAdapter[TestConfig](cfg, "")
|
|
|
|
// Initially, should not be watching
|
|
t.Assert(loader.IsWatching(), false)
|
|
|
|
// Try to stop watching when not watching
|
|
stopped, err := loader.StopWatch(context.Background())
|
|
t.AssertNE(err, nil)
|
|
t.Assert(stopped, false)
|
|
t.Assert(err.Error(), "No watcher name specified")
|
|
})
|
|
}
|