Files
2026-07-13 12:34:57 +08:00

580 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 配置管理使用说明
MyBoot 使用 [Dynaconf](https://www.dynaconf.com/) 管理配置,支持 YAML 文件、多文件合并、环境变量覆盖与远程配置文件。实现见 `myboot/core/config.py`
## 1. 快速开始
在项目根目录或 `conf/` 目录放置 `config.yaml`,应用启动时自动加载:
```yaml
# conf/config.yaml
app:
name: "MyBoot App"
version: "0.1.0"
server:
port: 8000
reload: false
logging:
level: "INFO"
```
代码中读取:
```python
from myboot.core.config import get_settings, get_config, get_config_bool
# 点号路径(推荐)
port = get_config("server.port", 8000)
debug = get_config_bool("app.debug", False)
# Dynaconf 对象
settings = get_settings()
name = settings.app.name # 属性访问
port = settings.get("server.port") # 与 get_config 等价
```
通过 `create_app` 创建应用时,配置挂在 `app.config` 上,与全局 `get_settings()` 为同一实例(首次初始化后):
```python
from myboot import create_app
app = create_app(name="我的应用", config_file="conf/config.yaml")
port = app.config.get("server.port", 8000)
```
## 2. 配置文件位置与合并
### 2.1 自动发现的文件(按加载顺序)
Dynaconf **按列表顺序加载**,**后加载的文件覆盖先加载的同名字段**:
| 顺序 | 路径 | 说明 |
| ---- | -------------------------------------------------------------------------- | ------------------------ |
| 1 | `{项目根}/config.yaml``config.yml` | 基底配置 |
| 2 | `{项目根}/conf/config.yaml``conf/config.yml` | 覆盖根目录同名键 |
| 3 | `create_app(config_file=...)` / `get_settings(config_file=...)` 传入的路径 | 覆盖上述文件 |
| 4 | 环境变量 `CONFIG_FILE` 指向的路径或 URL | **文件来源中优先级最高** |
示例:根目录 `server.port: 8000``conf/config.yaml``server.port: 9000`,最终以 **9000** 为准(若未再被 `CONFIG_FILE` 或环境变量键覆盖)。
同一嵌套对象(如 `server.cors`)在多个文件中出现时,还会受全局 `merge_enabled``dynaconf_merge` 影响,见下文 **第 3 节**
### 2.2 指定配置文件
```bash
# 本地文件
export CONFIG_FILE=/etc/myboot/production.yaml
# 远程 URL(会下载到系统临时目录缓存,失败时尝试使用缓存)
export CONFIG_FILE=https://example.com/config.yaml
```
```python
from myboot import create_app
app = create_app(config_file="conf/config.prod.yaml")
```
远程配置下载逻辑见 `_download_config`:网络失败且存在缓存时回退到缓存文件。
### 2.3 内置默认值
未在任何 YAML 或环境变量中声明时,使用 `config.py``default_settings`(节选):
| 键 | 默认值 |
| -------------------------------- | -------------- |
| `app.name` | `"MyBoot App"` |
| `app.version` | `"0.1.0"` |
| `server.host` | `"0.0.0.0"` |
| `server.port` | `8000` |
| `server.reload` | `true` |
| `server.workers` | `1` |
| `server.response_format.enabled` | `true` |
| `logging.level` | `"INFO"` |
| `scheduler.enabled` | `true` |
| `scheduler.timezone` | `"UTC"` |
| `scheduler.max_workers` | `10` |
## 3. 字典/列表合并与 `dynaconf_merge`
MyBoot 在 `create_settings()` 中启用了 Dynaconf 全局合并:
```python
# myboot/core/config.py
merge_enabled=True,
```
因此,**多个配置文件**(或带 `@merge` 的环境变量)加载到**同一嵌套字典/列表**时,默认会**深合并**,而不是简单地把后一个文件里的整块 YAML 盖掉前一个。
### 3.1 默认合并行为(`merge_enabled=True`
| 类型 | 后加载文件中的同路径值 | 结果 |
| --------------------------------- | ---------------------- | ---------------------------------------- |
| 标量(`str` / `int` / `bool` 等) | 新值 | **覆盖**旧值 |
| 字典 | 新键与旧键 | **递归合并**(保留旧文件中未出现的子键) |
| 列表 | 新元素 | **拼接合并**(可能产生重复项) |
**示例**:根目录与 `conf/` 均定义 `server.cors` 时,若后加载文件只改 `allow_origins`,未写 `allow_methods`,合并后仍会保留先前的 `allow_methods``allow_headers`
```yaml
# config.yaml(先加载)
server:
cors:
allow_origins: ["*"]
allow_methods: ["*"]
allow_headers: ["*"]
# conf/config.yaml(后加载,未使用 dynaconf_merge: false
server:
cors:
allow_origins: ["http://localhost:3000"]
```
合并结果等价于:
```yaml
server:
cors:
allow_origins: ["*", "http://localhost:3000"] # 列表被拼接
allow_methods: ["*"]
allow_headers: ["*"]
```
### 3.2 `dynaconf_merge: false` 的作用
在需要**整块替换**某个字典(或列表)而不是与旧值合并时,在该字典(或列表)内加上 **`dynaconf_merge: false`**。
该标记是 Dynaconf 的**合并控制元数据**,不会作为业务配置键出现在 `get_config()` 结果中。
| 写法 | 作用范围 |
| ----------------------------- | ---------------------------------------------------- |
| 写在某个 **dict / list 内部** | 仅该节点:后加载内容**整体替换**先加载的同级对象 |
| 写在 **YAML 文件顶层** | 整个后加载文件相对先前来源按「替换」策略处理(慎用) |
**示例:用 `false` 完全替换 `server.cors`**
```yaml
# config.yaml
server:
cors:
allow_origins: ["*"]
allow_methods: ["*"]
allow_headers: ["*"]
# conf/config.prod.yaml(后加载)
server:
cors:
allow_origins: ["https://api.example.com"]
allow_credentials: true
dynaconf_merge: false # 整块替换 cors,不保留 allow_methods / allow_headers
```
最终 `server.cors` 仅包含:
```yaml
allow_origins: ["https://api.example.com"]
allow_credentials: true
```
**对比**:若去掉 `dynaconf_merge: false`,后加载文件里未写的 `allow_methods``allow_headers` 会**继续保留**,且 `allow_origins` 可能与旧列表**拼接**。
### 3.3 `dynaconf_merge: true`
显式声明「与已有字典/列表合并」。在 `merge_enabled=True` 时,多数嵌套 dict/list **已默认合并**,一般仅在需要强调或配合 `@merge` 环境变量时使用:
```yaml
server:
cors:
allow_origins: ["http://localhost:3000"]
dynaconf_merge: true
```
环境变量也可使用 Dynaconf 的 `@merge` 标记(TOML/JSON 形式),例如:
```bash
export SERVER__CORS='@merge {"allow_origins": ["http://localhost:3000"]}'
```
### 3.4 何时用 `false`、何时用环境变量
| 场景 | 建议 |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------- |
| 生产配置要**换一整段** `server.cors` / `logging.third_party` | 后加载 YAML 中对应该段加 `dynaconf_merge: false` |
| 只改**一两个嵌套字段** | 直接写子键,或 `SERVER__CORS__ALLOW_ORIGINS=...` |
| 列表不想与旧值拼接 | 对该 list 使用 `dynaconf_merge: false`,或写完整列表并 `false` |
| 不确定合并结果 | 用 `get_settings().server.cors``get_config("server.cors")` 在本地打印验证 |
### 3.5 注意
1. **`dynaconf_merge` 只影响合并策略**,不改变「后加载文件优先」的顺序(见第 2 节)。
2. **标量字段**始终是覆盖,与 `dynaconf_merge` 无关。
3. 对**深层嵌套**的精细控制,除 `dynaconf_merge: false` 外,也可用环境变量 `__` 逐键覆盖(见第 5 节环境变量)。
4. 更多语法见 [Dynaconf 合并文档](https://www.dynaconf.com/merging/)。
## 4. 优先级总览
从高到低(后者覆盖前者):
```
环境变量键(如 SERVER__PORT > CONFIG_FILE 指向的文件 > config_file 参数 > conf/config.yaml > 根目录 config.yaml > default_settings
```
```mermaid
flowchart LR
A[default_settings] --> B[根目录 config.yaml]
B --> C[conf/config.yaml]
C --> D[config_file 参数]
D --> E[CONFIG_FILE]
E --> F[环境变量 SERVER__PORT 等]
```
## 5. 环境变量
### 5.1 规则
| 项 | 说明 |
| ------------ | ----------------------------------------------------------- |
| 前缀 | **无**`envvar_prefix=False`),变量名即配置路径的大写形式 |
| 嵌套分隔符 | **双下划线 `__`**,对应 YAML 中的层级 |
| 单下划线 `_` | 仅作为**同一层键名**的一部分(如 `keep_alive_timeout` |
| 类型 | `env_parse_values=True`,自动尝试解析布尔、数字等 |
| 未知变量 | `ignore_unknown_envvars=True`,**仅当该键已在配置树中存在时**才接受环境变量覆盖(见下) |
#### 已知键限制(`ignore_unknown_envvars=True`
MyBoot 在 `config.py` 中启用了 `ignore_unknown_envvars=True`。环境变量(含 `.env` 加载进 `os.environ` 的项)**不会**凭空新增配置项,只能**覆盖**下列来源里**已经出现过**的键路径:
1. 已加载的 YAML`config.yaml``conf/config.yaml``CONFIG_FILE` 等)
2. `config.py``default_settings` 内置项(如 `app.name``server.port``logging.level``scheduler.timezone` 等)
因此:
-`SERVER__PORT=9000` 有效(`server.port` 在默认配置或 YAML 中已有)
-`DATABASE__URL=...` **无效**,若 YAML / 默认配置里**没有** `database` 段——变量会进入 `os.environ`,但 **MyBoot 不会读入** `get_config("database.url")`
- ❌ 任意「只在 `.env` 里出现、YAML 从未声明」的键都会被**静默忽略**
**正确做法**:需要靠 `.env` 注入的新配置,先在 YAML 中**声明结构**(值可写占位符),再用环境变量覆盖:
```yaml
# conf/config.yaml — 先声明键结构
database:
url: "" # 占位,真实值放在 .env / .local.env
app:
secret_key: "" # 敏感项同理
jobs:
cleanup_task:
enabled: true
```
```bash
# .local.env — 再覆盖
DATABASE__URL=postgresql://user:pass@127.0.0.1:5432/mydb
APP__SECRET_KEY=your-local-secret-key
JOBS__CLEANUP_TASK__ENABLED=false
```
若希望环境变量**无需**在 YAML 预声明即可生效,需修改 `myboot/core/config.py``ignore_unknown_envvars` 设为 `False`(当前框架默认未开启)。
### 5.2 示例
| YAML 路径 | 环境变量 |
| --------------------------- | --------------------------------------------------------- |
| `app.name` | `APP__NAME=MyApp` |
| `server.port` | `SERVER__PORT=9000` |
| `logging.level` | `LOGGING__LEVEL=DEBUG` |
| `server.keep_alive_timeout` | `SERVER__KEEP_ALIVE_TIMEOUT=60` |
| `server.cors.allow_origins` | `SERVER__CORS__ALLOW_ORIGINS='["http://localhost:3000"]'` |
```bash
export APP__NAME="生产应用"
export SERVER__PORT=8080
export LOGGING__LEVEL=WARNING
export SCHEDULER__TIMEZONE=Asia/Shanghai
```
**不要**用单下划线表示嵌套层级,例如 `SERVER_PORT` **不会**映射到 `server.port`(除非你在 YAML 里真有名为 `server_port` 的顶层键)。
### 5.3 布尔值
`get_config_bool` 将字符串视为真:`true``1``yes``on`(不区分大小写)。
### 5.4 MyBoot 与 `.env` 的关系(0.2.0 起自动加载)
**0.2.0 起 MyBoot 自动加载项目根目录的 `.env` 文件**(通过 Dynaconf 的
`load_dotenv`),`main.py` 无需再手动调用 `load_dotenv()`
加载语义:
- 路径:**项目根目录**(含 `pyproject.toml` 的目录)下的 `.env`
- 优先级:**真实环境变量 > `.env` > YAML 配置 > 内置默认值**
`dotenv_override=False``.env` 不覆盖已存在的真实环境变量,与容器部署习惯一致);
- 仍受 **`ignore_unknown_envvars=True`** 约束:`.env` 里的变量名必须对应
**YAML 或内置默认配置中已声明过的键路径**,否则不会进入 `get_config()`
(见上文 **5.1 已知键限制**)。
```mermaid
flowchart LR
A[".env(项目根)"] -->|"框架自动加载"| C["os.environ"]
B["真实环境变量"] -->|"优先"| C
C --> D["Dynaconf / MyBoot"]
E["config.yaml"] --> D
```
**注意:**
| 事项 | 说明 |
| ------------------- | -------------------------------------------------------------------- |
| 修改 `.env` 后 | 需**重启进程**;不会热更新 |
| 版本控制 | `.env` 加入 `.gitignore`;可提交 `.env.example` 作模板 |
| 键必须已声明 | `.env` 中每一项都须在 YAML(或内置默认值)中有对应路径,见 5.1 |
| 多个 env 文件 | 框架只自动加载根目录 `.env`;需要 `.local.env` 分层时仍可在 main.py 顶部自行 `load_dotenv(..., override=True)` |
### 5.5 旧版本(≤0.1.x)手动加载方式
0.1.x 不自动加载 `.env`,需在 `create_app()` 之前手动调用:
```python
from pathlib import Path
from dotenv import load_dotenv
load_dotenv(Path(__file__).resolve().parent / ".env")
from myboot import create_app # 必须在 load_dotenv 之后
app = create_app(name="我的应用")
```
升级到 0.2.0 后这段代码可以删除(保留也无害——重复加载是幂等的)。
### 5.6 `.env` 文件如何书写
`.env` 使用 **dotenv 格式**(不是 YAML):每行 `键=值``#` 开头为注释。**不要**写 `export`(那是 shell 脚本写法)。
> **重要**`.env` 只用于**覆盖**已有配置键,不能代替 YAML 做「首次声明」。未在 YAML / `default_settings` 中出现的键(如仅写 `DATABASE__URL` 而 `config.yaml` 里没有 `database.url`)将被忽略。
#### 命名规则(与 MyBoot 一致)
- 嵌套配置用 **`__`(双下划线)** 对应 YAML 层级。
- 键名一般**大写**(与常见约定一致;写入环境后 Dynaconf 会映射到配置树)。
- 单下划线 `_` 只表示键名的一部分,例如 `KEEP_ALIVE_TIMEOUT`
#### 示例 `.env`(非敏感、可提交 `.env.example`
```bash
# 应用
APP__NAME=MyBoot App
APP__VERSION=0.1.0
APP__DEBUG=false
# 服务
SERVER__HOST=0.0.0.0
SERVER__PORT=8000
SERVER__RELOAD=true
# 日志
LOGGING__LEVEL=INFO
# 调度器
SCHEDULER__ENABLED=true
SCHEDULER__TIMEZONE=Asia/Shanghai
```
#### 示例 `.local.env`(敏感信息,勿提交 Git
以下变量均要求 `conf/config.yaml`(或根目录 `config.yaml`)中**已有同名路径**;示例见上一节 YAML 占位声明。
```bash
# 覆盖 .env 中的端口(server.port 已在 YAML/默认配置中存在)
SERVER__PORT=9000
# 以下键须先在 YAML 中声明 database.url、app.secret_key
DATABASE__URL=postgresql://user:password@127.0.0.1:5432/mydb
APP__SECRET_KEY=your-local-secret-key
```
#### 与 YAML 的对应关系
| `.env` 中的写法 | 等价 YAML 路径 | 代码读取 |
| ------------------------------------- | --------------------------- | ----------------------------------------- |
| `SERVER__PORT=9000` | `server.port` | `get_config("server.port")` |
| `LOGGING__LEVEL=DEBUG` | `logging.level` | `get_config("logging.level")` |
| `DATABASE__URL=...` | `database.url`**须先在 YAML 声明** | `get_config("database.url")` |
| `SERVER__CORS__ALLOW_ORIGINS='["*"]'` | `server.cors.allow_origins` | `get_config("server.cors.allow_origins")` |
#### 值的书写建议
```bash
# 字符串含空格或特殊字符时用引号
APP__NAME="My Boot App"
# 布尔(会被 env_parse_values 解析)
APP__DEBUG=true
SERVER__RELOAD=false
# 数字
SERVER__PORT=8080
SCHEDULER__MAX_WORKERS=20
# 列表 / 字典建议用 JSON 字符串(尤其嵌套较深时)
SERVER__CORS__ALLOW_ORIGINS=["http://localhost:3000","http://127.0.0.1:3000"]
```
**错误示例(不会映射到预期配置):**
```bash
# 错误:单下划线不能表示 server.port
SERVER_PORT=8000
# 错误:dotenv 行内不要用 export
export SERVER__PORT=8000
# 错误:YAML 缩进语法不能用在 .env
server:
port: 8000
# 错误:YAML 中未声明 database 段时,此行会被 ignore_unknown_envvars 忽略
DATABASE__URL=postgresql://...
```
**内置默认已存在、可直接用 `.env` 覆盖的键(无需 YAML** 包括:`app.name``app.version``server.host``server.port``server.reload``server.workers``logging.level``scheduler.enabled``scheduler.timezone``scheduler.max_workers` 等,完整列表见 `myboot/core/config.py``default_settings`
#### 验证是否加载成功
```python
# 在 load_dotenv 且 create_app 之后执行
from myboot.core.config import get_config
# server.port 在 YAML/默认配置中存在 → 能读到 .env 覆盖值
print(get_config("server.port"))
# database.url 仅当 YAML 中已声明 database.url 时才能读到 .env 值
print(get_config("database.url", "(YAML 未声明或 .env 未覆盖)"))
```
`echo $DATABASE__URL` 能看到 shell 环境变量,但 **`get_config` 仍可能取不到**——说明键未进入 MyBoot 配置树,请检查 YAML 是否已声明该路径。
或在 shell 中确认环境变量已写入(Windows Git Bash):
```bash
echo $SERVER__PORT
```
## 6. 读取 API
| 函数 / 对象 | 用途 |
| ------------------------------------- | ---------------------------------------- |
| `get_settings(config_file=None)` | 获取全局 `Dynaconf` 单例 |
| `get_config(key, default=None)` | 按点号路径取值 |
| `get_config_str(key, default="")` | 转为字符串 |
| `get_config_int(key, default=0)` | 转为整数,失败返回 default |
| `get_config_bool(key, default=False)` | 转为布尔 |
| `reload_config()` | 清空单例,下次 `get_settings()` 重新加载 |
| `app.config` | 应用持有的同一配置对象 |
```python
# 运行时修改(仅当前进程内存,不写回文件)
app.config.set("server.port", 9001)
```
### 6.1 单例注意
`get_settings()` 使用模块级单例,**首次调用**时确定加载了哪些文件;之后传入不同的 `config_file` 不会生效,除非先调用 `reload_config()`
```python
from myboot.core.config import get_settings, reload_config
settings = get_settings() # 已固定加载结果
reload_config()
settings = get_settings("other.yaml") # 重新加载
```
## 7. 常用配置项
与框架行为直接相关的键(完整示例可参考 `conf/config.yaml` 与 README):
### 7.1 `app`
| 键 | 说明 |
| ------------- | -------------------- |
| `app.name` | 应用名称 |
| `app.version` | 版本号 |
| `app.debug` | 调试开关(按需使用) |
### 7.2 `server`
| 键 | 说明 |
| -------------------------------------- | ----------------------------------- |
| `server.host` | 监听地址 |
| `server.port` | 端口 |
| `server.reload` | 热重载 |
| `server.workers` | Worker 数量 |
| `server.keep_alive_timeout` | Keep-Alive 超时 |
| `server.graceful_timeout` | 优雅关闭超时 |
| `server.cors` | CORS 子对象,存在时启用 CORS 中间件 |
| `server.response_format.enabled` | 是否统一响应格式 |
| `server.response_format.exclude_paths` | 排除路径列表 |
### 7.3 `logging`
| 键 | 说明 |
| --------------------- | -------------------- |
| `logging.level` | 日志级别 |
| `logging.format` | 日志格式 |
| `logging.file` | 日志文件路径(可选) |
| `logging.third_party` | 第三方库日志级别映射 |
### 7.4 `scheduler`
| 键 | 说明 |
| -------------------------- | ---------------------------------- |
| `scheduler.enabled` | 是否启用调度器 |
| `scheduler.timezone` | 时区(建议安装 `pytz` |
| `scheduler.max_workers` | 调度线程池大小 |
| `scheduler.on_all_workers` | 多进程时是否在每个 worker 运行调度 |
Cron 与任务装饰器详见 [scheduler.md](./scheduler.md)。
### 7.5 `jobs`(可选)
用于在 `@cron` / `@interval` / `@once``enabled` 参数中按任务开关:
```yaml
jobs:
cleanup_task:
enabled: false
```
```python
from myboot.core.config import get_config
@cron("0 2 * * *", enabled=get_config("jobs.cleanup_task.enabled", True))
def cleanup(self):
...
```
## 8. 配置与装饰器、组件
- **定时任务**`enabled=get_config('jobs.xxx.enabled', True)` 在类定义时求值,修改环境变量后需**重启进程**才生效。
- **日志**`Application` 构造时调用 `setup_logging(self.config)`,之后改 `logging.*` 需自行处理或重启。
- **依赖注入**`get_config` 可在模块级或组件方法内使用;与 `@component` 无冲突。
## 9. 最佳实践
1. **开发**用 `conf/config.yaml`**生产**用 `CONFIG_FILE` 或环境变量注入敏感项,避免密钥进仓库。
2. **嵌套键**统一用 `__` 环境变量,避免与 `snake_case` 字段混淆。
3. **显式设置** `logging.level``scheduler.timezone`,减少环境差异。
4. 列表、字典类配置在环境变量中优先使用 **JSON 字符串**(如 CORS origins)。
5. 需要切换配置源时调用 `reload_config()`,或保证在任意 `get_settings()` 之前设置好 `CONFIG_FILE`
6. 多文件拆分时保持「基底 → 环境专用」的加载顺序意识:后加载覆盖先加载。
7. 多文件共用同一嵌套段(如 `server.cors`)且不想**拼接列表、保留旧子键**时,在后加载文件中对应该段添加 **`dynaconf_merge: false`**(见第 3 节)。
8. 本地敏感项用 **`.local.env`** + `main.py``load_dotenv`(见第 5.5、5.6 节);敏感值放 `.env`,但**键名须先在 YAML 声明**(`ignore_unknown_envvars=True`),结构可进 `config.yaml`,秘密不进 Git。
## 10. 相关文档与代码
| 资源 | 说明 |
| ---------------------------------------------------- | ---------------------------- |
| `myboot/core/config.py` | 配置加载与便捷函数 |
| `conf/config.yaml` | 项目默认配置示例 |
| [scheduler.md](./scheduler.md) | 调度器配置与 Cron |
| [dependency-injection.md](./dependency-injection.md) | 组件与 `get_config` 结合示例 |