Spring Boot 项目整合多种短信接口的实现
本文档汇总「同一套代码支持多种短信服务商 」的实现方案:定义统一接口
SmsHandler,各厂商(腾讯云、阿里云...)各自实现,通过 Spring 自动注入到注册表SmsVerificationUtil中,业务侧只需按 key 取用即可切换渠道。本文只讲代码落地:引入 pom 依赖 → 编写接口 → 编写实现类 → 调用示例。
⚠️ 文中 AccessKey / AppKey 等敏感信息已脱敏,真实值请从配置中心读取。
一、设计思路
业务代码
│ smsVerificationUtil.getSmsByKey("aliyun")
▼
SmsVerificationUtil(注册表)
│ 持有 Map<String, SmsHandler>,Spring 自动注入
│ key = Bean 名称,value = 各厂商实现
▼
┌──────────────┬───────────────┬─────────────────┐
│ QcloudSmsHandler │ AliyunSmsHandler │ ...其他渠道 │
│ @Component("qcloud") │ @Component("aliyun") │ │
└──────────────┴───────────────┴─────────────────┘
三个关键点:
- 接口 :
SmsHandler定义统一的发送方法; - 多实现 :每个厂商一个实现类,
@Component("渠道key")指定 Bean 名称; - 注册表 :
SmsVerificationUtil注入Map<String, SmsHandler>------Spring 会自动把所有实现按「Bean 名称 → 实例 」装进 Map,业务侧getSmsByKey("渠道key")即可拿到对应实现。
新增渠道时只需加一个实现类,业务代码无需改动。
二、引入 pom 依赖
pom.xml(按需引入,用到哪个渠道就加哪个):
xml
<!-- ========== 腾讯云短信 ========== -->
<dependency>
<groupId>com.github.qcloudsms</groupId>
<artifactId>qcloudsms</artifactId>
<version>1.0.6</version>
</dependency>
<!-- ========== 阿里云短信 ========== -->
<!-- 阿里云 SDK 核心 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>3.3.1</version>
</dependency>
<!-- 阿里云短信 SDK -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>1.0.0</version>
</dependency>
三、编写统一接口 SmsHandler
com.redxun.common.sms.SmsHandler:
java
package com.redxun.common.sms;
import com.redxun.common.base.entity.JsonResult;
import java.util.Map;
/**
* 短信发送统一接口
* 各厂商实现该接口,并以 @Component("渠道key") 注册
*/
public interface SmsHandler {
/**
* 发送模板短信
*
* @param mobile 手机号
* @param templateCode 模板标识(阿里云为模板 CODE;腾讯云为模板 ID)
* @param params 模板变量,key 必须与模板中声明的变量一致
* @return 发送结果
*/
JsonResult send(String mobile, String templateCode, Map<String, String> params);
}
若目标项目使用若依系的
AjaxResult,把JsonResult全局替换为AjaxResult即可,方法签名不变。
四、编写注册表 SmsVerificationUtil
com.redxun.common.utils.SmsVerificationUtil:
java
package com.redxun.common.utils;
import com.redxun.common.sms.SmsHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 短信校验工具类(渠道注册表)
*/
@Service
public class SmsVerificationUtil {
/**
* Spring 会自动注入所有 SmsHandler 实现:
* key = @Component 的 Bean 名称(如 qcloud、aliyun)
* value = 对应实现类实例
*/
@Autowired
private Map<String, SmsHandler> smsMap;
/**
* 通过 Key 获取消息处理器
*
* @param key 渠道标识,如 "qcloud" / "aliyun"
* @return 对应渠道的处理器
*/
public SmsHandler getSmsByKey(String key) {
return smsMap.get(key);
}
}
五、编写实现类
5.1 腾讯云:QcloudSmsHandler
com.redxun.common.sms.QcloudSmsHandler:
java
package com.redxun.common.sms;
import com.alibaba.fastjson.JSONException;
import com.github.qcloudsms.SmsSingleSender;
import com.github.qcloudsms.SmsSingleSenderResult;
import com.redxun.common.base.entity.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* 腾讯云短信接口
*/
@Component(value = "qcloud")
public class QcloudSmsHandler implements SmsHandler {
private static final Logger log = LoggerFactory.getLogger(QcloudSmsHandler.class);
/** ===== 以下配置项建议放到 application.yml / 配置中心 ===== */
/** 短信应用 SDK AppID */
@Value("${tencent.sms.app-id}")
private int appId;
/** 短信应用 SDK AppKey */
@Value("${tencent.sms.app-key}")
private String appKey;
/** 短信签名内容(注意是「签名内容」,不是「签名 ID」) */
@Value("${tencent.sms.sign-name}")
private String smsSign;
/** 国际/港澳台短信区号,默认 86(中国大陆) */
@Value("${tencent.sms.nation-code:86}")
private String nationCode;
/**
* 发送模板短信
*
* @param mobile 手机号
* @param templateCode 腾讯云模板 ID(数字字符串)
* @param params 模板变量,按模板中定义的顺序传入
*/
@Override
public JsonResult send(String mobile, String templateCode, Map<String, String> params) {
JsonResult jsonResult = new JsonResult();
try {
SmsSingleSender ssender = new SmsSingleSender(appId, appKey);
// 签名参数未提供或者为空时,会使用默认签名发送短信
SmsSingleSenderResult result = ssender.sendWithParam(
nationCode, mobile, Integer.parseInt(templateCode),
toArray(params), smsSign, "", "");
log.info("腾讯云短信返回:result={}, errMsg={}, fee={}, sid={}",
result.result, result.errMsg, result.fee, result.sid);
if (result.result == 0) {
jsonResult.setSuccess(true);
} else {
jsonResult.setSuccess(false);
jsonResult.setMessage(result.errMsg);
}
return jsonResult;
} catch (JSONException e) {
jsonResult.setSuccess(false);
jsonResult.setMessage(e.getMessage());
} catch (IOException e) {
jsonResult.setSuccess(false);
jsonResult.setMessage(e.getMessage());
} catch (com.github.qcloudsms.httpclient.HTTPException e) {
jsonResult.setSuccess(false);
jsonResult.setMessage(e.getMessage());
} catch (Exception e) {
log.error("腾讯云短信发送失败", e);
jsonResult.setSuccess(false);
jsonResult.setMessage(e.getMessage());
}
return jsonResult;
}
/**
* 腾讯云模板变量按数组顺序传入,此处按 Map 的迭代顺序转换
* (需保证 Map 的顺序与模板变量顺序一致,建议使用 LinkedHashMap)
*/
private String[] toArray(Map<String, String> params) {
return params == null ? new String[0] : params.values().toArray(new String[0]);
}
}
对应的配置项
yaml
tencent:
sms:
app-id: 1400****** # 已脱敏,短信应用 SDK AppID
app-key: "****************" # 已脱敏,短信应用 SDK AppKey
sign-name: "友医伴健康" # 短信签名内容(不是签名 ID)
nation-code: "86" # 国际/港澳台区号,默认 86
5.2 阿里云:AliyunSmsHandler
com.redxun.common.sms.AliyunSmsHandler:
java
package com.redxun.common.sms;
import com.alibaba.fastjson.JSONObject;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.redxun.common.base.entity.JsonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 阿里云短信接口
*/
@Component(value = "aliyun")
public class AliyunSmsHandler implements SmsHandler {
private static final Logger log = LoggerFactory.getLogger(AliyunSmsHandler.class);
private static final String PRODUCT = "Dysmsapi";
private static final String DOMAIN = "dysmsapi.aliyuncs.com";
private static final String REGION = "cn-hangzhou";
/** ===== 以下配置项建议放到 application.yml / 配置中心 ===== */
@Value("${aliyun.sms.access-key-id}")
private String accessKeyId;
@Value("${aliyun.sms.access-key-secret}")
private String accessKeySecret;
/** 短信签名 */
@Value("${aliyun.sms.sign-name}")
private String signName;
/**
* 发送模板短信
*
* @param mobile 手机号
* @param templateCode 阿里云模板 CODE,如 SMS_508015266
* @param params 模板变量,key 必须与模板中的 ${} 变量一致
*/
@Override
public JsonResult send(String mobile, String templateCode, Map<String, String> params) {
JsonResult jsonResult = new JsonResult();
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
IClientProfile profile = DefaultProfile.getProfile(REGION, accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint(REGION, REGION, PRODUCT, DOMAIN);
IAcsClient acsClient = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(mobile);
request.setSignName(signName);
request.setTemplateCode(templateCode);
JSONObject obj = new JSONObject();
if (params != null) {
obj.putAll(params);
}
request.setTemplateParam(obj.toJSONString());
try {
SendSmsResponse response = acsClient.getAcsResponse(request);
log.info("阿里云短信返回:Code={}, Message={}, RequestId={}, BizId={}",
response.getCode(), response.getMessage(),
response.getRequestId(), response.getBizId());
if ("OK".equals(response.getCode())) {
jsonResult.setSuccess(true);
} else {
jsonResult.setSuccess(false);
jsonResult.setMessage(response.getMessage());
}
} catch (Exception e) {
log.error("阿里云短信发送失败", e);
jsonResult.setSuccess(false);
jsonResult.setMessage(e.getMessage());
}
return jsonResult;
}
}
对应的配置项
yaml
aliyun:
sms:
access-key-id: "LTAI****************" # 已脱敏,RAM 用户 AccessKeyId
access-key-secret: "************************" # 已脱敏,RAM 用户 AccessKeySecret
sign-name: "友医伴健康" # 短信签名
六、调用示例
统一约定:验证码、流程审批等场景的参数(验证码、订单号、审批信息等)全部由业务代码准备好,再按「手机号 + 模板 + 变量」调用统一发送方法。
6.1 业务侧维护模板常量
java
/**
* 短信模板常量:模板 CODE / 模板 ID 与业务场景的对应关系由业务侧维护
*/
public final class SmsTemplates {
private SmsTemplates() {
}
/** 验证码 */
public static final String CAPTCHA = "SMS_508015266";
/** 下单支付成功 */
public static final String ORDER_PAID = "SMS_508125195";
/** 订单指派给陪诊师 */
public static final String ORDER_ASSIGNED = "SMS_508160180";
/** 订单回退通知管理员 */
public static final String ORDER_BACK = "SMS_507950246";
/** 订单取消通知管理员和陪诊师 */
public static final String ORDER_CANCELED = "SMS_507930223";
// ... 其余场景按需补充
}
6.2 发送验证码(业务侧生成验证码)
java
@Service
public class LoginSmsService {
@Resource
private SmsVerificationUtil smsVerificationUtil;
public String sendLoginCode(String mobile) {
// 1. 业务侧生成验证码
String code = String.valueOf(new Random().nextInt(900000) + 100000);
// 2. 业务侧缓存验证码(如 Redis,并设置过期时间)
// redisCache.setCacheObject("sms:code:" + mobile, code, 5, TimeUnit.MINUTES);
// 3. 组装模板变量,调用统一发送接口
JsonResult result = smsVerificationUtil.getSmsByKey("aliyun")
.send(mobile, SmsTemplates.CAPTCHA, Map.of("code", code));
if (!result.isSuccess()) {
throw new RuntimeException("验证码发送失败:" + result.getMessage());
}
return code;
}
}
6.3 发送业务通知
java
// 订单指派给陪诊师
smsVerificationUtil.getSmsByKey("aliyun")
.send(mobile, SmsTemplates.ORDER_ASSIGNED, Map.of("orderNo", orderNo));
// 下单支付成功
smsVerificationUtil.getSmsByKey("aliyun")
.send(mobile, SmsTemplates.ORDER_PAID, Map.of("orderNo", orderNo));
6.4 发送流程审批通知(参数由业务侧组装)
java
// 腾讯云:模板变量按顺序传入,建议使用 LinkedHashMap 保证顺序
Map<String, String> params = new LinkedHashMap<>();
params.put("name", "张三");
params.put("date", "2026-10-13");
smsVerificationUtil.getSmsByKey("qcloud").send(mobile, "1234568", params);
6.5 切换渠道
java
// 只需换 key,业务参数完全不变
smsVerificationUtil.getSmsByKey("qcloud")
.send(mobile, "1234567", Map.of("code", code));
6.6 建议在 Service 中再封装一层
java
@Service
public class SmsService {
/** 当前使用的渠道,可配置化,便于随时切换 */
@Value("${sms.channel:aliyun}")
private String channel;
@Resource
private SmsVerificationUtil smsVerificationUtil;
/**
* 发送模板短信
*/
public JsonResult send(String mobile, String templateCode, Map<String, String> params) {
SmsHandler handler = smsVerificationUtil.getSmsByKey(channel);
if (handler == null) {
throw new IllegalArgumentException("未找到短信渠道实现:" + channel);
}
return handler.send(mobile, templateCode, params);
}
}
业务代码只需调用:
java
smsService.send(mobile, SmsTemplates.ORDER_ASSIGNED, Map.of("orderNo", orderNo));
七、扩展新渠道
以新增「某某云」为例,三步即可:
-
在
pom.xml引入该厂商 SDK; -
新建实现类,指定 Bean 名称:
java@Component(value = "xxyun") public class XxyunSmsHandler implements SmsHandler { @Override public JsonResult send(String mobile, String templateCode, Map<String, String> params) { // 调用该厂商 SDK 发送模板短信 } } -
业务侧改用
smsVerificationUtil.getSmsByKey("xxyun")即可,无需修改注册表与既有实现。
八、注意事项
-
配置外置:AccessKey / AppKey 不要硬编码,统一走配置文件或配置中心,便于多环境与密钥轮换;模板 CODE / 模板 ID 建议在业务侧用常量类统一维护(见 6.1)。
-
Bean 名称即渠道 key :
@Component("aliyun")中的名称就是getSmsByKey的入参,务必与业务侧约定一致。 -
建议加兜底 :
getSmsByKey返回null时应给出明确异常提示,避免空指针:javaSmsHandler handler = smsVerificationUtil.getSmsByKey(key); if (handler == null) { throw new IllegalArgumentException("未找到短信渠道实现:" + key); } -
异步发送:短信属外部依赖,建议用线程池 / MQ 异步发送,避免阻塞主业务流程、影响接口响应。
-
异常处理:当前实现对发送失败多为「记录日志 + 返回失败标识」,调用方应判断返回值,必要时做重试或降级。
-
模板变量必须匹配 :
params的 key 必须与厂商模板中声明的变量一致,缺失会导致发送失败;腾讯云等按数组顺序 传参的厂商,需保证变量顺序与模板一致(建议使用LinkedHashMap)。 -
超时设置 :
System.setProperty会设置 JVM 全局属性,属全局副作用,建议改为在 HTTP 客户端层单独设置。 -
日志脱敏:避免在日志中打印完整手机号,建议中间四位打码。