SpringBoot 项目里有一种代码,我每次看到都想顺手改掉:
if ("alipay".equals(payType)) {
// ...
} else if ("wechat".equals(payType)) {
// ...
}
刚开始只有两个值,看着没什么。
半年以后,alipay、wechat、unionpay、disabled、test 散落在配置文件、Service、定时任务和各种 if 里面。哪天产品把 wechat 改成 wxpay,你就知道全局搜字符串有多刺激了。
遇到这种配置,我一般不会继续堆字符串,而是直接收口到 Enum。
比如项目里有个通知功能,不同环境决定消息往哪里发:
notice:
channel: feishu
不少代码会这么接:
@Value("${notice.channel}")
private String channel;
然后业务里继续判断:
if ("feishu".equals(channel)) {
pushFeishu();
} else if ("mail".equals(channel)) {
sendMail();
}
这段代码能跑,但我不喜欢。
原因很简单,channel 从进入 Spring 容器那一刻起,就只是一个没有约束的字符串。配置里哪怕写成:
notice:
channel: fesihu
项目照样可能正常启动,直到真正走到通知逻辑才发现匹配不上。这种问题最烦的不是难修,而是发现得太晚。
我一般先把配置值变成枚举:
public enum NoticeChannel {
FEISHU,
MAIL,
NONE
}
然后配置对象别再用 String:
@ConfigurationProperties(prefix = "notice")
@Component
public class NoticeProperties {
private NoticeChannel channel = NoticeChannel.NONE;
public NoticeChannel getChannel() {
return channel;
}
public void setChannel(NoticeChannel channel) {
this.channel = channel;
}
}
配置还是原来的写法:
notice:
channel: FEISHU
业务代码一下就干净了:
@Service
public class NoticeService {
private final NoticeProperties properties;
public NoticeService(NoticeProperties properties) {
this.properties = properties;
}
public void send(String content) {
switch (properties.getChannel()) {
case FEISHU -> sendToFeishu(content);
case MAIL -> sendByMail(content);
case NONE -> {
// 当前环境关闭通知
}
}
}
private void sendToFeishu(String content) {
// 飞书发送逻辑
}
private void sendByMail(String content) {
// 邮件发送逻辑
}
}
这里真正有价值的,不只是少写了几个字符串,而是配置开始有"边界"了。
NoticeChannel 里没有的值,就不应该进入业务代码。以前 String 能装一万个乱七八糟的值,现在这个字段只能在 FEISHU、MAIL、NONE 里面选。这才是我更愿意用 Enum 管配置的原因。
不过做到这里还不够。实际项目里的配置值,经常不会老老实实跟枚举名字完全一致。比如运维习惯写:
notice:
channel: feishu-webhook
你总不能为了配合 YAML,把 Java 枚举写成这种奇怪名字。这种场景我会加一个 Converter,转换逻辑统一放一处,别让 Service 自己解析。
public enum NoticeChannel {
FEISHU("feishu-webhook"),
MAIL("email"),
NONE("off");
private final String configValue;
NoticeChannel(String configValue) {
this.configValue = configValue;
}
public static NoticeChannel parse(String value) {
for (NoticeChannel channel : values()) {
if (channel.configValue.equalsIgnoreCase(value)) {
return channel;
}
}
throw new IllegalArgumentException(
"unsupported notice.channel: " + value
);
}
}
再交给 Spring:
@Component
@ConfigurationPropertiesBinding
public class NoticeChannelConverter implements Converter<String, NoticeChannel> {
@Override
public NoticeChannel convert(String source) {
return NoticeChannel.parse(source.trim());
}
}
这样 YAML 就可以继续保持运维比较容易理解的配置:
notice:
channel: feishu-webhook
Java 内部拿到的依然是:
NoticeChannel.FEISHU
配置格式和代码模型彻底分开了。以后哪怕外部配置从 feishu-webhook 改成 feishu,只动枚举转换这一个地方,不用全项目搜字符串。
还有一类配置更适合这么干:策略开关。
比如订单超时之后的处理方式:
order:
timeout-policy: cancel
我不会写成:
if ("cancel".equals(policy)) {
...
}
if ("notify".equals(policy)) {
...
}
而是直接让枚举带一点自己的行为:
public enum TimeoutPolicy {
CANCEL {
@Override
public void handle(Long orderId) {
System.out.println("cancel order: " + orderId);
}
},
NOTIFY {
@Override
public void handle(Long orderId) {
System.out.println("notify order: " + orderId);
}
};
public abstract void handle(Long orderId);
}
调用处只剩:
timeoutPolicy.handle(orderId);
不过这种写法我也不会滥用。如果里面开始依赖数据库、Redis、MQ、Feign,我就不会继续往 Enum 里面塞。
枚举适合做状态、类型、简单映射和轻量规则,不适合变成一个偷偷摸摸的 Service。代码一旦需要注入三四个 Bean,还硬往枚举里塞,后面基本都会难看。
我平时判断一个 SpringBoot 配置该不该换 Enum,很简单。
像这种:
storage:
provider: minio
这种:
task:
mode: async
还有:
login:
strategy: sms
只要这个值是有限集合,而且业务代码会根据它进行分支,我基本都会考虑 Enum。
反过来,URL、用户名、线程数、超时时间这种配置,就别硬套枚举了。
Enum 最大的价值也不是"优雅"。它真正的作用,是能把原来藏在业务代码里的非法配置,尽量提前暴露出来。
线上配置最怕什么?不是值写错,而是值写错以后,应用还一脸正常地跑了半天。
所以我现在看 SpringBoot 配置,看到 String type、String mode、String strategy 这种字段,第一反应都会多看一眼。
有时候它真的是字符串。但很多时候,只是代码还没把边界收起来而已。如果你也有类似的配置管理心得,欢迎到 云栈社区 一起交流。