Files
gf/os/gcfg/gcfg_adapter_file.go
Lance Add 73211707fb refactor(container): add default nil checker, rename RegisterNilChecker to SetNilChecker, migrate instance containers to type-safe generics (#4630)
## 变更说明

本 PR 主要对代码库进行了重构,以提升类型安全性和优化连接管理实现。

### 详细变更

#### 1. 数据库连接管理优化
- 修改 `RegisterNilChecker`方法返回实例以支持链式调用,涉及
`KVMap`、`ListKVMap`、`TSet`、`AVLKVTree`、`BKVTree`、`RedBlackKVTree`
等多个容器类型
- 更新 `Core`结构体中 `links`字段类型为类型安全的 `KVMap[ConfigNode, *sql.DB]`
- 添加专门的链接检查器函数用于连接池管理
- 使用泛型 `KVMap`替代原始 map 类型提升类型安全性
- 简化连接关闭逻辑并移除不必要的类型断言
- 优化统计功能中的迭代器实现提高性能

#### 2. 数据库驱动类型安全增强
- 将 dm、gaussdb、mssql、oracle 驱动中的 `conflictKeySet` 从 `gset.New`修改为
`gset.NewStrSet`
- 统一使用字符串集合类型以提高类型安全性

#### 3. 配置文件适配器类型安全改进
- 将 `jsonMap`从 `StrAnyMap` 类型更改为泛型 `KVMap[string, *gjson.Json]` 类型
- 添加 `jsonMapChecker` 函数用于 JSON 对象验证
- 使用 `NewKVMapWithChecker` 替代 `NewStrAnyMap` 提高类型安全性
- 简化数据库链接关闭日志中的键值转换逻辑

## 影响范围

- 数据库连接管理模块
- 多个数据库驱动实现
- 配置文件管理系统

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: John Guo <john@johng.cn>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-01-23 16:37:38 +08:00

361 lines
13 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
import (
"context"
"github.com/gogf/gf/v2/container/garray"
"github.com/gogf/gf/v2/container/gmap"
"github.com/gogf/gf/v2/container/gtype"
"github.com/gogf/gf/v2/container/gvar"
"github.com/gogf/gf/v2/encoding/gjson"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/internal/command"
"github.com/gogf/gf/v2/internal/intlog"
"github.com/gogf/gf/v2/os/gfile"
"github.com/gogf/gf/v2/os/gfsnotify"
"github.com/gogf/gf/v2/os/gres"
"github.com/gogf/gf/v2/util/gmode"
"github.com/gogf/gf/v2/util/gutil"
)
var (
// Compile-time checking for interface implementation.
_ Adapter = (*AdapterFile)(nil)
_ WatcherAdapter = (*AdapterFile)(nil)
)
// AdapterFile implements interface Adapter using file.
type AdapterFile struct {
defaultFileNameOrPath *gtype.String // Default configuration file name or file path.
searchPaths *garray.StrArray // Searching the path array.
jsonMap *gmap.KVMap[string, *gjson.Json] // The parsed JSON objects for configuration files.
violenceCheck bool // Whether it does violence check in value index searching. It affects the performance when set true(false in default).
watchers *WatcherRegistry // Watchers for watching file changes.
}
const (
commandEnvKeyForFile = "gf.gcfg.file" // commandEnvKeyForFile is the configuration key for command argument or environment configuring file name.
commandEnvKeyForPath = "gf.gcfg.path" // commandEnvKeyForPath is the configuration key for command argument or environment configuring directory path.
)
var (
supportedFileTypes = []string{"toml", "yaml", "yml", "json", "ini", "xml", "properties"} // All supported file types suffixes.
checker = func(v *Config) bool { return v == nil }
localInstances = gmap.NewKVMapWithChecker[string, *Config](checker, true) // Instances map containing configuration instances.
customConfigContentMap = gmap.NewStrStrMap(true) // Customized configuration content.
// Prefix array for trying searching in resource manager.
resourceTryFolders = []string{
"", "/", "config/", "config", "/config", "/config/",
"manifest/config/", "manifest/config", "/manifest/config", "/manifest/config/",
}
// Prefix array for trying searching in the local system.
localSystemTryFolders = []string{"", "config/", "manifest/config"}
// jsonMapChecker is the checker for JSON map.
jsonMapChecker = func(v *gjson.Json) bool { return v == nil }
)
// NewAdapterFile returns a new configuration management object.
// The parameter `file` specifies the default configuration file name for reading.
func NewAdapterFile(fileNameOrPath ...string) (*AdapterFile, error) {
var (
err error
usedFileNameOrPath = DefaultConfigFileName
)
if len(fileNameOrPath) > 0 {
usedFileNameOrPath = fileNameOrPath[0]
} else {
// Custom default configuration file name from command line or environment.
if customFile := command.GetOptWithEnv(commandEnvKeyForFile); customFile != "" {
usedFileNameOrPath = customFile
}
}
config := &AdapterFile{
defaultFileNameOrPath: gtype.NewString(usedFileNameOrPath),
searchPaths: garray.NewStrArray(true),
jsonMap: gmap.NewKVMapWithChecker[string, *gjson.Json](jsonMapChecker, true),
watchers: NewWatcherRegistry(),
}
// Customized dir path from env/cmd.
if customPath := command.GetOptWithEnv(commandEnvKeyForPath); customPath != "" {
if gfile.Exists(customPath) {
if err = config.SetPath(customPath); err != nil {
return nil, err
}
} else {
return nil, gerror.Newf(`configuration directory path "%s" does not exist`, customPath)
}
} else {
// ================================================================================
// Automatic searching directories.
// It does not affect adapter object cresting if these directories do not exist.
// ================================================================================
// Dir path of working dir.
if err = config.AddPath(gfile.Pwd()); err != nil {
intlog.Errorf(context.TODO(), `%+v`, err)
}
// Dir path of the main package.
if mainPath := gfile.MainPkgPath(); mainPath != "" && gfile.Exists(mainPath) {
if err = config.AddPath(mainPath); err != nil {
intlog.Errorf(context.TODO(), `%+v`, err)
}
}
// Dir path of binary.
if selfPath := gfile.SelfDir(); selfPath != "" && gfile.Exists(selfPath) {
if err = config.AddPath(selfPath); err != nil {
intlog.Errorf(context.TODO(), `%+v`, err)
}
}
}
return config, nil
}
// SetViolenceCheck sets whether to perform hierarchical conflict checking.
// This feature needs to be enabled when there is a level symbol in the key name.
// It is off in default.
//
// Note that turning on this feature is quite expensive, and it is not recommended
// allowing separators in the key names. It is best to avoid this on the application side.
func (a *AdapterFile) SetViolenceCheck(check bool) {
a.violenceCheck = check
a.Clear()
}
// SetFileName sets the default configuration file name.
func (a *AdapterFile) SetFileName(fileNameOrPath string) {
a.defaultFileNameOrPath.Set(fileNameOrPath)
}
// GetFileName returns the default configuration file name.
func (a *AdapterFile) GetFileName() string {
return a.defaultFileNameOrPath.String()
}
// Get retrieves and returns value by specified `pattern`.
// It returns all values of the current JSON object if `pattern` is given empty or string ".".
// It returns nil if no value found by `pattern`.
//
// We can also access slice item by its index number in `pattern` like:
// "list.10", "array.0.name", "array.0.1.id".
//
// It returns a default value specified by `def` if value for `pattern` is not found.
func (a *AdapterFile) Get(ctx context.Context, pattern string) (value any, err error) {
j, err := a.getJson()
if err != nil {
return nil, err
}
if j != nil {
return j.Get(pattern).Val(), nil
}
return nil, nil
}
// Set sets value with specified `pattern`.
// It supports hierarchical data access by char separator, which is '.' in default.
// It is commonly used to update certain configuration values in runtime.
// Note that it is not recommended using `Set` configuration at runtime as the configuration would be
// automatically refreshed if the underlying configuration file changed.
func (a *AdapterFile) Set(pattern string, value any) error {
j, err := a.getJson()
if err != nil {
return err
}
if j != nil {
err = j.Set(pattern, value)
if err != nil {
return err
}
}
fileName := a.GetFileName()
filePath, _ := a.GetFilePath(fileName)
fileType := gfile.ExtName(fileName)
adapterCtx := NewAdapterFileCtx().WithOperation(OperationSet).WithKey(pattern).WithValue(value).
WithFileName(fileName).WithFilePath(filePath).WithFileType(fileType)
a.notifyWatchers(adapterCtx.Ctx)
return nil
}
// Data retrieves and returns all configuration data as map type.
func (a *AdapterFile) Data(ctx context.Context) (data map[string]any, err error) {
j, err := a.getJson()
if err != nil {
return nil, err
}
if j != nil {
return j.Var().Map(), nil
}
return nil, nil
}
// MustGet acts as a function, but it panics if error occurs.
func (a *AdapterFile) MustGet(ctx context.Context, pattern string) *gvar.Var {
v, err := a.Get(ctx, pattern)
if err != nil {
panic(err)
}
return gvar.New(v)
}
// Clear removes all parsed configuration files content cache,
// which will force reload configuration content from the file.
func (a *AdapterFile) Clear() {
a.jsonMap.Clear()
fileName := a.GetFileName()
filePath, _ := a.GetFilePath(fileName)
fileType := gfile.ExtName(fileName)
adapterFileCtx := NewAdapterFileCtx().WithOperation(OperationClear).WithFileName(fileName).WithFilePath(filePath).WithFileType(fileType)
a.notifyWatchers(adapterFileCtx.Ctx)
}
// Dump prints current JSON object with more manually readable.
func (a *AdapterFile) Dump() {
if j, _ := a.getJson(); j != nil {
j.Dump()
}
}
// Available checks and returns whether configuration of given `file` is available.
func (a *AdapterFile) Available(ctx context.Context, fileName ...string) bool {
checkFileName := gutil.GetOrDefaultStr(a.defaultFileNameOrPath.String(), fileName...)
// Custom configuration content exists.
if a.GetContent(checkFileName) != "" {
return true
}
// Configuration file exists in the system path.
if path, _ := a.GetFilePath(checkFileName); path != "" {
return true
}
return false
}
// autoCheckAndAddMainPkgPathToSearchPaths automatically checks and adds the directory path of package main
// to the searching path list if it's currently in the development environment.
func (a *AdapterFile) autoCheckAndAddMainPkgPathToSearchPaths() {
if gmode.IsDevelop() {
mainPkgPath := gfile.MainPkgPath()
if mainPkgPath != "" {
if !a.searchPaths.Contains(mainPkgPath) {
a.searchPaths.Append(mainPkgPath)
}
}
}
}
// getJson returns a *gjson.Json object for the specified `file` content.
// It would print error if file reading fails. It returns nil if any error occurs.
func (a *AdapterFile) getJson(fileNameOrPath ...string) (configJson *gjson.Json, err error) {
usedFileNameOrPath := a.GetFileName()
if len(fileNameOrPath) > 0 && fileNameOrPath[0] != "" {
usedFileNameOrPath = fileNameOrPath[0]
}
// It uses JSON map to cache specified configuration file content.
result := a.jsonMap.GetOrSetFuncLock(usedFileNameOrPath, func() *gjson.Json {
var (
content string
filePath string
)
// The configured content can be any kind of data type different from its file type.
isFromConfigContent := true
if content = a.GetContent(usedFileNameOrPath); content == "" {
isFromConfigContent = false
filePath, err = a.GetFilePath(usedFileNameOrPath)
if err != nil {
return nil
}
if filePath == "" {
return nil
}
if file := gres.Get(filePath); file != nil {
content = string(file.Content())
} else {
content = gfile.GetContents(filePath)
}
}
// Note that the underlying configuration JSON object operations are concurrent safe.
dataType := gjson.ContentType(gfile.ExtName(filePath))
if gjson.IsValidDataType(dataType) && !isFromConfigContent {
configJson, err = gjson.LoadContentType(dataType, []byte(content), true)
} else {
configJson, err = gjson.LoadContent([]byte(content), true)
}
if err != nil {
if filePath != "" {
err = gerror.Wrapf(err, `load config file "%s" failed`, filePath)
} else {
err = gerror.Wrap(err, `load configuration failed`)
}
return nil
}
configJson.SetViolenceCheck(a.violenceCheck)
// Add monitor for this configuration file,
// any changes of this file will refresh its cache in the Config object.
if filePath != "" && !gres.Contains(filePath) {
_, err := gfsnotify.Add(filePath, func(event *gfsnotify.Event) {
a.jsonMap.Remove(usedFileNameOrPath)
if event.IsWrite() || event.IsRemove() || event.IsCreate() || event.IsRename() || event.IsChmod() {
fileType := gfile.ExtName(usedFileNameOrPath)
adapterCtx := NewAdapterFileCtx().WithFileName(usedFileNameOrPath).WithFilePath(filePath).WithFileType(fileType)
switch {
case event.IsWrite():
adapterCtx.WithOperation(OperationWrite)
case event.IsRemove():
adapterCtx.WithOperation(OperationRemove)
case event.IsCreate():
adapterCtx.WithOperation(OperationCreate)
case event.IsRename():
adapterCtx.WithOperation(OperationRename)
case event.IsChmod():
adapterCtx.WithOperation(OperationChmod)
}
a.notifyWatchers(adapterCtx.Ctx)
}
_ = event.Watcher.Remove(filePath)
})
if err != nil {
intlog.Errorf(context.TODO(), "failed listen config file event[%s]: %v", filePath, err)
}
}
return configJson
})
if result != nil {
return result, err
}
return
}
// AddWatcher adds a watcher for the specified configuration file.
func (a *AdapterFile) AddWatcher(name string, fn WatcherFunc) {
a.watchers.Add(name, fn)
}
// RemoveWatcher removes the watcher for the specified configuration file.
func (a *AdapterFile) RemoveWatcher(name string) {
a.watchers.Remove(name)
}
// GetWatcherNames returns all watcher names.
func (a *AdapterFile) GetWatcherNames() []string {
return a.watchers.GetNames()
}
// IsWatching checks and returns whether the specified `name` is watching.
func (a *AdapterFile) IsWatching(name string) bool {
return a.watchers.IsWatching(name)
}
// notifyWatchers notifies all watchers.
func (a *AdapterFile) notifyWatchers(ctx context.Context) {
a.watchers.Notify(ctx)
}