集成sms4j修改源码实现发送不带短信模版id的短信


1.前言

1.1 sms4j是什么?

sms4j:让发送短信变得更简单,是一款开源、优雅、开箱即用、切换灵活。功能丰富等特性的发短信的开源好用开源好用的轮子,支持的短信提供商也比较的多,支持的短信提供厂商有:阿里云、容联云、天翼云、亿美软通、华为云短信、京东云短信、 网易云信、腾讯云短信、合一短信、云片短信、助通短信、鼎众短信、联麓短信、七牛云短信 、创蓝短信、极光短信、布丁云V2 、中国移动 云MAS 、百度云短信、螺丝帽短信、SUBMAIL短信、 单米短信;官网上也有详细的说明,本文以华为云短信为例。


1.2 缘由

由于最近项目需要一个发送短信的功能,所以我就选择想集成sms4j这个开源的轮子,其实有其他同事写好的接口可以调用,然后我一对比一看,还是集成sms4j实现比较优雅一点,毕竟不用去调用其他的服务接口,本项目中就直接实现了这个功能,避免了网路接口调用,做到了服务自己的高内聚低耦合,然后就看了下sms4j的官网,也看了sms4j的源码,华为云短信写的还是优雅的,就是不支持不带短信模版id的实现,所以我决定修改sms4j的源码来实现,sms4j的厂商yaml配置差异化的配置都是一个map注入的,具体的可以去看下它的源码,写的还是可以的,每一个短信厂商都有一个单列的工厂类,然后会给每个单列的工厂类生成一个SmsBlend 动态代理类(jdk的动态代理类,代理的对象是具体的短信厂商实现类,比如:HuaweiSmsImpl),然后在短信方法调用之前和之后会有一系列的处理器类,具体的可以去看源码。


1.3 官方地址

https://sms4j.com/https://gitee.com/dromara/sms4jhttps://github.com/dromara/SMS4J
sms4j官方提供了两个版本:2.x和3.x,具体可以参看官方文档,本文使用3.x的版本。


1.4 华为短信接口响应码文档地址

https://support.huaweicloud.com/api-msgsms/sms_05_0050.html#toTop


2.配置

2.1 依赖配置

<dependency>   <groupId>org.dromara.sms4jgroupId>   <artifactId>sms4j-spring-boot-starterartifactId>   <version>3.2.1version>dependency>
版本可以看如下官方说明:
https://sms4j.com/title/log.html

2.2 yaml配置

nacos项目yaml配置如下:
sms:  # 标注从yml读取配置  config-type: yaml  blends:    # 自定义的标识,也就是configId这里可以是任意值(最好不要是中文)    hw1:      # 接入地址      url: https://smsapi.cn-north-4.myhuaweicloud.com:443      # 国内短信签名通道号      sender: xxxxx      # 厂商标识,标定此配置是哪个厂商,详细请看厂商标识介绍部分      supplier: huawei      # 您的accessKey      access-key-id: xxxxx      # 您的accessKeySecret      access-key-secret: xxxxxxx      # 您的短信签名      signature: xxxxxx      # 模板ID 非必须配置,如果使用sendMessage的快速发送需此配置      #template-id: xxxxxxxx      # 您的sdkAppId      sdk-app-id: hw1      # 自定义的标识,也就是configId这里可以是任意值(最好不要是中文)


3.重写

3.1项目中重写如下类

3.2  HuaweiSmsImpl类

package org.dromara.sms4j.huawei.service;
import cn.hutool.core.collection.CollUtil;import cn.hutool.core.map.MapUtil;import cn.hutool.json.JSONObject;import lombok.extern.slf4j.Slf4j;import org.apache.commons.lang3.StringUtils;import org.dromara.sms4j.api.entity.SmsResponse;import org.dromara.sms4j.comm.constant.Constant;import org.dromara.sms4j.comm.constant.SupplierConstant;import org.dromara.sms4j.comm.delayedTime.DelayedTime;import org.dromara.sms4j.comm.exception.SmsBlendException;import org.dromara.sms4j.huawei.config.HuaweiConfig;import org.dromara.sms4j.huawei.utils.HuaweiBuilder;import org.dromara.sms4j.provider.service.AbstractSmsBlend;
import java.util.ArrayList;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;import java.util.Objects;import java.util.UUID;import java.util.concurrent.Executor;import java.util.stream.Collectors;
import static org.dromara.sms4j.huawei.utils.HuaweiBuilder.listToString;
@Slf4jpublic class HuaweiSmsImpl extends AbstractSmsBlend {
private int retry = 0;
public HuaweiSmsImpl(HuaweiConfig config, Executor pool, DelayedTime delayed) { super(config, pool, delayed); }
public HuaweiSmsImpl(HuaweiConfig config) { super(config); }
@Override public String getSupplier() { return SupplierConstant.HUAWEI; }
@Override public SmsResponse sendMessage(String phone, String message) { LinkedHashMap<String, String> mes = new LinkedHashMap<>(); mes.put(UUID.randomUUID().toString().replaceAll("-", ""), message); return sendMessage(phone, getConfig().getTemplateId(), mes); }
@Override public SmsResponse sendMessage(String phone, LinkedHashMap<String, String> messages) { if (Objects.isNull(messages)) { messages = new LinkedHashMap<>(); } return sendMessage(phone, getConfig().getTemplateId(), messages); }
@Override public SmsResponse sendMessage(String phone, String templateId, LinkedHashMap<String, String> messages) { if (Objects.isNull(messages)) { messages = new LinkedHashMap<>(); } String url = getConfig().getUrl() + Constant.HUAWEI_REQUEST_URL; List<String> list = new ArrayList<>(); for (Map.Entry<String, String> entry : messages.entrySet()) { list.add(entry.getValue()); } String mess = ""; if (StringUtils.isNotEmpty(templateId)) { mess = listToString(list); } else { if (list.size() == 1) { mess = list.stream().collect(Collectors.joining("")); } else { mess = list.stream().collect(Collectors.joining(",")); } } if (StringUtils.isEmpty(mess)) { throw new RuntimeException("华为发送短信消息不为空!"); }
String requestBody = HuaweiBuilder.buildRequestBody(getConfig().getSender(), phone, templateId, mess, getConfig().getStatusCallBack(), getConfig().getSignature());
Map<String, String> headers = MapUtil.newHashMap(3, true); headers.put("Authorization", Constant.HUAWEI_AUTH_HEADER_VALUE); headers.put("X-WSSE", HuaweiBuilder.buildWsseHeader(getConfig().getAccessKeyId(), getConfig().getAccessKeySecret())); headers.put("Content-Type", Constant.FROM_URLENCODED); SmsResponse smsResponse; try { smsResponse = getResponse(http.postJson(url, headers, requestBody)); } catch (SmsBlendException e) { smsResponse = new SmsResponse(); smsResponse.setSuccess(false); smsResponse.setData(e.getMessage()); } if (smsResponse.isSuccess() || retry == getConfig().getMaxRetries()) { retry = 0; return smsResponse; } return requestRetry(phone, templateId, messages); }
private SmsResponse requestRetry(String phone, String templateId, LinkedHashMap<String, String> messages) { http.safeSleep(getConfig().getRetryInterval()); retry++; log.warn("短信第 {} 次重新发送", retry); return sendMessage(phone, templateId, messages); }
@Override public SmsResponse massTexting(List<String> phones, String message) { return sendMessage(CollUtil.join(phones, ","), message); }
@Override public SmsResponse massTexting(List<String> phones, String templateId, LinkedHashMap<String, String> messages) { if (Objects.isNull(messages)) { messages = new LinkedHashMap<>(); } return sendMessage(CollUtil.join(phones, ","), templateId, messages); }
private SmsResponse getResponse(JSONObject resJson) { SmsResponse smsResponse = new SmsResponse(); smsResponse.setSuccess("000000".equals(resJson.getStr("code"))); smsResponse.setData(resJson); smsResponse.setConfigId(getConfigId()); return smsResponse; }
}

3.3 HuaweiBuilder类

package org.dromara.sms4j.huawei.utils;
import cn.hutool.core.codec.Base64;import cn.hutool.core.date.DateUtil;import org.apache.commons.lang3.StringUtils;import org.dromara.sms4j.comm.constant.Constant;import org.dromara.sms4j.comm.exception.SmsBlendException;
import javax.net.ssl.HttpsURLConnection;import javax.net.ssl.SSLContext;import javax.net.ssl.TrustManager;import javax.net.ssl.X509TrustManager;import java.io.UnsupportedEncodingException;import java.net.URLEncoder;import java.security.MessageDigest;import java.security.NoSuchAlgorithmException;import java.security.cert.X509Certificate;import java.util.Date;import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.UUID;
public class HuaweiBuilder { private HuaweiBuilder() { }
/** * buildWsseHeader *

构造X-WSSE参数值 * * @author :Wind */ public static String buildWsseHeader(String appKey, String appSecret) { if (null == appKey || null == appSecret || appKey.isEmpty() || appSecret.isEmpty()) { System.out.println("buildWsseHeader(): appKey or appSecret is null."); return null; } String time = dateFormat(new Date()); // Nonce String nonce = UUID.randomUUID().toString().replace("-", ""); MessageDigest md; byte[] passwordDigest;
try { md = MessageDigest.getInstance("SHA-256"); md.update((nonce + time + appSecret).getBytes()); passwordDigest = md.digest(); } catch (NoSuchAlgorithmException e) { throw new SmsBlendException(e); } // PasswordDigest String passwordDigestBase64Str = Base64.encode(passwordDigest); //若passwordDigestBase64Str中包含换行符,请执行如下代码进行修正 //passwordDigestBase64Str = passwordDigestBase64Str.replaceAll("[\\s*\t\n\r]", ""); return String.format(Constant.HUAWEI_WSSE_HEADER_FORMAT, appKey, passwordDigestBase64Str, nonce, time); }
static void trustAllHttpsCertificates() throws Exception { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { }
@Override public void checkServerTrusted(X509Certificate[] chain, String authType) { }
@Override public X509Certificate[] getAcceptedIssuers() { return null; } } }; SSLContext sc = SSLContext.getInstance("SSL"); sc.init(null, trustAllCerts, null); HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory()); }
/** * buildRequestBody *

构造请求Body体 * * @param sender 国内短信签名通道号 * @param receiver 短信接收者 * @param templateId 短信模板id * @param templateParas 模板参数 * @param statusCallBack 短信状态报告接收地 * @param signature | 签名名称,使用国内短信通用模板时填写 * @author :Wind */ public static String buildRequestBody(String sender, String receiver, String templateId, String templateParas, String statusCallBack, String signature) { if (null == sender || null == receiver || sender.isEmpty() || receiver.isEmpty()) { System.out.println("buildRequestBody(): sender, receiver is null."); return null; } Map<String, String> map = new HashMap<>();
map.put("from", sender); map.put("to", receiver); if (StringUtils.isNotEmpty(templateId)) { map.put("templateId", templateId); if (null != templateParas && !templateParas.isEmpty()) { map.put("templateParas", templateParas); } }else { map.put("body", templateParas); } if (null != statusCallBack && !statusCallBack.isEmpty()) { map.put("statusCallback", statusCallBack); } if (null != signature && !signature.isEmpty()) { map.put("signature", signature); }
StringBuilder sb = new StringBuilder(); String temp;
for (String s : map.keySet()) { try { temp = URLEncoder.encode(map.get(s), "UTF-8"); } catch (UnsupportedEncodingException e) { throw new SmsBlendException(e); } sb.append(s).append("=").append(temp).append("&"); }
return sb.deleteCharAt(sb.length() - 1).toString(); }
public static String listToString(List<String> list) { if (null == list || list.isEmpty()) { return null; } StringBuilder stringBuffer = new StringBuilder(); stringBuffer.append("[\""); for (String s : list) { stringBuffer.append(s); stringBuffer.append("\""); stringBuffer.append(","); stringBuffer.append("\""); } stringBuffer.delete(stringBuffer.length() - 3, stringBuffer.length() - 1); stringBuffer.append("]"); return stringBuffer.toString(); }
private static String dateFormat(Date date) { return DateUtil.format(date, Constant.HUAWEI_JAVA_DATE); }
}

3.4 SmsBlendsInitializer类

package org.dromara.sms4j.starter.config;
import cn.hutool.core.util.StrUtil;import cn.hutool.json.JSONObject;import cn.hutool.json.JSONUtil;import lombok.extern.slf4j.Slf4j;import org.dromara.sms4j.aliyun.config.AlibabaFactory;import org.dromara.sms4j.api.SmsBlend;import org.dromara.sms4j.api.universal.SupplierConfig;import org.dromara.sms4j.api.verify.PhoneVerify;import org.dromara.sms4j.cloopen.config.CloopenFactory;import org.dromara.sms4j.comm.constant.Constant;import org.dromara.sms4j.comm.enumerate.ConfigType;import org.dromara.sms4j.comm.utils.SmsUtils;import org.dromara.sms4j.core.datainterface.SmsReadConfig;import org.dromara.sms4j.core.factory.SmsFactory;import org.dromara.sms4j.core.proxy.EnvirmentHolder;import org.dromara.sms4j.core.proxy.SmsProxyFactory;import org.dromara.sms4j.core.proxy.processor.BlackListProcessor;import org.dromara.sms4j.core.proxy.processor.BlackListRecordingProcessor;import org.dromara.sms4j.core.proxy.processor.CoreMethodParamValidateProcessor;import org.dromara.sms4j.core.proxy.processor.RestrictedProcessor;import org.dromara.sms4j.core.proxy.processor.SingleBlendRestrictedProcessor;import org.dromara.sms4j.ctyun.config.CtyunFactory;import org.dromara.sms4j.dingzhong.config.DingZhongFactory;import org.dromara.sms4j.emay.config.EmayFactory;import org.dromara.sms4j.huawei.config.HuaweiFactory;import org.dromara.sms4j.jdcloud.config.JdCloudFactory;import org.dromara.sms4j.lianlu.config.LianLuFactory;import org.dromara.sms4j.netease.config.NeteaseFactory;import org.dromara.sms4j.provider.config.SmsConfig;import org.dromara.sms4j.provider.factory.BaseProviderFactory;import org.dromara.sms4j.provider.factory.ProviderFactoryHolder;import org.dromara.sms4j.qiniu.config.QiNiuFactory;import org.dromara.sms4j.starter.adepter.ConfigCombineMapAdeptor;import org.dromara.sms4j.tencent.config.TencentFactory;import org.dromara.sms4j.unisms.config.UniFactory;import org.dromara.sms4j.yunpian.config.YunPianFactory;import org.dromara.sms4j.zhutong.config.ZhutongFactory;import org.springframework.beans.factory.ObjectProvider;
import java.util.HashMap;import java.util.List;import java.util.Map;import java.util.ServiceLoader;

@Slf4jpublic class SmsBlendsInitializer { private final List<BaseProviderFactorySmsBlend, ? extends SupplierConfig>> factoryList;
private final SmsConfig smsConfig; private final Map<String, Map<String, Object>> blends; private final ObjectProvider<SmsReadConfig> extendsSmsConfigs;
public SmsBlendsInitializer(List<BaseProviderFactorySmsBlend, ? extends SupplierConfig>> factoryList, SmsConfig smsConfig, Map<String, Map<String, Object>> blends, ObjectProvider<SmsReadConfig> extendsSmsConfigs) { this.factoryList = factoryList; this.smsConfig = smsConfig; this.blends = blends; this.extendsSmsConfigs = extendsSmsConfigs; onApplicationEvent(); }
public void onApplicationEvent() { this.registerDefaultFactory(); // 注册短信对象工厂 ProviderFactoryHolder.registerFactory(factoryList);
if (ConfigType.YAML.equals(this.smsConfig.getConfigType())) { //持有初始化配置信息 Map<String, Map<String, Object>> blendsInclude = new ConfigCombineMapAdeptor<String, Map<String, Object>>(); blendsInclude.putAll(this.blends); int num = 0; for (SmsReadConfig smsReadConfig : extendsSmsConfigs) { String key = SmsReadConfig.class.getSimpleName() + num; Map<String, Object> insideMap = new HashMap<>(); insideMap.put(key, smsReadConfig); blendsInclude.put(key, insideMap); num++; } EnvirmentHolder.frozenEnvirmet(smsConfig, blendsInclude); //注册执行器实现 SmsProxyFactory.addProcessor(new RestrictedProcessor()); SmsProxyFactory.addProcessor(new BlackListProcessor()); SmsProxyFactory.addProcessor(new BlackListRecordingProcessor()); SmsProxyFactory.addProcessor(new SingleBlendRestrictedProcessor()); //如果手机号校验器存在实现,则注册手机号校验器 ServiceLoader<PhoneVerify> loader = ServiceLoader.load(PhoneVerify.class); if (loader.iterator().hasNext()) { loader.forEach(f -> { SmsProxyFactory.addProcessor(new CoreMethodParamValidateProcessor(f)); }); } else { //这里需要把处理器注释掉,否则smsBlend.massTexting(phones, msg)接口调用会抛异常:cant send message to null!,这里改造就不需要这些前置和后置的处理器了 //SmsProxyFactory.addProcessor(new CoreMethodParamValidateProcessor(null)); }
// 解析供应商配置 for (String configId : blends.keySet()) { Map<String, Object> configMap = blends.get(configId); Object supplierObj = configMap.get(Constant.SUPPLIER_KEY); String supplier = supplierObj == null ? "" : String.valueOf(supplierObj); supplier = StrUtil.isEmpty(supplier) ? configId : supplier; BaseProviderFactory<SmsBlend, SupplierConfig> providerFactory = (BaseProviderFactory<SmsBlend, org.dromara.sms4j.api.universal.SupplierConfig>) ProviderFactoryHolder.requireForSupplier(supplier); if (providerFactory == null) { log.warn("创建\"{}\"的短信服务失败,未找到供应商为\"{}\"的服务", configId, supplier); continue; } configMap.put("config-id", configId); SmsUtils.replaceKeysSeperator(configMap, "-", "_"); JSONObject configJson = new JSONObject(configMap); org.dromara.sms4j.api.universal.SupplierConfig supplierConfig = JSONUtil.toBean(configJson, providerFactory.getConfigClass()); SmsFactory.createSmsBlend(supplierConfig); } }

}
/** * 注册默认工厂实例 */ private void registerDefaultFactory() { ProviderFactoryHolder.registerFactory(AlibabaFactory.instance()); ProviderFactoryHolder.registerFactory(CloopenFactory.instance()); ProviderFactoryHolder.registerFactory(CtyunFactory.instance()); ProviderFactoryHolder.registerFactory(EmayFactory.instance()); ProviderFactoryHolder.registerFactory(HuaweiFactory.instance()); ProviderFactoryHolder.registerFactory(NeteaseFactory.instance()); ProviderFactoryHolder.registerFactory(TencentFactory.instance()); ProviderFactoryHolder.registerFactory(UniFactory.instance()); ProviderFactoryHolder.registerFactory(YunPianFactory.instance()); ProviderFactoryHolder.registerFactory(ZhutongFactory.instance()); ProviderFactoryHolder.registerFactory(LianLuFactory.instance()); ProviderFactoryHolder.registerFactory(DingZhongFactory.instance()); ProviderFactoryHolder.registerFactory(QiNiuFactory.instance()); if (SmsUtils.isClassExists("com.jdcloud.sdk.auth.CredentialsProvider")) { ProviderFactoryHolder.registerFactory(JdCloudFactory.instance()); } log.debug("加载内置运营商完成!"); }
}

4.测试

4.1 HwSmsService类

package xxxxx.service.impl;
import com.alibaba.fastjson.JSON;import lombok.extern.slf4j.Slf4j;import org.dromara.sms4j.api.SmsBlend;import org.dromara.sms4j.api.entity.SmsResponse;import org.dromara.sms4j.core.factory.SmsFactory;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;
import java.util.List;import java.util.Objects;
@Slf4j@Servicepublic class HwSmsService {

public SmsBlend getSmsBlend() { SmsBlend hw1 = SmsFactory.getSmsBlend("hw1"); return hw1; }

public Boolean sendMsg(List phones, String msg) { SmsBlend smsBlend = this.getSmsBlend(); SmsResponse smsResponse = smsBlend.massTexting(phones, msg); log.info("HwSmsService.smsResponse:{}", JSON.toJSONString(smsResponse)); if (smsResponse.isSuccess()) { return Boolean.TRUE; } return Boolean.FALSE; }
}

4.2 TestController测试类

package xxxxx.controller;
import com.alibaba.fastjson.JSON;import com.xxxxx.impl.HwSmsService;import com.xxl.job.core.biz.model.ReturnT;import lombok.extern.slf4j.Slf4j;import org.dromara.sms4j.api.SmsBlend;import org.dromara.sms4j.api.entity.SmsResponse;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RestController;
import java.util.Collections;import java.util.List;
@Slf4j@RestController@RequestMapping("/test")public class TestController { @Autowired private HwSmsService hwSmsService;
@GetMapping("/test3") public SmsResponse test3() { SmsBlend smsBlend = hwSmsService.getSmsBlend(); SmsResponse response = smsBlend.sendMessage("xxxxxxx手机号码", "【短信签名xxxx】 测试短信发送"); log.info("smsResponse:{}", JSON.toJSONString(response)); return response; }
@GetMapping("/test8") public SmsResponse test8() { SmsBlend smsBlend = hwSmsService.getSmsBlend(); SmsResponse response = smsBlend.massTexting(Collections.singletonList("xxxxxxx手机号码"), "【短信签名xxxx】测试短信发送"); log.info("smsResponse:{}", JSON.toJSONString(response)); return response; } }
经过上面源码的修改,输入正确的手机号和正确的短信内容(注意这里需要注意的是短信内容里面需要带有短信签名信息【短信签名xxxx】+ 文本字符串(注意不要有-,如果有-会被转换替换为_,具体可以去看源码,不符合你的需求可以改源码实现)),经过上面的修改,这两个接口就可以正常调用了,这两个接口亲测是可以收到短信的,至于项目中使用这两个接口只需要准备手机号和构造文本短信,例如:文本短信模版是:【短信签名xxxx】您的订单:orderNo已经支付,然后使用String的replace("orderNo",202407300000018)替换就可构造好短信内容,具体的根据自己的业务来搞一个短信模版,然后替换参数就得到一个文本的字符串,不使用短信模版id也可以发送短信,只需要短信内容里面包含短信签名即可。


5.总结

经过以上对sms4j的源码改造,实现了华为云发送不带短信模版id的短信,可以去把sms4j的源码拉下来,趴一趴看一看,集成到项目中,然后debug打断点跟一波源码,其实也还是简单的,华为云发短信是这种玩的,其他厂商的也是同样的道理,举一反三,触类旁通,从开源的轮子中可以学习到优秀的设计思想和设计实现,而不是CRUD,就算是写CRUD也要写了优雅、干净、整洁、帅气、复用性-可拓展性-鲁棒性-可读性-可维护性都好一点,这种bug自然是没有啥bug的,养成良好的编码习惯和编码风格可以降低很多的bug,我的分享到此结束了,希望我的分享对你有所启发和帮助,创作不易,都是亲身实践经验总结,不吝分享,还请不要照抄过去就成了你自己的原创,转发请标注原文及作者出处,发现抄袭(不带思考的原模原样的抄袭,没有多大意义,全成为千篇一律的文章,写文章还是要有一点思考、实践的,否则就是坑文水文,没有一点深度,过于肤浅),则直接举报(或者可以联系本人来举报),请一键三连,么么么哒!

请使用浏览器的分享功能分享到微信等