mchgood / multiple-sms-spring-boot

Multiple SMS Templates & Multiple SMS Providers for spring-boot, You can easily use multiple-sms-spring-boot-starter to send SMS for users by @SMS annotation and SmsTemplat in you projects.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

multiple-sms-spring-boot-starter

Apache License, Version 2.0, January 2004 Maven Central

多短信模板&多短信供应商, 可拔插,可拓展,支持使用者在同一个项目中指定注解以使用不同的短信模板实现,本项目已提交至maven**仓库,你可以直接在项目pom.xml中引入使用, 找个最新版引入坐标即可使用:

<dependency>
  <groupId>io.github.weasley-j</groupId>
  <artifactId>multiple-sms-spring-boot-starter</artifactId>
  <version>${multiple-email.verison}</version>
</dependency>

multiple-sms-spring-boot-starter启动器解决的事:

如何使用一个注解@SMS和一个模板类SmsTemplate在多个短信供应商多个短信模板之间优雅的切换

requirement

item requirement remark
SpringBoot 2.2.0.RELEASE <= version <= 3.0.0-M3
JDK JDK1.8 or latest
Environment Spring Web Application

project tree map

image-20220328232832302

1 故事背景

我们先来看一张笔者一朋友张三所在的公司的短信模板图:

image-20210929173756644

张三的苦恼是每新开一个业务模块他都要把之前发送短信的业务代码CV一遍,有没有优雅的一种方式呢?接下来,我们一同交流下这种问题。

通常规模稍大点的公司的业务板块比较多,对应的则是每一种业务的短信消息通知,通常情况下,短信通知分为三类:

  1. 验证码通知
  2. 营销通知
  3. 内容通知

从以上图片中张三所在的公司短信消息的模板已达到9个,传统的XxxUtilsCV编码方式会大大增加代码的重复率,基于SpringBoot极高的可拓展性,我们可以有更优雅的编码方式。

2 controller使用效果

import cn.alphahub.multiple.sms.SmsTemplate;
import cn.alphahub.multiple.sms.annotation.EnableMultipleSmsSupport;
import cn.alphahub.multiple.sms.annotation.SMS;
import cn.alphahub.multiple.sms.enums.SmsSupplier;
import cn.alphahub.multiple.sms.demo.MyCustomSmsClientDemoImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

import static cn.alphahub.multiple.sms.SmsTemplate.SmsParam;

/**
 * SMS Service Test Controller
 * <p>多短信供应商、多短信模板示例Controller</p>
 *
 * @author lwj
 * @version 1.0
 * @date 2021-09-29 14:14
 */
@Slf4j
@RestController
@RequestMapping("/sms/support/demo")
@ConditionalOnBean(annotation = {EnableMultipleSmsSupport.class})
public class SmsServiceDemoController {

    @Autowired
    private SmsTemplate smsTemplate;

    /**
     * 使用默认短信模板发送短信
     * <p>默认模板可以注解{@code @SMS},也可以不加</p>
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @PostMapping("/sendWithDefaultTemplate")
    public Object sendWithDefaultTemplate(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 使用阿里云短信模板1发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(name = "促销短信模板")
    @PostMapping("/sendWithAliCloud1")
    public Object sendWithAliCloud1(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 使用阿里云短信模板2发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(name = "秒杀短信模板")
    @PostMapping("/sendWithAliCloud2")
    public Object sendWithAliCloud2(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 使用华为云发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(name = "验证码短信模板", supplier = SmsSupplier.HUAWEI)
    @PostMapping("/sendWithHuaweiCloud")
    public Object sendWithHuaweiCloud(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 使用京东云发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(name = "京东云短信验证码模板", supplier = SmsSupplier.JINGDONG)
    @PostMapping("/sendWithJingdongCloud")
    public Object sendWithJingdongCloud(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 使用七牛云发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(name = "验证码短信模板", supplier = SmsSupplier.QINIU)
    @PostMapping("/sendWithQiniuCloud")
    public Object sendWithQiniuCloud(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 使用腾讯云发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(name = "内容短信模板", supplier = SmsSupplier.TENCENT)
    @PostMapping("/sendWithTencentCloud")
    public Object sendWithTencentCloud(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 自定义短信实现发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(invokeClass = MyCustomSmsClientDemoImpl.class)
    @PostMapping("/sendCustomSmsClient")
    public Object sendWithCustomSmsClient(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }
}

说明:

  • 上面上8个发送短信消息的方法都是调用SmsTemplate类的send(SmsParam smsParam)方法
  • 通过注解 @SMS动态切换短信模板
  • 8个方法支持自定义短信实现,通过注解@SMS(invokeClass = MyCustomSmsClientDemoImpl.class)指定自定义短信实现类
  • 通过注解@SMS同一短信提供商的多个短信模板不同短信提供上商多个短信模板之间自由切换短信模板
  • 发送短信的方法只有一个send()方法
  • 支持的短信提供商有5家:阿里云、腾讯云、华为云、京东云、七牛云

3 细节分享

3.1 配置文件

提示:同一短信供应商下面的多个模板的模板名称(template-name)不能重复。

以下是阿里云腾讯云华为云京东云七牛云等短信模板的配置示例,包含模板情景:同一短信供应商多个短信模板、多个短信供应商多个模板

spring:
  sms:
    #默认线程池配置,一下参数是默认值,可根据自己的业务调整大小
    thread:
      core-pool-size: 50
      maximum-pool-size: 200
      keep-alive-time: 10
      time-unit: seconds
      capacity: 2000
    #默认短信模板配置,短信模板名称, 默认: DEFAULT, 无需修改
    template-name: DEFAULT
    #默认短信供应商,大小写不敏感,可选值: ali, huawei, jingdong, qiniu, tencent
    sms-supplier: ALI
    #短信配置
    sms-properties:
      #短信access-key
      access-key: accessKey1
      #短信secret-key
      secret-key: secretKey1
      #区域
      region-id: regionId1
      #短信签名
      sign-name: signName1
      #短信模板code、短信模板id
      template-code: templateCode1
      #短信sdk的appId,有就填,没有留空,如:腾讯云需要、阿里云不需要
      app-id: null
    #多供应商、多短信模块配置
    multi-sms-templates:
      template-properties:
        - template-name: "促销短信模板"
          sms-supplier: ALI
          sms-properties:
            access-key: accessKey1
            secret-key: secretKey1
            region-id: regionId1
            sign-name: signName1
            template-code: templateCode1
        - template-name: "秒杀短信模板"
          sms-supplier: ALI
          sms-properties:
            access-key: accessKey1
            secret-key: secretKey1
            region-id: regionId1
            sign-name: signName1
            template-code: templateCode1
        - template-name: "验证码短信模板"
          sms-supplier: HUAWEI
          sms-properties:
            access-key: accessKey2
            secret-key: secretKey2
            region-id: regionId2
            sign-name: signName2
            template-code: templateCode2
            app-id: your-app-id
        - template-name: "京东云短信验证码模板"
          sms-supplier: JINGDONG
          sms-properties:
            access-key: your-ak
            secret-key: your-sk
            region-id: cn-north-1
            sign-name: your-sign-name
            template-code: your-template-id
        - template-name: "验证码短信模板"
          sms-supplier: QINIU
          sms-properties:
            access-key: accessKey4
            secret-key: secretKey4
            region-id: regionId4
            sign-name: signName4
            template-code: templateCode4
        - template-name: "腾讯云内容短信模板"
          sms-supplier: TENCENT
          sms-properties:
            access-key: accessKey4
            secret-key: secretKey4
            region-id: "ap-nanjing"
            sign-name: "xxx的个人主页"
            template-code: 1141766
            app-id: "your-sms-sdk-app-id"

3.2 注解说明

3.2.1 业务注解@SMS

import cn.alphahub.multiple.sms.SmsClient;
import cn.alphahub.multiple.sms.enums.SmsSupplier;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import static cn.alphahub.multiple.sms.SmsClient.DefaultSmsClientPlaceholder;

/**
 * 多模板短信注解
 *
 * @author lwj
 * @version 1.0
 * @apiNote 基于此注解解析不同的短信模板, 使用注解{@code @SMS}指定以:短信供应商、短信模板发送短信
 * @date 2021-09-24
 */
@Documented
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface SMS {

    /**
     * 默认模板名称
     */
    String DEFAULT_TEMPLATE = "DEFAULT";

    /**
     * 短信模板名称,必须和{@code yml}配置文件里面的{@code template-name}一致,默认:DEFAULT
     *
     * @return 短信模板名称
     */
    String name() default DEFAULT_TEMPLATE;

    /**
     * 短信供应商,默认短信供应商: 阿里云
     *
     * @apiNote 如果需要拓展其他短信供应商,见枚举{@code SmsSupplier}
     * @see SmsSupplier
     */
    SmsSupplier supplier() default SmsSupplier.ALI;

    /**
     * 自定义实现发送发送短信的实现类,必须显现或继承{@code SmsClient}接口
     *
     * @return 发送短信的实现类class
     * @apiNote 当指定自定义短信发送类时将优先采用自定义短信发送实现完成发送短信的逻辑
     */
    Class<? extends SmsClient> invokeClass() default DefaultSmsClientPlaceholder.class;
}

@SMS可作用于方法上, 支持自定义发送短信逻辑实现,通过invokeClass指定自定义实现类。

3.2.2 自动配置注解@EnableMultipleSmsSupport

import cn.alphahub.multiple.sms.SmsTemplate;
import cn.alphahub.multiple.sms.aspect.SmsAspect;
import cn.alphahub.multiple.sms.config.SmsConfig;
import org.springframework.context.annotation.Import;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Enable Multiple SMS Support
 *
 * @author lwj
 */
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import({SmsConfig.class, SmsAspect.class, SmsTemplate.class})
public @interface EnableMultipleSmsSupport {

}

注解@EnableMultipleSmsSupport作用于上,用于需要发送短信的web应用启用短信支持,并自动装配配置文件,只需要在发送短信的服务的yml配置文件中配置3.1的短信AKSK 等数据即可,

3.3 短信提供商

目前只整合一下5种短信供应商的短信实现,读者可以自行增加其他短信产商的发送短信的实现

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.stream.Stream;

/**
 * 短信供应商类型枚举枚举
 *
 * @author lwj
 * @version 1.0
 * @date 2021-09-24
 */
@Getter
@AllArgsConstructor
public enum SmsSupplier {
    /**
     * 阿里云
     */
    ALI("ALI_CLOUD", "阿里云"),
    /**
     * 华为云
     */
    HUAWEI("HUAWEI_CLOUD", "华为云"),
    /**
     * 京东云
     */
    JINGDONG("JINGDONG_CLOUD", "京东云"),
    /**
     * 七牛云
     */
    QINIU("QINIU_CLOUD", "七牛云"),
    /**
     * 腾讯云
     */
    TENCENT("TENCENT_CLOUD", "腾讯云"),
    ;

    /**
     * 供应商名称编码
     */
    private final String code;
    /**
     * 供应商名称
     */
    private final String name;

    /**
     * 获取短信供应商枚举
     *
     * @param code 供应商名称编码
     * @return 短信供应商枚举
     */
    public static SmsSupplier getEnum(String code) {
        return Stream.of(SmsSupplier.values())
                .filter(smsSupplier -> smsSupplier.getCode().equals(code))
                .findFirst().orElse(null);
    }
}

3.4 自定义短信发送实现

自定义短信发送实现需实现cn.alphahub.multiple.sms.SmsClient接口并覆写send方法。

image-20211008174003086

3.5 自动装配使用说明

步骤:

  1. 新建一个springbootweb项目, pom.xml中引入multiple-sms-spring-boot-startermaven坐标:
<!-- multiple-sms-spring-boot-starter -->
<dependency>
    <groupId>io.github.weasley-j</groupId>
    <artifactId>multiple-sms-spring-boot-starter</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>
  1. 在该服务的启动类上标注注解@EnableMultipleSmsSupport启用短信支持

这是从我另一个项目 里面拆离的基础组件,注解是使用本项目的的注解是: @EnableMultipleSmsSupport

  1. ServiceController层注入SmsTemplate,并在业务方法或业务类上标注注解@SMS 发送短信,详见源码cn.alphahub.multiple.sms.test.controller.SmsServiceDemoController

推荐注解@SMS作用于方法,方法级别使用。

image-20211008175303335

4 关于注解@SMS作用在类和方法的优先级问题

  • 当注解@SMS同时作用类,和方法上时,方法上注解@SMS的优先级高于类上@SMS注解的优先级
  • 当注解@SMS作用方法上时,该方法短信客户端的为注解@SMS指定的短信客户端
  • 当注解@SMS作用类上时,该类所有短信模板方法发送短信的客户端都以注解@SMS指定为准客户端

5 关于Spring IOC容器中的同一个Bean实例里面被@SMS注解标注的方法间嵌套调用的问题

5.1 我们先看一个AOP嵌套调用的示例类:SMSAnnotateWithClassAndMethod

import cn.alphahub.multiple.sms.SmsTemplate;
import cn.alphahub.multiple.sms.annotation.SMS;
import cn.alphahub.multiple.sms.demo.MyCustomSmsClientDemoImpl;
import cn.alphahub.multiple.sms.enums.SmsSupplier;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestBody;

import javax.annotation.Resource;

import static cn.alphahub.multiple.sms.SmsTemplate.SmsParam;

/**
 * 测试{@code @SMS}同时标注在方法类、类方法上面
 *
 * @author lwj
 * @version 1.0
 * @date 2021-10-02 18:18
 */
@SMS
@Service
public class SMSAnnotateWithClassAndMethod {

    @Autowired
    private SmsTemplate smsTemplate;

    /**
     * 使用腾讯云发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(name = "腾讯云内容短信模板", supplier = SmsSupplier.TENCENT)
    public Object sendWithTencentCloud(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 自定义短信实现发送短信
     *
     * @param smsParam 短信参数
     * @return 发送结果
     */
    @SMS(invokeClass = MyCustomSmsClientDemoImpl.class)
    public Object sendWithCustomSmsClient(@RequestBody SmsParam smsParam) {
        return smsTemplate.send(smsParam);
    }

    /**
     * 嵌套调用,使用Spring AOP代理当前对象
     *
     * @param smsParam 短信参数
     */
    public void nested(@RequestBody SmsParam smsParam) {
        ((SMSAnnotateWithClassAndMethod) AopContext.currentProxy()).sendWithTencentCloud(smsParam);
        ((SMSAnnotateWithClassAndMethod) AopContext.currentProxy()).sendWithCustomSmsClient(smsParam);
    }
}

以上多模板短信实例代码的应用场景:

当用户在乐璟商城下单成功后,平台方应当发送消息通知给用户商家,告知用户物流情况,告知商家本单交易情况。

假设sendWithTencentCloud()是告诉用户的短信模板消息,sendWithCustomSmsClient()是告诉商家的短信模板消息,为了完成以上业务场景,我们写了nested()方法来完成这个业务场景。

5.2 结论分享

  • nested()方法一共调用两个本类里面被注解@SMS标注的方法完成我们的业务需求:“当用户在乐璟商城下单成功后,平台方应当发送消息通知给用户商家,告知用户物流情况,告知商家本单交易情况”,
  • 基于CGLib的动态代理调用本类方法完成业务时,使用的是增强代理类去执行业务方法,并不是本类this自身,因此我们需要从线程变量中获取当前AOP的真实代理对象,让真实代理对象调用本类(this )的方法执行业务,执行前后AOP能根据我们设定好的代理规则解析正确的业务参数完成业务需求。

About

Multiple SMS Templates & Multiple SMS Providers for spring-boot, You can easily use multiple-sms-spring-boot-starter to send SMS for users by @SMS annotation and SmsTemplat in you projects.

License:Apache License 2.0


Languages

Language:Java 99.7%Language:Shell 0.3%