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\"}")
    		);
    	}
    
    }
相关推荐
带刺的坐椅2 天前
SpringBoot3 使用 SolonMCP 开发 MCP
java·ai·springboot·solon·mcp
LUCIAZZZ3 天前
JVM之虚拟机运行
java·jvm·spring·操作系统·springboot
堕落年代3 天前
SpringSecurity当中的CSRF防范详解
前端·springboot·csrf
一零贰肆3 天前
深入理解SpringBoot中的SpringCache缓存技术
java·springboot·springcache·缓存技术
LUCIAZZZ4 天前
JVM之内存管理(一)
java·jvm·spring·操作系统·springboot
LUCIAZZZ6 天前
JVM之内存管理(二)
java·jvm·后端·spring·操作系统·springboot
天上掉下来个程小白7 天前
缓存套餐-01.Spring Cache入门案例
java·redis·spring·缓存·springboot·springcache
程序员buddha9 天前
SpringBoot+Dubbo+Zookeeper实现分布式系统步骤
分布式·zookeeper·dubbo·springboot
天上掉下来个程小白9 天前
缓存套餐-03.功能测试
redis·缓存·springboot·苍穹外卖·springcache
Ten peaches10 天前
苍穹外卖(订单状态定时处理、来单提醒和客户催单)
java·数据库·sql·springboot