Skip to content

.env 文件管理

为了项目开发方便, 一般会将一些重要的配置放到 .env 文件中, 方便随时修改 比如: 链接数据库服务的用户名/密码/主机/端口等 在 Rust 中, 可以使用: dotenvy 来加载和解析 .env 文件

项目初始化

sh
cargo new env_demo && cd env_demo

安装依赖

注意安装版本API是否兼容笔记中的

sh
cargo add dotenvy
toml
[dependencies]
dotenvy = "0.15.7"

创建 .env 文件

sh
touch .env

文件内容如下:

env
DEBUG_MODE=false
DB_HOST="127.0.0.1"
DB_PORT="3306"
DB_NAME="projdb"
DB_USERNAME="root"
DB_PASSWORD="73xF3f7%9r2436z"

加载配置文件并解析

rust
use dotenvy;

use std::env;
use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    // 加载当前目录或父目录下的 .env 文件, 返回一个 Result
    // 只要这个函数执行, 就会解析env文件内容并设置环境变量(std::env::set_var)
    // 然后就可以使用标准库的 env::var 来获取
    dotenvy::dotenv()?;

    // 获取单个环境变量, 返回 Result
    let db_host = env::var("DB_HOST")?;
    println!("db_host = {db_host:?}"); // 127.0.0.1

    // 获取所有环境变量, 返回 std::env::Vars
    let env_vars = env::vars();
    for (k, v) in env_vars {
        println!("{k} = {v}");
    }

    Ok(())
}

通用配置文件管理

一般为了更直观的管理配置文件, 不会选择使用 .env 来管理配置文件, 因为 .env 一般是用于 docker-compose 部署时用的, 程序用的配置文件 一般会选择使用 toml/json/yaml 等更结构化的配置文件格式, 而需要使用这些 格式的配置文件, 可以选择使用 config

初始化项目

sh
cargo new config_demo && cd config_demo

添加依赖

注意版本API是否兼容

sh
cargo add config
toml
[dependencies]
config = "0.15.25"

创建配置文件

笔记这里以 toml 为例, 其他格式的配置文件也是一样的

在项目根目录创建 config.toml

toml
debug_mode=false
db_host="127.0.0.1"
db_port="3306"
db_name="projdb"
db_username="root"
db_password="73xf3f7%9r2436z"

加载配置文件

rust
use std::collections::HashMap;
use std::error::Error;

use config::Config;

fn main() -> Result<(), Box<dyn Error>> {
    let cfg_builder = Config::builder()
        .add_source(config::File::with_name("config.toml"))
        .build()?;

    // 直接返回一个 HashMap
    let config = cfg_builder.try_deserialize::<HashMap<String, String>>()?;
    for (k, v) in config.iter() {
        println!("{k} = {v}");
    }

    // 我准备的配置文件中有这个, 所以可以直接 unwrap
    let is_debug_mode = config.get("debug_mode");
    println!("is_debug_mode = {is_debug_mode:?}");


    Ok(())
}

将配置文件并解析为 struct

rust
use std::error::Error;

use config::Config;

// 注意需要安装依赖
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
    pub debug_mode: bool,
    pub db_host: Option<String>, // 可选配置, 默认 127.0.0.1
    pub db_port: u32,
    pub db_name: String,
    pub db_username: String,
    pub db_password: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    let cfg_builder = Config::builder()
        .add_source(config::File::with_name("config.toml"))
        .build()?;

    // 将配置文件解析为 AppConfig 实例
    let config = cfg_builder.try_deserialize::<AppConfig>()?;

    // 我准备的配置文件中有这个, 所以可以直接 unwrap
    let is_debug_mode = config.debug_mode;

    // 此时: debug_mode 是一个 bool 值, 而不是一个 String
    println!("is_debug_mode = {is_debug_mode:?}");

    // 关于 db_host 字段 Option<String> 类型, 可以这样测试
    // 1. 将 config 直接输出
    // 2. 注释掉 config.toml 中的 db_host 字段, 然后再次测试
    println!("{config:?}");

    Ok(())
}

解析配置时设置默认值

rust
use std::error::Error;

use config::Config;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
    pub debug_mode: bool,
    pub db_host: Option<String>, // 可选配置, 默认 127.0.0.1
    pub db_port: u32,
    pub db_name: String,
    pub db_username: String,
    pub db_password: String,
}

fn main() -> Result<(), Box<dyn Error>> {
    let cfg_builder = Config::builder()
        // 设置默认值, 即使配置文件中没有这个字段也不会报错, 而是默认值
        .set_default("debug_mode", false)?
        .add_source(config::File::with_name("config.toml"))
        .build()?;

    let config = cfg_builder.try_deserialize::<AppConfig>()?;
    let is_debug_mode = config.debug_mode;
    println!("is_debug_mode = {is_debug_mode:?}");
    println!("{config:?}");

    Ok(())
}

解析层级嵌套的配置文件

所谓层级嵌套, 就是说所有的配置字段并不都是在顶层的, 而是有层级结构的, 如:

json
{
  "debug": false,
  "db": {
    "host": "127.0.0.1",
    "port": 3306,
    "database": "projdb",
    "username": "root",
    "password": "123456"
  }
}
toml
# top level
debug_mode=true

# db field
[db]
host="127.0.0.1"
port="3306"
name="projdb"
username="root"
password="73xf3f7%9r2436z"

像这样的配置就需要嵌套的结构体来解析

rust
use std::error::Error;

use config::Config;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppDbConfig {
    pub host: String,
    pub port: u32,
    pub database: String,
    pub username: String,
    pub password: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct AppConfig {
    pub debug_mode: bool,
    pub db: AppDbConfig,
}

fn main() -> Result<(), Box<dyn Error>> {
    let cfg_builder = Config::builder()
        .set_default("debug_mode", false)?
        // 如果要给非顶层的字段设置默认值, 可以这样设置
        .set_default("db.port", 3306)?
        .add_source(config::File::with_name("config.toml"))
        .build()?;

    let config = cfg_builder.try_deserialize::<AppConfig>()?;
    println!("{config:?}");
    // 控制台输出如下:
    // AppConfig {
    //     debug_mode: true,
    //     db: AppDbConfig {
    //         host: "127.0.0.1",
    //         port: 3306,
    //         database: "projdb",
    //         username: "root",
    //         password: "73xf3f7%9r2436z"
    //     }
    // }

    Ok(())
}

配置文件分层

所有的配置文件分层就是:

  1. 按照不同的命令行参数, 加载不同的配置文件
  2. 比如: 在开发环境就用开发环境配置文件(合并默认配置文件)

在项目根目录下创建如下配置文件

txt
.
├── Cargo.lock
├── Cargo.toml
├── config
│   ├── default.toml  # 默认配置文件,所有环境都加载
│   ├── dev.toml      # 开发环境配置文件
│   ├── prod.toml     # 生产环境配置文件
│   └── test.toml     # 测试环境配置文件
└── src
    └── main.rs
toml
env = "dev" # default is: dev
debug_mode = false

# db field
[db]
host = "127.0.0.1"
port = 3306
database = "projdb_dev"
username = "root"
password = "123456"
toml
[db]
port = 33060 # override default.toml
toml
env = "prod"
debug_mode = false

[db]
database = "projdb_prod"
username = "root"
toml
env = "test"
debug_mode = false

[db]
database = "projdb_test"
username = "test"

代码实现:

rust
use std::error::Error;
use std::process;

use config::{Config, File, FileFormat};
use serde::{Deserialize, Serialize};

// 1. 处理命令行参数, 注意安装依赖 clap
use clap::Parser;

#[derive(Parser, Debug)]
#[command(author, version, about)]
pub struct Cli {
    /// 运行环境: dev, test, prod
    #[arg(short, long, default_value = "dev")]
    pub env: String,
}

// 用户解析配置文件 struct
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
    pub debug_mode: bool,
    pub db: AppDbConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppDbConfig {
    pub env: String,
    pub host: String,
    pub port: u32,
    pub database: String,
    pub username: String,
    pub password: String,
}

// env 参数允许的值
pub fn get_allow_envs() -> Vec<&'static str> {
    vec!["dev", "prod", "test"]
}

// 分层加载配置文件
pub fn load_configs(default_cfg_path: &str, env_cfg_path: &str) -> Result<Config, Box<dyn Error>> {
    // 1. 构建配置构建器
    let mut config_builder = Config::builder();

    // 2. 第一层: 加载基础配置 config.base.toml
    // format(FileFormat::Toml): 表示是 toml 格式的配置文件
    // required(true): 配置文件必须存在
    let base_cfg_file = File::with_name(default_cfg_path)
        .format(FileFormat::Toml)
        .required(true);
    config_builder = config_builder.add_source(base_cfg_file);

    // 3. 第二层: 根据环境加载对应的配置文件
    // required(false) 表示特定环境配置文件可以不存在(test.toml)
    // 以允许优雅降级, 使用 defalt.toml 中的内容, 如果需要环境配置
    // 文件也必须存在, 可以设置为 true
    let env_cfg_file = File::with_name(env_cfg_path)
        .format(FileFormat::Toml)
        .required(false);
    config_builder = config_builder.add_source(env_cfg_file);

    // 4. 构建 & 返回结果
    let cfg = config_builder.build()?;
    Ok(cfg)
}

fn main() -> Result<(), Box<dyn Error>> {
    // 1. 解析命令行参数获取 env 并验证, 只允许值为 dev/test/prod
    let cli = Cli::parse();
    let app_env = cli.env;
    let allowd_envs = get_allow_envs();
    if !allowd_envs.iter().any(|v| v.eq(&app_env)) {
        eprintln!("env arguments only allowed to be {}", allowd_envs.join(","));
        process::exit(1);
    }

    // 2. 分层加载配置文件, 后者覆盖前者
    println!("{app_env} config file loading...");
    let base_cfg_path = "config/default.toml";
    let env_cfg_path = format!("config/{}.toml", app_env);
    let cfg = load_configs(base_cfg_path, &env_cfg_path)?;

    // 3. 解析配置文件
    let config = cfg.try_deserialize::<AppConfig>()?;
    println!("{config:?}");
    // cargo run -- --env dev 输出结果如下:
    // AppConfig {
    //     env: "dev",
    //     debug_mode: false,
    //     db: AppDbConfig {
    //         host: "127.0.0.1",
    //         port: 33060, // dev.toml 存在所以被覆盖了
    //         database: "projdb_dev",
    //         username: "root",
    //         password: "123456"
    //     }
    //  }
    // cargo run -- --env test 输出结果如下:
    // AppConfig {
    //     env: "test",
    //     debug_mode: true,
    //     db: AppDbConfig {
    //         host: "127.0.0.1",
    //         port: 3306,
    //         database: "projdb_test",
    //         username: "test",
    //         password: "123456"
    //     }
    // }
    // cargo run -- --env prod 输出结果如下:
    // AppConfig {
    //     env: "prod",
    //     debug_mode: false,
    //     db: AppDbConfig {
    //         host: "127.0.0.1",
    //         port: 3306,
    //         database: "projdb_prod",
    //         username: "root",
    //         password: "123456"
    //     }
    // }

    Ok(())
}

Released under the MIT License.