【短信发送】Spring Boot整合多种短信接口的实现(附源码)

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") │              │
└──────────────┴───────────────┴─────────────────┘

三个关键点:

  1. 接口 :SmsHandler 定义统一的发送方法;
  2. 多实现 :每个厂商一个实现类,@Component("渠道key") 指定 Bean 名称;
  3. 注册表 :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));

七、扩展新渠道

以新增「某某云」为例,三步即可:

  1. 在 pom.xml 引入该厂商 SDK;

  2. 新建实现类,指定 Bean 名称:

    java 复制代码
    @Component(value = "xxyun")
    public class XxyunSmsHandler implements SmsHandler {
        @Override
        public JsonResult send(String mobile, String templateCode, Map<String, String> params) {
            // 调用该厂商 SDK 发送模板短信
        }
    }
  3. 业务侧改用 smsVerificationUtil.getSmsByKey("xxyun") 即可,无需修改注册表与既有实现。


八、注意事项

  1. 配置外置:AccessKey / AppKey 不要硬编码,统一走配置文件或配置中心,便于多环境与密钥轮换;模板 CODE / 模板 ID 建议在业务侧用常量类统一维护(见 6.1)。

  2. Bean 名称即渠道 key :@Component("aliyun") 中的名称就是 getSmsByKey 的入参,务必与业务侧约定一致。

  3. 建议加兜底 :getSmsByKey 返回 null 时应给出明确异常提示,避免空指针:

    java 复制代码
    SmsHandler handler = smsVerificationUtil.getSmsByKey(key);
    if (handler == null) {
        throw new IllegalArgumentException("未找到短信渠道实现:" + key);
    }
  4. 异步发送:短信属外部依赖,建议用线程池 / MQ 异步发送,避免阻塞主业务流程、影响接口响应。

  5. 异常处理:当前实现对发送失败多为「记录日志 + 返回失败标识」,调用方应判断返回值,必要时做重试或降级。

  6. 模板变量必须匹配 :params 的 key 必须与厂商模板中声明的变量一致,缺失会导致发送失败;腾讯云等按数组顺序 传参的厂商,需保证变量顺序与模板一致(建议使用 LinkedHashMap)。

  7. 超时设置 :System.setProperty 会设置 JVM 全局属性,属全局副作用,建议改为在 HTTP 客户端层单独设置。

  8. 日志脱敏:避免在日志中打印完整手机号,建议中间四位打码。

相关推荐
不爱吃鱼的蔓热莓2 小时前
Autosar之统计SWC中port、runnable、信号名等信息(三)
后端·restful
wespaul2 小时前
VeapAI 自然语言操作实战|第 04 篇:系统数据加工——把系统自身的元数据吃成知识
java·大数据·运维·数据库·人工智能
幼儿园蛋小黑2 小时前
SPI 子系统:从原理到实践
java·前端·数据库
ly76892 小时前
PostgreSQL JSONB 在 Spring Boot 中的读写边界:从 JDBC 类型映射到 GIN 索引失效
spring boot·postgresql·gin·jsonb·pgjdbc·gin 索引
yangmu32032 小时前
Unity 开发过程与底层原理
java·开发语言
lexiangqicheng2 小时前
自定义表单跨端共用机制-PC设计APP渲染
java·前端·javascript
zhikouai2 小时前
AI辅助逆向工程链路:从二进制接入到等价性校验
java·linux·前端
北城笑笑3 小时前
Server 18 ,Nginx 双服务器 HTTP转HTTPS 代理实战( HTTP 地址换 HTTPS 域名 )
运维·服务器·前端·后端·nginx·http·https
紫洋葱hh3 小时前
Go 入门基础:类型、变量与运算符
开发语言·后端·ai·golang