做Java开发的这几年几乎每个项目都会碰到短信发送的需求。注册验证码、登录提醒、订单状态通知、告警推送短信看着不起眼但真要自己从零对接一遍坑多得能让你怀疑人生。这篇文章就围绕“java短信API示例代码”这个主题把我在Spring Boot项目里集成短信发送功能的完整思路、代码实现和排坑经验一次说清楚不管是刚入门的Java新手还是需要快速搞定短信模块的老手都能直接照着抄。先说结论短信发送这事正确姿势不是去研究运营商协议而是找一个靠谱的短信服务商调用它的HTTP API配上官方SDK十几行代码就能把发送功能跑起来。真正花时间的不是写代码而是理解签名、模板、AccessKey这些概念以及处理好各种边界情况。1. 短信API接入前的方案选型1.1 短信API到底能做什么短信API说白了就是一个HTTP接口你往它那里提交手机号、短信内容、签名这些参数它帮你把短信发出去然后把发送状态通过回调或查询方式返回给你。整个过程里你不需要关心短信网关怎么连、运营商怎么对接、通道怎么维护这些脏活累活服务商都替你干了。从业务角度看短信API真正解决的是这几个刚需场景验证码用户注册、登录、找回密码这是短信用量最大的一块特点是高并发、短时效、要求3秒内到达。状态通知订单发货、支付成功、预约提醒这类消息对实时性和可追溯性要求高。营销推送促销活动、会员关怀这类对发送频率和用户退订管理有明确合规要求。系统告警服务器异常、接口失败、安全告警这类要求极低延迟通常配合异步队列使用。用API而不是自己对接运营商最大的好处是省掉了通道费和开发成本。个人或小团队直接申请运营商通道基本不可能动辄需要企业资质、月发送量承诺而用服务商API个人开发者注册账号、实名认证就能用有免费额度也有按量计费的套餐灵活得多。1.2 服务商怎么选成本、稳定性和接入难度市面上主流的短信服务商国内用的比较多的是阿里云、腾讯云、华为云还有像云片这样的老牌第三方。选哪家不能只看价格我做了个对比把关键维度列出来服务商接入方式SDK成熟度审核速度价格区间大约适合场景阿里云SDK / HTTP很高文档全较快约0.045元/条大多数业务场景生态好腾讯云SDK / HTTP高文档较全较快约0.05元/条腾讯生态内的项目华为云SDK / HTTP中上中规中矩约0.045元/条华为云上部署的项目云片HTTP SDK老牌稳定中等按套餐对价格敏感已有合作基础我的建议是项目如果部署在某个云厂商上优先选同一家的短信服务。为什么一是内网调用延迟更低二是统一账号体系运维方便三是部分服务可以共用VPC网络省去公网调用的一些麻烦事。如果项目没有云厂商绑定选阿里云或者腾讯云都行两者的文档和社区讨论量足够多搜问题更容易。选型时还要注意一个隐形坑国际短信和国内短信的通道、计费方式不一样。如果你的业务有跨境需求提前确认服务商是否支持目标国家或地区的通道别等上线了才发现发不出去。1.3 技术路线SDK优先还是HTTP裸调短信服务商基本都会提供两种接入方式直接调HTTP API或者用官方SDK。新手容易纠结其实选择标准很简单。HTTP裸调的好处是零依赖一个HTTP工具类就能搞定适合那种不想引入额外包、或者SDK版本老有兼容问题的老项目。缺点是你要自己处理签名算法、请求序列化、响应解析、异常重试代码写起来麻烦而且每个服务商的消息结构还不一样后续换服务商要改一大片。SDK的方式是我更推荐的。官方SDK帮你封装好了签名、重试、序列化这些细节暴露给你的就是几个方法调用代码量少出错概率低。像阿里云的短信SDK引入依赖、初始化Client、调用sendSms方法三步搞定没有任何黑魔法。本文后面的示例代码会以阿里云短信服务为例子同时给出一个多服务商适配的抽象思路。用阿里云举例不是吹捧它主要是它的SDK结构清晰、文档丰富用它能说清楚短信API的所有关键环节其他家思路大同小异。2. 集成前的准备账号、签名、模板和依赖2.1 三个绕不开的概念AccessKey、签名、模板很多人第一次对接短信API看文档时看到AccessKey、签名、模板直接懵了。这几个概念其实特别像寄快递AccessKey相当于你的寄件人身份凭证服务商靠它识别是谁在调用API。它分AccessKey ID和AccessKey SecretID是公开的Secret必须保密相当于你的银行卡号和密码的关系。签名相当于快递单上的寄件人名称比如“【某某科技】”它必须提前在服务商那边申请审核审核通过后你调用API时填的这个签名才能用。模板相当于快递里的内容模板短信内容不能随便写得先提交模板比如“您的验证码为${code}5分钟内有效”审核通过后会得到一个模板ID或模板Code。实际操作中最容易出问题的就是签名和模板。签名和模板都是需要人工审核的审核内容主要是看有没有明显的营销骚扰嫌疑、有没有违反内容规范。个人开发者申请签名时一般用“【个人应用】”之类的名称注意签名类型要和你的实际使用场景匹配频繁修改签名或申请与实际业务不符的签名容易被驳回。短信模板有个容易被忽略的点模板里的变量用${}占位比如“您的验证码为${code}”你在调用API时传的JSON参数里要对应传一个code字段。变量名必须和模板里完全一致大小写都要一致否则服务商会直接报参数不匹配的错误。2.2 开通短信服务与获取AccessKey以阿里云为例整个准备过程大概是这样的注册并登录阿里云账号完成实名认证。在产品控制台搜索“短信服务”开通服务新用户一般有免费短信额度。在“签名管理”里申请签名填写签名内容、适用场景等待审核。在“模板管理”里申请模板填模板内容、变量说明等待审核。在RAM访问控制里创建子用户给它授予短信服务的权限拿到AccessKey ID和AccessKey Secret。这里要提醒一下生产环境强烈建议用RAM子用户而不是主账号的AccessKey。主账号密钥权限太大一旦泄露后果是整个账号下的所有资源都裸奔。RAM子用户可以把权限精确限定在短信服务这一个产品上出了事也能及时禁用和轮换密钥。关于审核时间快的可能十几分钟慢的可能一个工作日。所以我的经验是签名和模板一定要提前申请不要等开发完了才想起来去申请。另外模板里有敏感词的时候审核时间会拉长尽量用中性措辞。2.3 开发环境与依赖引入开发环境建议JDK 8以上用了Spring Boot 2.x或3.x都可以。我示例用的是Spring Boot 2.7 Maven生产上只要不是特别老的JDK版本基本无压力。短信SDK的依赖很简单Maven的pom.xml里加这么一段dependency groupIdcom.aliyun/groupId artifactIddysmsapi20170525/artifactId version2.0.24/version /dependency这个SDK是阿里云短信服务专用的。它的命名方式看着有点长实际上是标准的阿里云OpenAPI命名规则dysmsapi是产品名20170525是API版本号。加完依赖后Maven会自动把依赖树拉下来里面包含了核心的HTTP调用、签名认证、JSON序列化等底层逻辑你完全不用关心。另外一个好用的工具是lombok如果你的项目已经用了那配置类可以写得更简洁。如果你不想用lombok手写getter/setter最多也就多点代码量不影响功能。3. 核心代码实现从配置到发送一条龙3.1 配置类把密钥和参数集中管理短信SDK需要一个Client对象它包含你的AccessKey和地域信息。我习惯把短信相关配置抽到application.yml里再用一个配置类读取这样密钥不会散落在代码里换环境也只需要改配置。先看application.yml里的配置spring: application: name: sms-demo sms: aliyun: access-key-id: your-access-key-id access-key-secret: your-access-key-secret sign-name: 你的签名 template-code: SMS_123456789 region-id: cn-hangzhou endpoint: dysmsapi.aliyuncs.com对应写一个属性绑定类Data Component ConfigurationProperties(prefix sms.aliyun) public class SmsProperties { private String accessKeyId; private String accessKeySecret; private String signName; private String templateCode; private String regionId; private String endpoint; }然后初始化SDK的Client对象Configuration public class SmsClientConfig { Bean public com.aliyun.dysmsapi20170525.Client aliyunSmsClient(SmsProperties props) { com.aliyun.teaopenapi.models.Config config new com.aliyun.teaopenapi.models.Config() .setAccessKeyId(props.getAccessKeyId()) .setAccessKeySecret(props.getAccessKeySecret()); config.endpoint props.getEndpoint(); return new com.aliyun.dysmsapi20170525.Client(config); } }这段代码的核心逻辑是创建一个带认证信息的SDK Client后续所有发送操作都通过这个Client发起。你可能注意到我用了全限定类名这是因为阿里云SDK中确实有多个叫Client的类比如有核心RPC Client和短信专用Client全限定类名能避免引入时的语义混淆。3.2 发送短信的核心方法验证码场景示例短信发送的核心代码本质是构建请求参数、调用Client、解析响应、处理异常。我们以发送验证码为例写一个完整的发送方法Service public class SmsService { Resource private com.aliyun.dysmsapi20170525.Client aliyunSmsClient; Resource private SmsProperties smsProperties; public SendSmsResult sendVerifyCode(String phone, String code) { com.aliyun.dysmsapi20170525.models.SendSmsRequest request new com.aliyun.dysmsapi20170525.models.SendSmsRequest() .setPhoneNumbers(phone) .setSignName(smsProperties.getSignName()) .setTemplateCode(smsProperties.getTemplateCode()) .setTemplateParam({\code\:\ code \}); try { com.aliyun.dysmsapi20170525.models.SendSmsResponse response aliyunSmsClient.sendSms(request); return parseResponse(response); } catch (Exception e) { // 记录异常方便排查 log.error(短信发送失败, phone: {}, reason: {}, phone, e.getMessage(), e); return SendSmsResult.fail(e.getMessage()); } } private SendSmsResult parseResponse(SendSmsResponse response) { SendSmsResponseBody body response.body; if (body ! null OK.equals(body.code)) { return SendSmsResult.success(body.bizId); } return SendSmsResult.fail(body null ? 响应为空 : body.message); } }整体流程很简单但有几个关键点需要展开。第一setTemplateParam传入的是一个JSON字符串不是直接填内容。短信模板是“您的验证码为${code}5分钟内有效”调用时你把这个JSON传进去服务端会解析它并替换模板变量。所以JSON里的key必须和模板占位符一致value必须是字符串类型如果是数字类型服务商有可能会拒绝。第二验证码这个场景有几个隐含要求验证码长度一般为4到6位数字即可有效期一般5分钟同一个手机号发送间隔要有限制防止短信轰炸。这些逻辑虽然API本身不强制但产品上必须做。我习惯在服务层里加一个简单的Redis计数器来控制频率下面这段是核心逻辑的补充思路public void checkSendFrequency(String phone) { String key sms:limit: phone; Long count redisTemplate.opsForValue().increment(key); if (count ! null count 5) { throw new BusinessException(发送太频繁请稍后再试); } if (count ! null count 1) { redisTemplate.expire(key, Duration.ofMinutes(10)); } }这段代码不是短信API本身的内容但它是短信功能上线前必须考虑的。没有这个限制你的短信接口就是别人刷量的提款机。第三发送成功之后验证码一定要存起来等用户后续提交时做校验。存储时建议存加密后的值至少不能明文落库。校验时要做防重放处理比如验证通过后立刻删除这个验证码防止同一验证码被多次使用。3.3 查询发送状态异步回调与主动查询短信发送是异步的调用sendSms接口返回成功只能说明服务商已经受理了你的发送请求并不代表用户真的收到了短信。真实的发送结果有两种获取方式消息回调SMSReport和主动查询QuerySendDetails。消息回调是推荐的生产方案。你在服务商控制台配置好回调URL服务商会在短信真实送达后往这个URL推一条JSON数据包含手机号、发送状态、错误码、回执时间等信息。回调的好处是零轮询成本、延迟低缺点是需要你的接口能稳稳定定地接收POST请求而且要注意回调消息存在重复推送的可能接口要做幂等处理。主动查询则是发送后手动调用查询接口适合低频场景。比如用户反馈没收到短信我们可以用这个接口查一下到底发到哪一步了。查询代码如下public QuerySmsResult querySendStatus(String phone, String bizId) { com.aliyun.dysmsapi20170525.models.QuerySendDetailsRequest queryRequest new com.aliyun.dysmsapi20170525.models.QuerySendDetailsRequest() .setPhoneNumber(phone) .setBizId(bizId) .setCurrentPage(1L) .setPageSize(10L) .setSendDate(20250101); try { QuerySendDetailsResponse response aliyunSmsClient.querySendDetails(queryRequest); // 解析明细列表 ListQuerySendDetailsResponseBody.QuerySendDetailsResponseBodySmsSendDetailDTOs list response.body.smsSendDetailDTOs; return QuerySmsResult.parse(list); } catch (Exception e) { log.error(查询短信状态失败, phone: {}, bizId: {}, phone, bizId, e); return QuerySmsResult.fail(); } }这里有个参数要特别注意setSendDate是必传项格式是YYYYMMDD只能查询当天的发送记录查历史数据需要调整日期参数。这也是查询接口的一个局限性如果你的业务需要长期追溯短信状态最可靠的做法还是把回调数据落库。回调报文的处理和普通接口没什么区别唯一需要注意的是回调来源验证。短信服务商推送回调时可能会在Header里带签名或Token你要校验一下来源防止伪造回调捣乱。这个细节很多教程都不提但实际生产环境特别重要。3.4 多服务商适配面向接口编程如果我们把短信API封装成统一的接口后续切换服务商或者做多通道灾备就会轻松很多。这也是我在多个项目里验证过的做法定义一个短信发送接口提供阿里云、腾讯云等不同实现通过配置控制激活哪一套。public interface SmsSender { SmsResult send(SmsRequest request); } Data public class SmsRequest { private String phone; private String templateCode; private MapString, String templateParams; } public class AliyunSmsSender implements SmsSender { Override public SmsResult send(SmsRequest request) { // 内部构建 AliSendSmsRequest调用 SDK } } public class TencentSmsSender implements SmsSender { Override public SmsResult send(SmsRequest request) { // 内部构建 TencentSendSmsRequest调用 SDK } }这样设计带来的直接好处是业务层只依赖SmsSender接口不清楚底层到底用的是哪家。哪天阿里云涨价了、审核不过了、或者出故障了你只需要替换实现类业务代码一行不用改。如果要做多通道灾备也可以通过一个路由层按权重或优先级选择不同的Sender。不过也要提醒一下不要为了设计而设计。如果你的项目只是内部小工具用一个服务商就足够了过度抽象反而浪费时间。多服务商适配方案适合那种短信量比较大、或者业务对短信可用性要求极高的场景。4. 常见问题与排查技巧实录4.1 错误码速查表短信API返回的错误码统一放在响应体的code字段里。我在实际开发中把它们分成了三类参数类错误、权限类错误、业务类错误。下面这张表是从踩过的坑里整理出来的高频错误码错误码含义常见原因与解决方向isv.INVALID_PARAMETERS参数不合法检查手机号格式、模板变量是否每一项都传了isv.SMS_SIGNATURE_ILLEGAL签名不合法签名未审核通过或与实际签名内容不一致isv.SMS_TEMPLATE_ILLEGAL模板不合法模板未审核或模板Code填错isv.MOBILE_NUMBER_ILLEGAL手机号格式错误检查是否带了86前缀、是否有空格isv.BUSINESS_LIMIT_CONTROL业务限流触发服务商频控策略需降低频率isv.AMOUNT_NOT_ENOUGH账户余额不足充值检查是否欠费isv.RAM_PERMISSION_DENYRAM权限被拒绝子账号未授权短信服务权限默认错误签名签名与模板不匹配签名或模板归属于不同应用或版本排查的时候先判断是不是配置问题再判断是不是业务问题。最蠢的排查方式是一开始就怀疑代码写错了实际上八成是签名字符串少了个“【】”或者多打了个空格。4.2 签名和模板那点事最常见的坑签名和模板是短信API使用中报错率最高的地方。拿签名来说一个很典型的坑是在控制台申请签名时填的是“某某科技”但在代码里setSignName传的却是“【某某科技】”。不同服务商对签名格式要求不一样阿里云要求传纯签名内容不带【】腾讯云则要看具体API版本有的是需要带【】的。所以一定要先确认你用的服务商的具体要求。模板方面的坑也很多。最经典的一个就是模板变量里加了特殊字符比如JSON里传的value带了换行符服务商那边解析时直接报参数不合法。遇到这种情况建议在发送前对参数值做一次trim和长度校验。另外还有一类是审核期间的坑。签名和模板提交后在审核通过前的状态可能显示“待审核”或“审核中”此时调用API大概率会失败。我的经验是写代码前先看控制台上签名和模板的状态别闷头写代码写完发现啥都发不出去。4.3 生产环境必须处理好的三个问题第一个问题是超时设置。短信API是外部调用网络抖动、服务商繁忙都可能导致请求超时。SDK默认的超时时间可能偏长或偏短建议显式设置连接超时和读取超时一般连接超时设3秒读取超时设5秒比较合适。超时后要做重试但重试策略要有上限比如最多3次并且使用指数退避防止雪崩。第二个问题是日志记录。发送短信涉及用户隐私和费用消耗每个请求都要记日志包括手机号、模板Code、参数、返回码、耗时。排查用户投诉“收不到短信”时没有日志就只能干瞪眼。我习惯把短信日志单独放一个Logger输出到独立的日志文件方便按时间线排查。第三个问题是异步发送。如果短信是登录流程里的关键环节不要在用户请求线程里同步等待短信服务商返回然后才给用户响应。正确做法是把发送任务丢到消息队列或线程池里异步执行用户先收到“验证码已发送”的页面反馈短信在后台飞。这样用户体验好短信服务商偶发延迟也不至于拖垮整个请求链路。如果要用线程池异步处理下面是一个简单的示例Component public class SmsExecutor { private final ExecutorService executor Executors.newFixedThreadPool(8); public void submit(Runnable task) { executor.execute(task); } }这里要注意线程池的拒绝策略。如果短信并发量特别大或者线程池队列满了需要选择合理的拒绝策略至少不要是AbortPolicy直接把任务丢掉。我用的是CallerRunsPolicy发现线程池满的时候让调用线程自己执行宁可慢一点也不要丢短信。4.4 从“能发短信”到“发得好”踩坑之后的心得短信功能写完之后并不是万事大吉。上线前有几件事一定要做第一准备好一套测试手机号。不同运营商的手机号对短信接收有细微差别建议至少准备移动、联通、电信各一个实测一遍。有些短信服务商的通道在某个运营商下会有延迟提前发现比上线后接到投诉强。第二签名和模板的审核不代表永久有效。某个服务商的规范调整后你的模板可能会被重新审核内容违规或者敏感词命中会被禁用。建议定时去控制台看一眼模板状态也可以用开放API批量查询模板列表把这个检查放到告警平台里。第三短信费用要监控。充值的钱扣完了短信会静默失败用户那边毫无感知。我见过真实线上事故用户注册收不到验证码后台日志一堆AMOUNT_NOT_ENOUGH一查余额是0。所以余额监控必须做低于阈值就触发告警最好接上钉钉或企微机器人。最后想说一点个人体会。短信API本身不复杂真正难的是把它嵌入业务场景后的各种边界处理频率控制、幂等、超时重试、状态通知、费用监控。很多项目上线时“能发短信”但一遇到流量高峰就各种挂。把这些基础功课做扎实了才不会在半夜被用户投诉电话叫醒。如果你也正在做Java项目的短信对接建议从最小可用版本起步一个配置文件、一个发送方法、一条验证码下发链路。跑通了再逐步补全查询、回调、多通道这些能力。别一开始就铺太大把核心链路踩稳了后面都是加分项。