SpringBoot+SMS(短信)

  1. pom.xml

    <!-- https://mvnrepository.com/artifact/com.aliyun/dysmsapi20170525 -->
    <dependency>
        <groupId>com.aliyun</groupId>
        <artifactId>dysmsapi20170525</artifactId>
        <version>2.0.23</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/com.tencentcloudapi/tencentcloud-sdk-java-sms -->
    <dependency>
        <groupId>com.tencentcloudapi</groupId>
        <artifactId>tencentcloud-sdk-java-sms</artifactId>
        <version>3.1.810</version>
    </dependency>
    
    <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.20</version>
    </dependency>
    
  2. application.yml

    server:
      port: 8888
      servlet:
        context-path: /sms
    
    --- # sms 短信
    sms:
      enabled: true
      # 阿里云 dysmsapi.aliyuncs.com
      # 腾讯云 sms.tencentcloudapi.com
      endpoint: "dysmsapi.aliyuncs.com"
      # 阿里云 https://ram.console.aliyun.com/manage/ak
      # 腾讯云 https://console.cloud.tencent.com/cam/capi
      accessKeyId: xxxxxxxxx
      accessKeySecret: xxxxxxxxx
      # 阿里云 https://dysms.console.aliyun.com/domestic/text
      # 腾讯云 https://console.cloud.tencent.com/smsv2/csms-sign
      signName: xxxxxxxxx
      # 腾讯专用 https://console.cloud.tencent.com/cam/capi
      sdkAppId: xxxxxxxxx
    
  3. 属性类

    @Data
    @Component
    @ConfigurationProperties(prefix = "sms")
    public class SmsProperties {
    
        private Boolean enabled;
    
        /**
         * 配置节点
         * 阿里云 dysmsapi.aliyuncs.com
         * 腾讯云 sms.tencentcloudapi.com
         */
        private String endpoint;
    
        /**
         * key
         */
        private String accessKeyId;
    
        /**
         * 密匙
         */
        private String accessKeySecret;
    
        /*
         * 短信签名
         */
        private String signName;
    
        /**
         * 短信应用ID (腾讯专属)
         */
        private String sdkAppId;
    
    }
    
  4. 配置类

    @Configuration
    public class SmsConfig {
    
        @Configuration
        @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
        @ConditionalOnClass(com.aliyun.dysmsapi20170525.Client.class)
        static class AliyunSmsConfig {
    
            @Bean
            public SmsTemplate aliyunSmsTemplate(SmsProperties smsProperties) {
                return new AliyunSmsTemplate(smsProperties);
            }
    
        }
    
        @Configuration
        @ConditionalOnProperty(value = "sms.enabled", havingValue = "true")
        @ConditionalOnClass(com.tencentcloudapi.sms.v20190711.SmsClient.class)
        static class TencentSmsConfig {
    
            @Bean
            public SmsTemplate tencentSmsTemplate(SmsProperties smsProperties) {
                return new TencentSmsTemplate(smsProperties);
            }
    
        }
    
    }
    
  5. 接口类

    public interface SmsTemplate {
    
        /**
         * 发送短信
         *
         * @param phones     电话号(多个逗号分割)
         * @param templateId 模板id
         * @param param      模板对应参数
         *                   阿里 需使用 模板变量名称对应内容 例如: code=1234
         *                   腾讯 需使用 模板变量顺序对应内容 例如: 1=1234, 1为模板内第一个参数
         */
        SmsResult send(String phones, String templateId, Map<String, String> param);
    
    }
    
  6. 实现类(阿里云)

    public class AliyunSmsTemplate implements SmsTemplate {
    
        private final SmsProperties properties;
    
        private final Client client;
    
        @SneakyThrows(Exception.class)
        public AliyunSmsTemplate(SmsProperties smsProperties) {
            this.properties = smsProperties;
            Config config = new Config()
                // 您的AccessKey ID
                .setAccessKeyId(smsProperties.getAccessKeyId())
                // 您的AccessKey Secret
                .setAccessKeySecret(smsProperties.getAccessKeySecret())
                // 访问的域名
                .setEndpoint(smsProperties.getEndpoint());
            this.client = new Client(config);
        }
    
        @Override
        public SmsResult send(String phones, String templateId, Map<String, String> param) {
            if (StringUtils.isBlank(phones)) {
                throw new SmsException("手机号不能为空");
            }
            if (StringUtils.isBlank(templateId)) {
                throw new SmsException("模板ID不能为空");
            }
            SendSmsRequest req = new SendSmsRequest()
                .setPhoneNumbers(phones)
                .setSignName(properties.getSignName())
                .setTemplateCode(templateId)
                .setTemplateParam(JsonUtils.toJsonString(param));
            try {
                SendSmsResponse resp = client.sendSms(req);
                return SmsResult.builder()
                    .isSuccess("OK".equals(resp.getBody().getCode()))
                    .message(resp.getBody().getMessage())
                    .response(JsonUtils.toJsonString(resp))
                    .build();
            } catch (Exception e) {
                throw new SmsException(e.getMessage());
            }
        }
    
    }
    
  7. 实现类(腾讯云)

    public class TencentSmsTemplate implements SmsTemplate {
    
        private final SmsProperties properties;
    
        private final SmsClient client;
    
        @SneakyThrows(Exception.class)
        public TencentSmsTemplate(SmsProperties smsProperties) {
            this.properties = smsProperties;
            Credential credential = new Credential(smsProperties.getAccessKeyId(), smsProperties.getAccessKeySecret());
            HttpProfile httpProfile = new HttpProfile();
            httpProfile.setEndpoint(smsProperties.getEndpoint());
            ClientProfile clientProfile = new ClientProfile();
            clientProfile.setHttpProfile(httpProfile);
            this.client = new SmsClient(credential, "", clientProfile);
        }
    
        @Override
        public SmsResult send(String phones, String templateId, Map<String, String> param) {
            if (StringUtils.isBlank(phones)) {
                throw new SmsException("手机号不能为空");
            }
            if (StringUtils.isBlank(templateId)) {
                throw new SmsException("模板ID不能为空");
            }
            SendSmsRequest req = new SendSmsRequest();
            Set<String> set = Arrays.stream(phones.split(",")).map(p -> "+86" + p).collect(Collectors.toSet());
            req.setPhoneNumberSet(ArrayUtil.toArray(set, String.class));
            if (CollUtil.isNotEmpty(param)) {
                req.setTemplateParamSet(ArrayUtil.toArray(param.values(), String.class));
            }
            req.setTemplateID(templateId);
            req.setSign(properties.getSignName());
            req.setSmsSdkAppid(properties.getSdkAppId());
            try {
                SendSmsResponse resp = client.SendSms(req);
                SmsResult.SmsResultBuilder builder = SmsResult.builder()
                    .isSuccess(true)
                    .message("send success")
                    .response(JsonUtils.toJsonString(resp));
                for (SendStatus sendStatus : resp.getSendStatusSet()) {
                    if (!"Ok".equals(sendStatus.getCode())) {
                        builder.isSuccess(false).message(sendStatus.getMessage());
                        break;
                    }
                }
                return builder.build();
            } catch (Exception e) {
                throw new SmsException(e.getMessage());
            }
        }
    
    }
    
  8. 控制器

    @Validated
    @RequiredArgsConstructor
    @RestController
    @RequestMapping("/sms")
    public class SmsController {
    
        private final SmsProperties smsProperties;
    
        private AliyunSmsTemplate aliyunSmsTemplate; // 阿里云
        private TencentSmsTemplate tencentSmsTemplate; // 腾讯云
    
        /**
         * 发送短信Aliyun
         *
         * @param phones     电话号
         * @param templateId 模板ID
         */
        @GetMapping("/sendAliyun")
        public R<Object> sendAliyun(String phones, String templateId) {
            if (!smsProperties.getEnabled()) {
                return R.fail("当前系统没有开启短信功能!");
            }
            if (!SpringUtils.containsBean("aliyunSmsTemplate")) {
                return R.fail("阿里云依赖未引入!");
            }
            AliyunSmsTemplate aliyunSmsTemplate = SpringUtils.getBean(AliyunSmsTemplate.class);
            Map<String, String> map = new HashMap<>(1);
            map.put("code", "1234");
            Object send = aliyunSmsTemplate.send(phones, templateId, map);
            return R.ok(send);
        }
    
        /**
         * 发送短信Tencent
         *
         * @param phones     电话号
         * @param templateId 模板ID
         */
        @GetMapping("/sendTencent")
        public R<Object> sendTencent(String phones, String templateId) {
            if (!smsProperties.getEnabled()) {
                return R.fail("当前系统没有开启短信功能!");
            }
            if (!SpringUtils.containsBean("tencentSmsTemplate")) {
                return R.fail("腾讯云依赖未引入!");
            }
            TencentSmsTemplate tencentSmsTemplate = SpringUtils.getBean(TencentSmsTemplate.class);
            Map<String, String> map = new HashMap<>(1);
            map.put("1", "1234");
            Object send = tencentSmsTemplate.send(phones, templateId, map);
            return R.ok(send);
        }
    
    }
    
  9. 相关类

    ---------------------------------------结果类---------------------------------------
    @Data
    @Builder
    public class SmsResult {
    
        /**
         * 是否成功
         */
        private boolean isSuccess;
    
        /**
         * 响应消息
         */
        private String message;
    
        /**
         * 实际响应体
         * <p>
         * 可自行转换为 SDK 对应的 SendSmsResponse
         */
        private String response;
    }
    ---------------------------------------异常类---------------------------------------
    public class SmsException extends RuntimeException {
    
        private static final long serialVersionUID = 1L;
    
        public SmsException(String msg) {
            super(msg);
        }
    
    }
    ---------------------------------------工具类---------------------------------------
    @NoArgsConstructor(access = AccessLevel.PRIVATE)
    public class JsonUtils {
    
        private static final ObjectMapper OBJECT_MAPPER = SpringUtils.getBean(ObjectMapper.class);
    
        public static ObjectMapper getObjectMapper() {
            return OBJECT_MAPPER;
        }
    
        public static String toJsonString(Object object) {
            if (ObjectUtil.isNull(object)) {
                return null;
            }
            try {
                return OBJECT_MAPPER.writeValueAsString(object);
            } catch (JsonProcessingException e) {
                throw new RuntimeException(e);
            }
        }
    
        public static <T> T parseObject(String text, Class<T> clazz) {
            if (StringUtils.isEmpty(text)) {
                return null;
            }
            try {
                return OBJECT_MAPPER.readValue(text, clazz);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        public static <T> T parseObject(byte[] bytes, Class<T> clazz) {
            if (ArrayUtil.isEmpty(bytes)) {
                return null;
            }
            try {
                return OBJECT_MAPPER.readValue(bytes, clazz);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        public static <T> T parseObject(String text, TypeReference<T> typeReference) {
            if (StringUtils.isBlank(text)) {
                return null;
            }
            try {
                return OBJECT_MAPPER.readValue(text, typeReference);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        public static Dict parseMap(String text) {
            if (StringUtils.isBlank(text)) {
                return null;
            }
            try {
                return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructType(Dict.class));
            } catch (MismatchedInputException e) {
                // 类型不匹配说明不是json
                return null;
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        public static List<Dict> parseArrayMap(String text) {
            if (StringUtils.isBlank(text)) {
                return null;
            }
            try {
                return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, Dict.class));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        public static <T> List<T> parseArray(String text, Class<T> clazz) {
            if (StringUtils.isEmpty(text)) {
                return new ArrayList<>();
            }
            try {
                return OBJECT_MAPPER.readValue(text, OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, clazz));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
    }
    ---------------------------------------工具类---------------------------------------
    @Component
    public final class SpringUtils extends SpringUtil {
    
        /**
         * 如果BeanFactory包含一个与所给名称匹配的bean定义,则返回true
         *
         * @param name
         * @return boolean
         */
        public static boolean containsBean(String name) {
            return getBeanFactory().containsBean(name);
        }
    
        /**
         * 判断以给定名字注册的bean定义是一个singleton还是一个prototype。
         * 如果与给定名字相应的bean定义没有被找到,将会抛出一个异常(NoSuchBeanDefinitionException)
         *
         * @param name
         * @return boolean
         */
        public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException {
            return getBeanFactory().isSingleton(name);
        }
    
        /**
         * @param name
         * @return Class 注册对象的类型
         */
        public static Class<?> getType(String name) throws NoSuchBeanDefinitionException {
            return getBeanFactory().getType(name);
        }
    
        /**
         * 如果给定的bean名字在bean定义中有别名,则返回这些别名
         *
         * @param name
         */
        public static String[] getAliases(String name) throws NoSuchBeanDefinitionException {
            return getBeanFactory().getAliases(name);
        }
    
        /**
         * 获取aop代理对象
         *
         * @param invoker
         * @return
         */
        @SuppressWarnings("unchecked")
        public static <T> T getAopProxy(T invoker) {
            return (T) AopContext.currentProxy();
        }
    
    }
    ---------------------------------------响应类---------------------------------------
    @Data
    @NoArgsConstructor
    public class R<T> implements Serializable {
        private static final long serialVersionUID = 1L;
    
        /**
         * 成功
         */
        public static final int SUCCESS = 200;
    
        /**
         * 失败
         */
        public static final int FAIL = 500;
    
        private int code;
    
        private String msg;
    
        private T data;
    
        public static <T> R<T> ok() {
            return restResult(null, SUCCESS, "操作成功");
        }
    
        public static <T> R<T> ok(T data) {
            return restResult(data, SUCCESS, "操作成功");
        }
    
        public static <T> R<T> ok(String msg) {
            return restResult(null, SUCCESS, msg);
        }
    
        public static <T> R<T> ok(String msg, T data) {
            return restResult(data, SUCCESS, msg);
        }
    
        public static <T> R<T> fail() {
            return restResult(null, FAIL, "操作失败");
        }
    
        public static <T> R<T> fail(String msg) {
            return restResult(null, FAIL, msg);
        }
    
        public static <T> R<T> fail(T data) {
            return restResult(data, FAIL, "操作失败");
        }
    
        public static <T> R<T> fail(String msg, T data) {
            return restResult(data, FAIL, msg);
        }
    
        public static <T> R<T> fail(int code, String msg) {
            return restResult(null, code, msg);
        }
    
        /**
         * 返回警告消息
         *
         * @param msg 返回内容
         * @return 警告消息
         */
        public static <T> R<T> warn(String msg) {
            return restResult(null, HttpStatus.WARN, msg);
        }
    
        /**
         * 返回警告消息
         *
         * @param msg 返回内容
         * @param data 数据对象
         * @return 警告消息
         */
        public static <T> R<T> warn(String msg, T data) {
            return restResult(data, HttpStatus.WARN, msg);
        }
    
        private static <T> R<T> restResult(T data, int code, String msg) {
            R<T> r = new R<>();
            r.setCode(code);
            r.setData(data);
            r.setMsg(msg);
            return r;
        }
    
        public static <T> Boolean isError(R<T> ret) {
            return !isSuccess(ret);
        }
    
        public static <T> Boolean isSuccess(R<T> ret) {
            return R.SUCCESS == ret.getCode();
        }
    }
    ---------------------------------------状态类---------------------------------------
    public interface HttpStatus {
        /**
         * 操作成功
         */
        int SUCCESS = 200;
    
        /**
         * 对象创建成功
         */
        int CREATED = 201;
    
        /**
         * 请求已经被接受
         */
        int ACCEPTED = 202;
    
        /**
         * 操作已经执行成功,但是没有返回数据
         */
        int NO_CONTENT = 204;
    
        /**
         * 资源已被移除
         */
        int MOVED_PERM = 301;
    
        /**
         * 重定向
         */
        int SEE_OTHER = 303;
    
        /**
         * 资源没有被修改
         */
        int NOT_MODIFIED = 304;
    
        /**
         * 参数列表错误(缺少,格式不匹配)
         */
        int BAD_REQUEST = 400;
    
        /**
         * 未授权
         */
        int UNAUTHORIZED = 401;
    
        /**
         * 访问受限,授权过期
         */
        int FORBIDDEN = 403;
    
        /**
         * 资源,服务未找到
         */
        int NOT_FOUND = 404;
    
        /**
         * 不允许的http方法
         */
        int BAD_METHOD = 405;
    
        /**
         * 资源冲突,或者资源被锁
         */
        int CONFLICT = 409;
    
        /**
         * 不支持的数据,媒体类型
         */
        int UNSUPPORTED_TYPE = 415;
    
        /**
         * 系统内部错误
         */
        int ERROR = 500;
    
        /**
         * 接口未实现
         */
        int NOT_IMPLEMENTED = 501;
    
        /**
         * 系统警告消息
         */
        int WARN = 601;
    }
    
  10. 测试说明

    测试未通过,报错如下,可能版本原因导致,代码仅供学习参考。
    com.example.sms.exception.SmsException: Failed to connect to dysmsapi.aliyuncs.com/127.0.1.2:443
    
    // 简易测试短信发送仍然报同样错误
    @SpringBootApplication
    @RestController
    public class SmsApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(SmsApplication.class, args);
    	}
    
    	@GetMapping("/sendSms")
    	public void sendSms() throws Exception {
    		// 发送短信
    		SendSmsResponse resp = new Client(
    				new Config().setAccessKeyId("xxxxxxxxx")
    						.setAccessKeySecret("xxxxxxxxx")
    						.setEndpoint("dysmsapi.aliyuncs.com")
    		).sendSms(
    				new SendSmsRequest().setSignName("xxxxxxxxx")
    						.setTemplateCode("xxxxxxxxx")
    						.setPhoneNumbers("xxxxxxxxx")
    						.setTemplateParam("{\"code\":\"1234\"}")
    		);
    	}
    
    }
    
相关推荐
潘帕斯的雄鹰7 小时前
【1个月速成Java】基于Android平台开发个人记账app学习日记——第7天,申请阿里云SMS短信服务SDK
java·学习·阿里云·sms·短信服务sdk
东皋长歌21 小时前
SpringBoot+ClickHouse集成
clickhouse·springboot
程序员徐师兄1 天前
基于 JavaWeb 的宠物商城系统(附源码,文档)
java·vue·springboot·宠物·宠物商城
果冻眼企鹅1 天前
常见用于从 HTTP 请求中提取数据的注解
springboot
你白勺男孩TT2 天前
Vue项目中点击按钮后浏览器屏幕变黑,再次点击恢复的解决方法
vue.js·vue·springboot
小云小白2 天前
springboot 传统应用程序,适配云原生改造
云原生·系统架构·k8s·springboot
IT界的奇葩2 天前
MQTT+Disruptor 提高物联网高并发
java·物联网·mqtt·springboot·disruptor
null_173 天前
ruoyi若依vue分离版前端资源打包到jar中
java·vue·springboot
AshCode4 天前
Docker远程管理和应用容器远程部署
docker·springboot·devops·开发效率·容器部署
爱吃土豆的程序员4 天前
深入理解跨域资源共享(CORS)安全问题原理及解决思路
java·springboot·cors·安全问题