概述
在现代应用开发中,短信验证码、通知等场景经常需要集成第三方短信服务。
当存在多个短信供应商(例如阿里云和LeanCloud)时,如何让系统在它们之间灵活切换,而无需修改代码、重新编译?
本篇博客将结合Spring Boot的条件注解、外部化配置以及环境变量管理,展示一种优雅的多供应商短信服务实现方案。
纲要
- 需求背景与设计思路
- 项目代码结构总览
- 核心接口
SmsService定义 - 外部配置类
AppProperties与多供应商配置结构 - 阿里云短信实现
- Maven依赖与版本管理
AliSmsConfig配置类初始化IAcsClientAliSmsService具体发送逻辑
- LeanCloud短信实现
- Maven依赖
LeanCloudConfig配置类与@PostConstruct初始化LeanCloudSmsService发送逻辑
- 基于
@ConditionalOnProperty的动态Bean选择 - 环境变量与
.env文件保护敏感信息 - 完整可运行代码示例
- 总结
背景与设计思路
在项目中接入短信服务时,通常会有多个候选供应商。为了保持代码的整洁与扩展性,我们设计一个公共的 SmsService 接口,然后分别用阿里云和LeanCloud的SDK实现该接口。
通过Spring Boot的条件注解 @ConditionalOnProperty,我们可以在配置文件中指定当前使用的供应商,从而达到运行时切换的目的。
敏感信息(如AccessKey、Secret)则通过环境变量注入,避免硬编码泄露。
项目代码结构
以下为示例项目的核心包结构:
dir
src/main/java/com/example/sms/
├── SmsService.java
├── config/
│ ├── AppProperties.java
│ ├── AliSmsConfig.java
│ └── LeanCloudConfig.java
└── service/
├── AliSmsService.java
└── LeanCloudSmsService.java
src/main/resources/
├── application.yml
└── .env (本地环境变量,加入.gitignore)
核心接口定义
首先抽象出短信发送的统一接口,只包含一个发送方法:
java
package com.example.sms;
public interface SmsService {
void send(String phoneNumber, String message);
}
外部配置类
为了统一管理两家供应商的配置参数,我们创建一个配置属性类 AppProperties,并使用 @ConfigurationProperties 绑定配置文件中的前缀。此处同时包含阿里云和LeanCloud的配置组。
java
package com.example.sms.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "sms")
public class AppProperties {
private Ali ali = new Ali();
private LeanCloud leanCloud = new LeanCloud();
private Provider provider = new Provider();
// getters and setters
public static class Ali {
private String accessKeyId;
private String accessKeySecret;
private String regionId = "cn-hangzhou";
private String signName;
private String templateCode;
// getters and setters
}
public static class LeanCloud {
private String appId;
private String appKey;
private String sign;
private String template;
// getters and setters
}
public static class Provider {
private String name; // 值为 "ali" 或 "leancloud"
// getters and setters
}
}
配置文件 application.yml 示例(使用环境变量占位符):
yaml
sms:
ali:
access-key-id: ${ALI_ACCESS_KEY_ID}
access-key-secret: ${ALI_ACCESS_KEY_SECRET}
region-id: cn-hangzhou
sign-name: 您的签名
template-code: SMS_123456789
lean-cloud:
app-id: ${LEANCLOUD_APP_ID}
app-key: ${LEANCLOUD_APP_KEY}
sign: 您的签名
template: 您的模板名称
provider:
name: ${SMS_PROVIDER:ali} # 默认使用阿里云
阿里云短信服务实现
依赖引入
在 pom.xml 中添加阿里云短信SDK依赖(版本号建议使用属性统一管理):
xml
<properties>
<aliyun-sdk.version>4.5.0</aliyun-sdk.version>
</properties>
<dependencies>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>${aliyun-sdk.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>
阿里云客户端配置
我们需要初始化一个 IAcsClient,它会被多个阿里云服务共用。创建配置类 AliSmsConfig,读取外部配置并构造客户端实例。
java
package com.example.sms.config;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AliSmsConfig {
private final AppProperties appProperties;
public AliSmsConfig(AppProperties appProperties) {
this.appProperties = appProperties;
}
@Bean
public IAcsClient acsClient() {
AppProperties.Ali ali = appProperties.getAli();
IClientProfile profile = DefaultProfile.getProfile(
ali.getRegionId(),
ali.getAccessKeyId(),
ali.getAccessKeySecret()
);
return new DefaultAcsClient(profile);
}
}
阿里云短信发送实现
AliSmsService 实现了 SmsService 接口,并且通过 @ConditionalOnProperty 控制只有当 sms.provider.name=ali 时该Bean才会被加载。
java
package com.example.sms.service;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.example.sms.SmsService;
import com.example.sms.config.AppProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
@Service
@ConditionalOnProperty(name = "sms.provider.name", havingValue = "ali")
public class AliSmsService implements SmsService {
private static final Logger log = LoggerFactory.getLogger(AliSmsService.class);
private final IAcsClient acsClient;
private final AppProperties appProperties;
public AliSmsService(IAcsClient acsClient, AppProperties appProperties) {
this.acsClient = acsClient;
this.appProperties = appProperties;
}
@Override
public void send(String phoneNumber, String message) {
AppProperties.Ali ali = appProperties.getAli();
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phoneNumber);
request.setSignName(ali.getSignName());
request.setTemplateCode(ali.getTemplateCode());
request.setTemplateParam("{\"code\":\"" + message + "\"}");
try {
SendSmsResponse response = acsClient.getAcsResponse(request);
log.info("阿里云短信发送结果: {}", response.getMessage());
} catch (ClientException e) {
log.error("发送阿里云短信失败", e);
}
}
}
LeanCloud短信服务实现
依赖引入
xml
<properties>
<leancloud.version>6.1.0</leancloud.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.leancloud</groupId>
<artifactId>leancloud-core</artifactId>
<version>${leancloud.version}</version>
</dependency>
</dependencies>
LeanCloud初始化配置
LeanCloud的初始化方式与阿里云不同,它不是创建一个可复用的客户端对象,而是需要通过静态方法初始化全局环境。我们可以在配置类中使用 @PostConstruct 来完成这一步骤。
java
package com.example.sms.config;
import cn.leancloud.core.LeanCloud;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Configuration
public class LeanCloudConfig {
private final AppProperties appProperties;
public LeanCloudConfig(AppProperties appProperties) {
this.appProperties = appProperties;
}
@PostConstruct
public void initialize() {
AppProperties.LeanCloud lc = appProperties.getLeanCloud();
LeanCloud.initialize(lc.getAppId(), lc.getAppKey());
}
}
LeanCloud短信发送实现
同样使用 @ConditionalOnProperty 控制加载条件。
java
package com.example.sms.service;
import cn.leancloud.sms.LCSMSOption;
import cn.leancloud.sms.LCSMSService;
import com.example.sms.SmsService;
import com.example.sms.config.AppProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Service;
@Service
@ConditionalOnProperty(name = "sms.provider.name", havingValue = "leancloud")
public class LeanCloudSmsService implements SmsService {
private static final Logger log = LoggerFactory.getLogger(LeanCloudSmsService.class);
private final AppProperties appProperties;
public LeanCloudSmsService(AppProperties appProperties) {
this.appProperties = appProperties;
}
@Override
public void send(String phoneNumber, String message) {
AppProperties.LeanCloud lc = appProperties.getLeanCloud();
LCSMSOption option = new LCSMSOption();
option.setSignatureName(lc.getSign());
option.setTemplateName(lc.getTemplate());
option.setTtl(10); // 有效期分钟
LCSMSService.getInstance().sendSMSInBackground(
phoneNumber,
option,
message
).subscribe(
result -> log.info("LeanCloud短信发送成功: {}", result),
error -> log.error("LeanCloud短信发送失败", error)
);
}
}
环境变量与敏感信息保护
生产环境中,AccessKey等绝不应直接写入配置文件。我们通过 ${} 占位符引用环境变量,并在本地使用 .env 文件管理这些变量(该文件需要加入 .gitignore)。IntelliJ IDEA 可安装 EnvFile 插件,在启动配置中勾选对应的 .env 文件,即可自动加载为环境变量。
示例 .env 文件内容:
properties
ALI_ACCESS_KEY_ID=your_ali_key
ALI_ACCESS_KEY_SECRET=your_ali_secret
LEANCLOUD_APP_ID=your_leancloud_id
LEANCLOUD_APP_KEY=your_leancloud_key
SMS_PROVIDER=ali
确保 .gitignore 中包含:
gitignore
.env
动态切换效果
当 application.yml 中 sms.provider.name 的值为 ali 时,只有 AliSmsService 会被注入Spring容器;当值改为 leancloud 时,LeanCloudSmsService 生效。应用程序的其他部分只需注入 SmsService 接口即可,完全无感知切换。
#mermaid-svg-WALqBPP127KYgAf6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-WALqBPP127KYgAf6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-WALqBPP127KYgAf6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-WALqBPP127KYgAf6 .error-icon{fill:#552222;}#mermaid-svg-WALqBPP127KYgAf6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-WALqBPP127KYgAf6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-WALqBPP127KYgAf6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-WALqBPP127KYgAf6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-WALqBPP127KYgAf6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-WALqBPP127KYgAf6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-WALqBPP127KYgAf6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-WALqBPP127KYgAf6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-WALqBPP127KYgAf6 .marker.cross{stroke:#333333;}#mermaid-svg-WALqBPP127KYgAf6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-WALqBPP127KYgAf6 p{margin:0;}#mermaid-svg-WALqBPP127KYgAf6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-WALqBPP127KYgAf6 .cluster-label text{fill:#333;}#mermaid-svg-WALqBPP127KYgAf6 .cluster-label span{color:#333;}#mermaid-svg-WALqBPP127KYgAf6 .cluster-label span p{background-color:transparent;}#mermaid-svg-WALqBPP127KYgAf6 .label text,#mermaid-svg-WALqBPP127KYgAf6 span{fill:#333;color:#333;}#mermaid-svg-WALqBPP127KYgAf6 .node rect,#mermaid-svg-WALqBPP127KYgAf6 .node circle,#mermaid-svg-WALqBPP127KYgAf6 .node ellipse,#mermaid-svg-WALqBPP127KYgAf6 .node polygon,#mermaid-svg-WALqBPP127KYgAf6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-WALqBPP127KYgAf6 .rough-node .label text,#mermaid-svg-WALqBPP127KYgAf6 .node .label text,#mermaid-svg-WALqBPP127KYgAf6 .image-shape .label,#mermaid-svg-WALqBPP127KYgAf6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-WALqBPP127KYgAf6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-WALqBPP127KYgAf6 .rough-node .label,#mermaid-svg-WALqBPP127KYgAf6 .node .label,#mermaid-svg-WALqBPP127KYgAf6 .image-shape .label,#mermaid-svg-WALqBPP127KYgAf6 .icon-shape .label{text-align:center;}#mermaid-svg-WALqBPP127KYgAf6 .node.clickable{cursor:pointer;}#mermaid-svg-WALqBPP127KYgAf6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-WALqBPP127KYgAf6 .arrowheadPath{fill:#333333;}#mermaid-svg-WALqBPP127KYgAf6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-WALqBPP127KYgAf6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-WALqBPP127KYgAf6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WALqBPP127KYgAf6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-WALqBPP127KYgAf6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WALqBPP127KYgAf6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-WALqBPP127KYgAf6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-WALqBPP127KYgAf6 .cluster text{fill:#333;}#mermaid-svg-WALqBPP127KYgAf6 .cluster span{color:#333;}#mermaid-svg-WALqBPP127KYgAf6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-WALqBPP127KYgAf6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-WALqBPP127KYgAf6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-WALqBPP127KYgAf6 .icon-shape,#mermaid-svg-WALqBPP127KYgAf6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-WALqBPP127KYgAf6 .icon-shape p,#mermaid-svg-WALqBPP127KYgAf6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-WALqBPP127KYgAf6 .icon-shape .label rect,#mermaid-svg-WALqBPP127KYgAf6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-WALqBPP127KYgAf6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-WALqBPP127KYgAf6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-WALqBPP127KYgAf6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} @ConditionalOnProperty
@ConditionalOnProperty
SmsService 接口
sms.provider.name=ali
sms.provider.name=leancloud
AliSmsService
LeanCloudSmsService
调用阿里云 API
调用 LeanCloud API
总结
本文通过Spring Boot的条件装配、外部化配置以及多供应商接口抽象,实现了短信服务的灵活切换。所有敏感信息均通过环境变量注入,符合安全最佳实践。
这种设计模式同样适用于支付、存储等其他需要多供应商集成的场景。