4. 参数来源管理:硬编码参数、配置文件参数、YAML/JSON/INI、环境变量参数、数据库参数、命令行参数

说到参数来源,我见过太多项目在这上面栽跟头了。

有的团队把所有参数写死在代码里,改个数据库连接地址都要重新部署。有的团队把参数散落在五六个配置文件里,新同事入职第一周全在找参数。还有的团队,环境变量、配置文件、数据库参数混着用,优先级乱成一锅粥。

说白了,参数来源管理不是技术难题,而是工程习惯问题。你想想看,一个批量发送系统,少说几十个参数,多则上百个。如果来源不清晰、优先级不明确,出问题的时候你根本不知道是哪个参数在捣鬼。

4.1 硬编码参数:最直接,也最危险

硬编码,就是把参数直接写在代码里。比如:

// 硬编码参数示例
public class EmailSender {
    private static final String SMTP_HOST = "smtp.example.com";
    private static final int SMTP_PORT = 587;
    private static final int BATCH_SIZE = 100;
    private static final int RETRY_TIMES = 3;
    private static final long TIMEOUT_MS = 5000;
}

这种方式的好处很明显:简单、直观、编译期就能检查。但坏处更致命——改参数就得改代码、重新编译、重新部署。

我曾经踩过的坑: 有个线上系统,SMTP 服务器地址写死在代码里。后来邮件服务迁移,需要改地址。结果运维同学找不到代码仓库,硬是反编译了 jar 包才找到参数位置。前后折腾了 4 个小时,业务停了半天。

我的建议是:硬编码只适合那些不变的值。比如数学常量、枚举值、或者你确定业务上永远不会改的配置。其他参数,请交给更灵活的方式。

4.2 配置文件参数:YAML、JSON、INI

配置文件是参数管理的主力军。三种主流格式各有千秋,我分别说说。

格式 可读性 层级支持 注释支持 生态成熟度
YAML 高(缩进结构) 强 支持(#) 高(Spring Boot 标配)
JSON 中(括号多) 强 不支持原生 极高(Web 生态)
INI 高(扁平结构) 弱(仅 Section) 支持(; 或 #) 中(Windows 传统)

我个人习惯用 YAML 做主力配置格式。原因很简单:可读性好,支持层级结构,还能写注释。你看这个例子:

# 批量发送系统配置
app:
  name: batch-sender
  version: 2.1.0

smtp:
  host: smtp.example.com
  port: 587
  auth:
    enabled: true
    username: ${SMTP_USER}
    password: ${SMTP_PASS}

batch:
  size: 100
  retry:
    times: 3
    interval_ms: 2000

rate_limit:
  per_second: 50
  per_minute: 2000

注意看,我在 YAML 里用了 ${SMTP_USER} 这种占位符。这是为了把敏感信息交给环境变量管理,后面会细说。

小技巧: 如果你的项目用 JSON 做配置,可以考虑用 JSON5 或带注释的 JSON 变体。不过说实话,YAML 在配置场景下比 JSON 顺手得多。

4.3 环境变量参数:12-Factor App 的推荐方式

环境变量是 12-Factor App 方法论里推荐的配置方式。说白了,就是把参数从代码里剥离出来,交给运行环境管理。

为什么推荐环境变量?

  • 环境隔离: 开发、测试、生产环境用不同的环境变量,代码完全不用改
  • 敏感信息: 密码、密钥、 不落盘,降低泄露风险
  • 容器友好: Docker、K8s 原生支持环境变量注入

举个例子:

# 设置环境变量(Linux/Mac)
export SMTP_HOST="smtp.prod.example.com"
export SMTP_PORT="587"
export SMTP_USER="sender@example.com"
export SMTP_PASS="s3cr3t!"
export BATCH_SIZE="200"

# 在代码中读取
String smtpHost = System.getenv("SMTP_HOST");
int smtpPort = Integer.parseInt(System.getenv("SMTP_PORT"));
注意: 环境变量是字符串类型,数字、布尔值都需要手动转换。另外,环境变量太多会污染进程空间,建议只放那些真正需要环境隔离的参数。

我在项目中遇到过一个问题:团队把所有参数都塞进环境变量,结果一个服务启动时需要读取 30 多个环境变量。新环境部署时,运维同学对着长长的环境变量列表头皮发麻。后来我们做了分层:环境变量只放敏感信息和环境差异参数,通用配置还是走配置文件。

4.4 数据库参数:动态配置的利器

有些参数需要在运行时动态修改,不能重启服务。这时候,数据库参数就派上用场了。

典型的场景:

  • 限流阈值:运营活动期间需要临时调整
  • 黑名单/白名单:需要实时更新
  • 功能开关:灰度发布时动态开启/关闭

数据库参数通常配合缓存使用,避免每次读取都查库:

-- 参数配置表
CREATE TABLE system_config (
    config_key   VARCHAR(128) PRIMARY KEY,
    config_value TEXT NOT NULL,
    updated_at   TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    description  VARCHAR(256)
);

-- 插入示例数据
INSERT INTO system_config (config_key, config_value, description)
VALUES 
('rate_limit.per_second', '50', '每秒发送上限'),
('rate_limit.per_minute', '2000', '每分钟发送上限'),
('feature.retry_on_failure', 'true', '失败是否重试');

代码里怎么用?我一般会封装一个配置中心客户端:

public class DbConfigClient {
    private final JdbcTemplate jdbcTemplate;
    private final Cache<String, String> cache;
    
    public String getConfig(String key) {
        // 先查缓存
        String value = cache.get(key);
        if (value != null) {
            return value;
        }
        // 缓存未命中,查数据库
        value = jdbcTemplate.queryForObject(
            "SELECT config_value FROM system_config WHERE config_key = ?",
            String.class, key
        );
        // 写入缓存
        cache.put(key, value);
        return value;
    }
    
    public void updateConfig(String key, String value) {
        jdbcTemplate.update(
            "UPDATE system_config SET config_value = ?, updated_at = NOW() WHERE config_key = ?",
            value, key
        );
        // 更新缓存
        cache.put(key, value);
    }
}
核心原则: 数据库参数适合管理那些需要动态变更的配置。但要注意,数据库挂了会影响参数读取,建议加本地缓存兜底。

4.5 命令行参数:临时覆盖的终极手段

命令行参数是最灵活的方式。它可以在启动时临时覆盖其他来源的参数,适合调试、临时调整、一次性任务。

常见的命令行参数解析方式:

# 使用 --key=value 风格
java -jar batch-sender.jar \
    --batch.size=200 \
    --smtp.host=smtp.test.example.com \
    --dry-run=true

# 或者使用 -D 参数(JVM 属性)
java -Dbatch.size=200 -Dsmtp.host=smtp.test.example.com \
    -jar batch-sender.jar

在代码里解析:

public class CommandLineArgs {
    public static Map<String, String> parse(String[] args) {
        Map<String, String> result = new HashMap<>();
        for (String arg : args) {
            if (arg.startsWith("--")) {
                String[] parts = arg.substring(2).split("=", 2);
                if (parts.length == 2) {
                    result.put(parts[0], parts[1]);
                }
            }
        }
        return result;
    }
}

当然,生产环境建议用成熟的命令行解析库,比如 Apache Commons CLI 或 picocli。自己手写解析容易出 bug。

4.6 参数优先级:谁说了算?

多个来源同时存在时,到底听谁的?这是参数管理最核心的问题。

我习惯用这个优先级规则(从高到低):

  1. 命令行参数:临时覆盖,最高优先级
  2. 环境变量:环境隔离,次高优先级
  3. 数据库参数:动态配置,中等优先级
  4. 配置文件参数:默认配置,较低优先级
  5. 硬编码参数:兜底默认值,最低优先级

为什么这么排?你想想看:

  • 命令行参数是启动时指定的,最临时、最具体,理应最高
  • 环境变量区分环境,比配置文件更灵活
  • 数据库参数可以动态改,但依赖数据库可用性
  • 配置文件是静态的,适合放通用默认值
  • 硬编码是最后的保底,确保没有配置时也能跑
实现技巧: 可以用「配置合并器」模式,按优先级依次读取,后面的覆盖前面的。这样代码里只需要调用一个方法就能拿到最终值。

4.7 知识体系总览

下面这张图总结了参数来源管理的整体结构,你可以对照着梳理自己的项目:

参数来源管理 · 知识体系 参数来源管理 硬编码参数 配置文件参数 环境变量参数 数据库参数 命令行参数 特点 编译期确定 改值需重新部署 适合常量 特点 YAML/JSON/INI 静态默认配置 支持层级和注释 特点 环境隔离 敏感信息不落盘 容器友好 特点 运行时动态修改 需配合缓存 依赖数据库可用性 特点 启动时临时覆盖 最高优先级 适合调试/临时调整 优先级:命令行 > 环境变量 > 数据库 > 配置文件 > 硬编码 高优先级覆盖低优先级,硬编码作为兜底默认值 工程实践建议 1. 敏感信息走环境变量,通用配置走配置文件 2. 动态参数走数据库,临时覆盖走命令行 3. 硬编码只放不变的常量

4.8 实战中的选择策略

说了这么多,到底怎么选?我总结了一个简单的决策树:

  • 这个参数会变吗? 不会 → 硬编码
  • 会变,但不同环境值不同? → 环境变量
  • 会变,但所有环境都一样? → 配置文件
  • 需要运行时动态改? → 数据库参数
  • 只是临时调试或覆盖? → 命令行参数

嗯,这个决策树我用了好几年,基本没出过问题。你可以在自己的项目里试试看。

最后说一句: 参数来源管理没有银弹。关键是要建立清晰的优先级规则,并且让团队所有人都知道这个规则。文档写清楚,代码里实现清楚,出问题的时候才能快速定位。