背景
博主通过在工作中的总结,接到过各种不同短信提供商的短信管理功能,一旦涉及到 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,后续会有更多实战、源码、架构干货分享!
大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!