基于设计模式改造短信网关服务实战篇(设计思想、方案呈现、源码)

本文涉及的产品
短信服务,100条 3个月
短信服务,200条 3个月
数字短信套餐包(仅限零售电商行业),100条 12个月
简介: 基于设计模式改造短信网关服务实战篇(设计思想、方案呈现、源码)

背景

博主通过在工作中的总结,接到过各种不同短信提供商的短信管理功能,一旦涉及到 2B 服务时,经常会出现需要根据不同的对接方来进行短信发放了,比如:阿里云、腾讯云、华为云等等,各大短信提供商平台,至此,想整理一套集成多种云服务短信提供商的方案出来,提供可扩展、低耦合的设计方案呈现出来,同时可以一起和大家回顾一下设计模式这块相关的理论知识以及具体落地!

方案

如何设计?

往往每个功能,例如:签名、模板,服务提供方它们对应的实体结构又截然不同,所以对于实体而言,它们也可以抽象出来,例如:签名->AbstractSMSSign、模板->AbstractSMSTemplate、短链->AbstractSMSShortLink

从实体这块抽象出来以后,最终都会去调用同一个能力,而实体只能有一个,这时候需要一个抽象工厂来对每个服务提供方工厂进行能力的聚合,每个服务提供方的工厂只创建与自己相关的操纵实体,从而引出了另外一种设计模式:抽象工厂模式

从短信服务提供方提供的能力来看,涉及到的功能有签名、模板、短链,短信发送,查询状态等等,而这些功能一般服务提供方都会存在,那么这些就可以看做是公共能力了,就可以将它们抽象出来作为父类的能力,从而让不同的服务提供方子类各自继承父类,由子类去作具体的能力实现逻辑,从而引出了一种设计模式:策略模式

In addition(另外),在调用服务提供方的能力时,我们往往需要与它们建立好连接,它们那边作为服务端,而我们这边作为客户端,因此创建客户端实例成为了必不可少的一部分,从这里看来,我们可以把客户端的这个实例,作为单例存在,所有能力都基于同一个客户端去向服务提供方发出请求,从而引出了另外一种设计模式:单例模式

改造方案

聚合实体

基于设计的方法进行类结构关系的梳理如上,使用抽象工厂模式来分解不同实体之间的耦合度

从传统的理解来看,把阿里云、腾讯云比喻为不同的产品,组合产品族,所以每一套有相似能力的产品将它们统一由抽象工厂去管理,从而引出了:AbstractSMSFactory

每一种产品中,创建签名、模板、短信实体是它所属的能力,但每个产品对它的能力又会不一样的生产过程,比如:校验方式不同、传值多与少;所以将这些能力统一由抽象实体去统一,当然它可以把公共的属性放入到其中,从而引出了:AbstractSign、AbstractTemplate、AbstractSendSms

对于抽象工厂这种模式,有一种弊端,当产品出现了新的能力以后,这时候需要改动抽象工厂类以及抽象工厂子类的代码,比如:增加了短链的功能,那么 AbstractSMSFactory 需要提供创建抽象短链的实体,而每个子工厂要去创建自己产品内短链特有的创建逻辑

对于短信这块能力,一般可以先采购需求,将服务商之间的功能差异进行并集,一次性把这些能力放在抽象工厂,后续这一块就无需再动了,只需要关注具体子工厂下的创建细节了!

聚合功能

上面通过抽象工厂模式方案对不同服务提供商的实体部分进行了聚合,这时就需要根据不同的服务提供商调用它们的 API 了,也就是有不同的实现

基于设计思路,对服务商的能力进行了抽象化,使用策略设计模式来分解不同能力之间的耦合度

短信签名模块:审核签名、删除签名、更新签名、查询签名审核状态

短信模板模块:审核模板、删除模板、更新模板、查询模板审核状态

短信发送模块:发送短信、查询短信发送回执状态

以上三个模块的功能基本上服务商都有提供,这部分功能就是公共能力了,只是每个服务商它具体调用方式不同,从而将公共能力抽取出来,放在 AbstractSMSService 中体现,同时搭配 聚合实体 中提到的抽象实体一起使用,具体能力的实现交由子类去自行实现

对于能力这块,当调用服务商能力出现异常或增强时,只需要对具体的类进行修改,同时支持引入更多的短信服务商,适配能力

聚合实例

当调用短信服务商能力,要创建一个客户端实例与之通信,为了避免我们程序内部资源的开销,每次调用都创建一个实例,进行重复的操作,将实例通过安全单例设计模式实现,有几种:双重检测-单例懒汉式、静态内部类-单例懒汉式、枚举单例

博主是通过,静态内部类-单例懒汉式实现的:由 JVM 保证单例,加载外部类时不会加载内部类,这样可以实现懒加载

源码

前提改造方法讲解完了,这个时候就到了具体的代码实操实现过程

由于一个企业只会用到其中一种短信服务商来进行短信下发动作,此时就需要从 Bean 这方面进行约束只能存在一个,那么就可以通过 Nacos 配置以及搭配 @Condition 注解来完成,贴出部分源码

/**
 * sms.provider.service=TencentCloud 注入 Bean:TencentCloudSMSFactory、TencentCloudSMSServiceImpl
 * @author vnjohn
 * @since 2023/3/17
 */
public class TencentCloudOnCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String propertyVal = context.getEnvironment().getProperty(Constants.SMS_CLOUD_PROVIDER_PROPERTIES);
        return null != propertyVal && propertyVal.equals(SMSCloudProviderEnum.TENCENT_CLOUD.getCode());
    }
}
/**
 * sms.provider.service=AliCloud 注入 Bean:AliCloudSMSFactory、AliCloudSMSServiceImpl
 * @author vnjohn
 * @since 2023/3/17
 */
public class AliCloudOnCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String propertyVal = context.getEnvironment().getProperty(Constants.SMS_CLOUD_PROVIDER_PROPERTIES);
        return null != propertyVal && propertyVal.equals(SMSCloudProviderEnum.ALI_CLOUD.getCode());
    }
}

抽象工厂模式

抽象工厂以及它对应的工厂子类,通过 @Condition 注解确保容器中只会存在一个抽象工厂实现类,以审核签名为例

/**
 * @author vnjohn
 * @since 2023/3/17
 */
public abstract class AbstractSMSFactory {
    /**
     * 创建待审核签名实体
     *
     * @param <T>
     * @return
     */
    public abstract <T extends AbstractSMSSign> AbstractSMSSign createApplySign(ApplySignDTO applySignDTO);
    // 省略其他代码 .......
} 
@Component
@Conditional(AliCloudOnCondition.class)
public class AliCloudSMSFactory extends AbstractSMSFactory {
    @Override
    public <T extends AbstractSMSSign> AliApplyOrModifySign createApplySign(ApplySignDTO applySignDTO) {
        AliSignSourceEnum signSourceEnum = checkAliSignSource(source);
        AliSignTypeEnum signTypeEnum = checkAliSignType(type);
        String fileContent = processFileContent(file);
        return AliApplyOrModifySign.builder()
                                   .name(name)
                                   .source(signSourceEnum.getOutCode())
                                   .type(signTypeEnum.getCode())
                                   .fileList(fileContent)
                                   .remark(remark)
                                   .build();
    }
    // 省略其他代码 .......
}
@Component
@Conditional(TencentCloudOnCondition.class)
public class TencentCloudSMSFactory extends AbstractSMSFactory {
    @Override
    public <T extends AbstractSMSSign> AbstractSMSSign createApplySign(ApplySignDTO applySignDTO) {
        TencentSMSTypeEnum signTypeEnum = checkTencentSignType(applySignDTO.getType());
        TencentDocumentTypeEnum certificationTypeEnum = checkCertificationType(applySignDTO.getCertificationType());
        TencentSignSourceEnum signSourceEnum = checkTencentSignSource(applySignDTO.getSource(), certificationTypeEnum);
        String imageBase64 = CertificationFileUtil.encryptFileToBase64(applySignDTO.getFile());
        return TencentApplyOrModifySign.builder()
                                       .name(applySignDTO.getName())
                                       .documentType(certificationTypeEnum.getCode())
                                       .purpose(applySignDTO.getPurpose())
                                       .type(signTypeEnum.getOutCode())
                                       .proofImage(imageBase64)
                                       .remark(applySignDTO.getRemark())
                                       .source(signSourceEnum.getOutCode())
                                       .build();
    }
    // 省略其他代码 .......
}

号外号外,当前在这里你可以自己任意扩展实现其他云服务,只需要继承抽象工厂类,然后在创建对应该云服务下的功能实体,继承至抽象能力实体类即可!

策略模式

抽象短信服务类以及它对应的短信服务子类,通过 @Condition 注解确保容器中只会存在一个抽象短信服务实现类,以审核签名为例

/**
 * 抽象短信服务公共能力
 *
 * @author vnjohn
 * @since 2023/3/17
 */
public abstract class AbstractSMSService {
    /**
     * 申请签名
     *
     * @param applySmsSign
     * @param <T>
     */
    public abstract <T extends AbstractSMSSign> String applySign(AbstractSMSSign applySmsSign);
    // 省略其他代码 .......
}
@Slf4j
@Component
@Conditional(AliCloudOnCondition.class)
public class AliCloudSMSServiceImpl extends AbstractSMSService {
  @Override
    public <T extends AbstractSMSSign> String applySign(AbstractSMSSign applySmsSign) {
        AliApplyOrModifySign applySign = (AliApplyOrModifySign) applySmsSign;
        AddSmsSignRequest addSmsSignRequest = applySign.toApplySmsSignRequest();
        try {
            AddSmsSignResponse applySmsSignResponse = getInstance().addSmsSign(addSmsSignRequest);
            log.info("apply ali sign,request【{}】,response【{}】", JacksonUtils.toJson(addSmsSignRequest), JacksonUtils.toJson(applySmsSignResponse));
            processMessageByCode(applySmsSignResponse.getBody().getCode(), applySmsSignResponse.getBody().getMessage());
            return applySmsSignResponse.getBody().getSignName();
        } catch (TeaException teaException) {
            log.error("Ali applySign teaException:{}", teaException.getMessage());
            throw new SmsBusinessException("apply ali sign fail");
        } catch (Exception e) {
            log.error("Ali applySign Exception:{}", e.getMessage());
            throw new SmsBusinessException(e.getMessage());
        }
    }
    // 省略其他代码 .......
}
@Slf4j
@Component
@Conditional(TencentCloudOnCondition.class)
public class TencentCloudSMSServiceImpl extends AbstractSMSService {
  @Override
    public <T extends AbstractSMSSign> String applySign(AbstractSMSSign applySmsSign) {
        TencentApplyOrModifySign applySign = (TencentApplyOrModifySign) applySmsSign;
        AddSmsSignRequest addSmsSignRequest = applySign.toAddSmsSignRequest();
        try {
            AddSmsSignResponse applySmsSignResponse = getInstance().AddSmsSign(addSmsSignRequest);
            log.info("apply tencent sign,request【{}】,response【{}】", JacksonUtils.toJson(addSmsSignRequest), JacksonUtils.toJson(applySmsSignResponse));
            return String.valueOf(applySmsSignResponse.getAddSignStatus().getSignId());
        } catch (TencentCloudSDKException sdkException) {
            log.error("Tencent applySign sdkException:{}", sdkException.getMessage());
            processMessageByCode(sdkException.getErrorCode());
        } catch (Exception e) {
            log.error("Tencent applySign Exception:{}", e.getMessage());
            throw new SmsBusinessException(e.getMessage());
        }
        return null;
    }
    // 省略其他代码 .......
}

号外号外,当前在这里你可以自己任意扩展实现其他云服务,只需要继承抽象短信服务类,然后在短信服务实现类写入 API 逻辑!

单例模式

通过阿里短信云、腾讯短信云 Open API 对接服务,客户端以单例的方式进行调用,源码如下:

核心参数配置类,如下:

@Data
@Component
@RefreshScope
public class SMSCloudProviderConfig {
    @Value("${sms.provider.ali.access-key}")
    private String aliAccessKey;
    @Value("${sms.provider.ali.secret}")
    private String aliSecret;
    @Value("${sms.provider.ali.endpoint:dysmsapi.aliyuncs.com}")
    private String aliEndPoint;
    @Value("${sms.provider.tencent.access-key}")
    private String tencentAccessKey;
    @Value("${sms.provider.tencent.secret}")
    private String tencentSecret;
    @Value("${sms.provider.tencent.endpoint:sms.tencentcloudapi.com}")
    private String tencentEndPoint;
    @Value("${sms.provider.tencent.region:ap-guangzhou}")
    private String tencentRegion;
}

阿里云

private static String END_POINT;
    private static String ACCESS_KEY;
    private static String SECRET;
    @PostConstruct
    public void init() {
        ACCESS_KEY = smsProviderConfig.getAliAccessKey();
        SECRET = smsProviderConfig.getAliSecret();
        END_POINT = smsProviderConfig.getAliEndPoint();
    }
    /**
     * 调用阿里云客户端-确保安全单例模式
     */
    private static final class SingletonClientHolder {
        static Client SINGLETON_CLIENT = null;
        static {
            try {
                SINGLETON_CLIENT = new Client(new Config().setAccessKeyId(ACCESS_KEY).setAccessKeySecret(SECRET).setEndpoint(END_POINT));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public static Client getInstance() {
        return SingletonClientHolder.SINGLETON_CLIENT;
    }

腾讯云

private static String ACCESS_KEY;
    private static String SECRET;
    private static String END_POINT;
    private static String REGION;
  @PostConstruct
    public void init() {
        ACCESS_KEY = smsProviderConfig.getTencentAccessKey();
        SECRET = smsProviderConfig.getTencentSecret();
        END_POINT = smsProviderConfig.getTencentEndPoint();
        REGION = smsProviderConfig.getTencentRegion();
    }
    /**
     * 调用腾讯云客户端-确保安全单例模式
     */
    private static final class SingletonClientHolder {
        static SmsClient SINGLETON_CLIENT = null;
        static {
            try {
                Credential cred = new Credential(ACCESS_KEY, SECRET);
                HttpProfile httpProfile = new HttpProfile();
                httpProfile.setEndpoint(END_POINT);
                // 实例化要请求产品的 client 对象,clientProfile 是可选的
                SINGLETON_CLIENT= new SmsClient(cred, REGION);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    public static SmsClient getInstance() {
        return SingletonClientHolder.SINGLETON_CLIENT;
    }

总结

基于阿里云 OpenApi 2.x、腾讯云 OpenApi 3.0 接口实现,对短信网关服务支持可扩展、解耦合,在实际业务场景中,通过几大原则以及几种设计模式对短信网关服务进行了重构

初衷为了回顾一下六大设计原则以及设计模式,其实重构的设计也从中运用了几大原则,如下:

  • 开闭原则:对扩展开放,对修改关闭,但不意味着不做任何的修改;对其他的短信网关可支持动态扩展
  • 单一职责原则:一个接口或类只有一个原因引起变化,也就是一个接口或类只有一个职责,它就负责一件事情;比如,阿里云短信服务实现类、腾讯云短信服务类
  • 里氏替换原则:所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生错误和异常;比如,抽象实体,在短信服务实现类中能够隐式转换为子类
  • 迪米特法则(最少知识法则):一个对象应该对其他对象有最少的了解。通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,你(被耦合或调用的类)内部时如何复杂都和我没关系,那是你的事情,我就知道你提供的这么多 public 方法,我就调用这么多,其他的我一概不关心;比如,将能力抽取出来放在抽象类中,调用方只支持对这些能力进行调用

关于源码:GitHub 短信服务网关开源项目,博主以开源的方式放在 GitHub 中,希望能得到你的支持,对于该部分源码,博主有进行整体的单元测试,大家可以基于公司的业务进行接入,能够帮助到你是我最大的快乐!

博主在业余时间会进行 README.md 文档优化(方便快速引入)、代码结构优化以及支持其他云的扩展实现,大家有什么问题可以在底下评论或私信留言

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!


目录
相关文章
|
4月前
|
监控 负载均衡 Java
深入理解Spring Cloud中的服务网关
深入理解Spring Cloud中的服务网关
|
1月前
|
安全 5G 网络性能优化
|
2月前
|
监控 负载均衡 安全
微服务(五)-服务网关zuul(一)
微服务(五)-服务网关zuul(一)
|
3月前
|
运维 Kubernetes 安全
利用服务网格实现全链路mTLS(一):在入口网关上提供mTLS服务
阿里云服务网格(Service Mesh,简称ASM)提供了一个全托管式的服务网格平台,兼容Istio开源服务网格,用于简化服务治理,包括流量管理和拆分、安全认证及网格可观测性,有效减轻开发运维负担。ASM支持通过mTLS提供服务,要求客户端提供证书以增强安全性。本文介绍如何在ASM入口网关上配置mTLS服务并通过授权策略实现特定用户的访问限制。首先需部署ASM实例和ACK集群,并开启sidecar自动注入。接着,在集群中部署入口网关和httpbin应用,并生成mTLS通信所需的根证书、服务器证书及客户端证书。最后,配置网关上的mTLS监听并设置授权策略,以限制特定客户端对特定路径的访问。
130 2
|
11天前
|
负载均衡 Java 应用服务中间件
Gateway服务网关
Gateway服务网关
24 1
Gateway服务网关
|
1月前
|
前端开发 Java API
vertx学习总结5之回调函数及其限制,如网关/边缘服务示例所示未来和承诺——链接异步操作的简单模型响应式扩展——一个更强大的模型,特别适合组合异步事件流Kotlin协程
本文是Vert.x学习系列的第五部分,讨论了回调函数的限制、Future和Promise在异步操作中的应用、响应式扩展以及Kotlin协程,并通过示例代码展示了如何在Vert.x中使用这些异步编程模式。
47 5
vertx学习总结5之回调函数及其限制,如网关/边缘服务示例所示未来和承诺——链接异步操作的简单模型响应式扩展——一个更强大的模型,特别适合组合异步事件流Kotlin协程
|
2月前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
405 37
|
1月前
|
设计模式 SQL 安全
PHP中的设计模式:单例模式的深入探索与实践在PHP开发领域,设计模式是解决常见问题的高效方案集合。它们不是具体的代码,而是一种编码和设计经验的总结。单例模式作为设计模式中的一种,确保了一个类仅有一个实例,并提供一个全局访问点。本文将深入探讨单例模式的基本概念、实现方式及其在PHP中的应用。
单例模式在PHP中的应用广泛,尤其在处理数据库连接、日志记录等场景时,能显著提高资源利用率和执行效率。本文从单例模式的定义出发,详细解释了其在PHP中的不同实现方法,并探讨了使用单例模式的优势与注意事项。通过对示例代码的分析,读者将能够理解如何在PHP项目中有效应用单例模式。
|
2月前
|
Rust API Go
API 网关 OpenID Connect 实战:单点登录(SSO)如此简单
单点登录(SSO)可解决用户在多系统间频繁登录的问题,OIDC 因其标准化、简单易用及安全性等优势成为实现 SSO 的优选方案,本文通过具体步骤示例对 Higress 中开源的 OIDC Wasm 插件进行了介绍,帮助用户零代码实现 SSO 单点登录。
|
2月前
|
测试技术 微服务
微服务(八)-服务网关zuul(四)
微服务(八)-服务网关zuul(四)