Java框架快速入门: Spring Security+OAuth2之短信服务多供应商动态切换(阿里云与LeanCloud)

概述

在现代应用开发中,短信验证码、通知等场景经常需要集成第三方短信服务。

当存在多个短信供应商(例如阿里云和LeanCloud)时,如何让系统在它们之间灵活切换,而无需修改代码、重新编译?

本篇博客将结合Spring Boot的条件注解、外部化配置以及环境变量管理,展示一种优雅的多供应商短信服务实现方案。

纲要

  • 需求背景与设计思路
  • 项目代码结构总览
  • 核心接口 SmsService 定义
  • 外部配置类 AppProperties 与多供应商配置结构
  • 阿里云短信实现
    • Maven依赖与版本管理
    • AliSmsConfig 配置类初始化 IAcsClient
    • AliSmsService 具体发送逻辑
  • 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.ymlsms.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的条件装配、外部化配置以及多供应商接口抽象,实现了短信服务的灵活切换。所有敏感信息均通过环境变量注入,符合安全最佳实践。

这种设计模式同样适用于支付、存储等其他需要多供应商集成的场景。

相关推荐
Doubbbbbbble云1 小时前
区间合并问题的常见算法模式与优化思路4
java·数据结构·算法
小溪学编程1 小时前
AQS 原理详解:从 CLH 队列到 ReentrantLock 的实现
java·开发语言
蜗牛互联网1 小时前
语音AI开始边听边说,改变的不只是响应速度
java·人工智能·后端·语音识别
qq_349447951 小时前
阿里云centos7.9系统安装jenkins
服务器·阿里云·jenkins
卓怡学长2 小时前
w233基于vue和springboot的宠物管理系统的设计与实现
java·vue.js·spring boot·spring·intellij-idea
我命由我123452 小时前
人脸识别 - 去重时间窗口
java·开发语言·python·学习·java-ee·学习方法·python3.11
逆境不可逃2 小时前
Pi Agent 学习笔记:一次提问背后的工具调用循环
java·笔记·学习
落魄大学生之流水线上谋生计2 小时前
Java并发核心机制详解:线程池、CAS、AQS、锁升级
java·开发语言·数据库
aosky2 小时前
Linux 写内容到文件的几种方式
java·linux·服务器