这破玩意是规则引擎?
前阵子卷了几天,发了一版hades
,已经发到maven
的中央仓库了,项目里有example
的模块,README
也已经补充完整了。但是,还是有好些同学说有点抽象,还是不知道怎么接入和使用。
今天还是以austin
为例,详细来说下接入hades
的过程。
0、需求背景
之前有聊到过,austin
作为消息推送平台,它是会接入多个短信渠道的。一方面是不同的渠道会有不同的价格,我们可能会尝试接入发送成本更低的渠道,另一方面,有多个短信渠道可以做容灾(假设只有一个短信渠道,要是该渠道挂了,那austin
就相当于发不了短信了)
接入短信渠道这块,在austin
是有设计过的(至少可以说是面向接口编程吧),每个渠道都要实现SmsScript
接口。
而接入短信的代码往往很简单,核心逻辑只是编写代码调用其HTTP接口去下发短信,对于整个系统而言都没什么新的依赖要引入(很轻量)。
而每次接入短信(就相当于写一个类),我都要重启发布上线吗?这不靠谱吧?效率这么低?
解决方案:上规则引擎(hades)将业务代码抽离,无须上下线即可实现功能。
注:比较轻的逻辑是适合用规则引擎去做这种抽离的,这会提高我们的开发效率。而如果业务是核心链路上的主流程或者要引入各种的SDK才能实现的,这种就不适合用规则引擎了。
1、本地写好代码
比如,我们现在系统已经接入了腾讯云短信了,现在商务说云片这个渠道更便宜,让我去接入下。这时候,我还是正常在IDE
上开发,加入云片这个渠道。
于是我写出以下的代码(实现了SmsScript
接口,剩下就是组装参数,调用HTTP
的过程哈):
package com.java3y.austin.handler.script.impl;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpRequest;
import com.alibaba.fastjson.JSON;
import com.google.common.base.Throwables;
import com.java3y.austin.common.constant.CommonConstant;
import com.java3y.austin.common.dto.account.sms.YunPianSmsAccount;
import com.java3y.austin.common.enums.SmsStatus;
import com.java3y.austin.handler.domain.sms.SmsParam;
import com.java3y.austin.handler.domain.sms.YunPianSendResult;
import com.java3y.austin.handler.script.SmsScript;
import com.java3y.austin.support.domain.SmsRecord;
import com.java3y.austin.support.utils.AccountUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.*;
/**
* @author 3y
* @date 2022年5月23日
* 发送短信接入文档:https://www.yunpian.com/official/document/sms/zh_CN/domestic_list
*/
//@Slf4j
@Component("YunPianSmsScript")
public class YunPianSmsScript implements SmsScript {
private static Logger log = LoggerFactory.getLogger(YunPianSmsScript.class);
@Autowired
private AccountUtils accountUtils;
@Override
public List<SmsRecord> send(SmsParam smsParam) {
try {
YunPianSmsAccount account = Objects.nonNull(smsParam.getSendAccountId()) ? accountUtils.getAccountById(smsParam.getSendAccountId(), YunPianSmsAccount.class)
: accountUtils.getSmsAccountByScriptName(smsParam.getScriptName(), YunPianSmsAccount.class);
Map<String, Object> params = assembleParam(smsParam, account);
String result = HttpRequest.post(account.getUrl())
.header(Header.CONTENT_TYPE.getValue(), CommonConstant.CONTENT_TYPE_FORM_URL_ENCODE)
.header(Header.ACCEPT.getValue(), CommonConstant.CONTENT_TYPE_JSON)
.form(params)
.timeout(2000)
.execute().body();
YunPianSendResult yunPianSendResult = JSON.parseObject(result, YunPianSendResult.class);
return assembleSmsRecord(smsParam, yunPianSendResult, account);
} catch (Exception e) {
log.error("YunPianSmsScript#send fail:{},params:{}", Throwables.getStackTraceAsString(e), JSON.toJSONString(smsParam));
return null;
}
}
@Override
public List<SmsRecord> pull(Integer accountId) {
// .....
return null;
}
/**
* 组装参数
*
* @param smsParam
* @param account
* @return
*/
private Map<String, Object> assembleParam(SmsParam smsParam, YunPianSmsAccount account) {
Map<String, Object> params = new HashMap<>(8);
params.put("apikey", account.getApikey());
params.put("mobile", StringUtils.join(smsParam.getPhones(), StrUtil.C_COMMA));
params.put("tpl_id", account.getTplId());
params.put("tpl_value", "");
return params;
}
private List<SmsRecord> assembleSmsRecord(SmsParam smsParam, YunPianSendResult response, YunPianSmsAccount account) {
if (Objects.isNull(response) || ArrayUtil.isEmpty(response.getData())) {
log.error("YunPianSmsScript#assembleSmsRecord response null :{}" , JSON.toJSONString(response));
return null;
}
List<SmsRecord> smsRecordList = new ArrayList<>();
for (YunPianSendResult.DataDTO datum : response.getData()) {
SmsRecord smsRecord = SmsRecord.builder()
.sendDate(Integer.valueOf(DateUtil.format(new Date(), DatePattern.PURE_DATE_PATTERN)))
.messageTemplateId(smsParam.getMessageTemplateId())
.phone(Long.valueOf(datum.getMobile()))
.supplierId(account.getSupplierId())
.supplierName(account.getSupplierName())
.msgContent(smsParam.getContent())
.seriesId(datum.getSid())
.chargingNum(Math.toIntExact(datum.getCount()))
.status(0 == datum.getCode() ? SmsStatus.SEND_SUCCESS.getCode() : SmsStatus.SEND_FAIL.getCode())
.reportContent(datum.getMsg())
.created(Math.toIntExact(DateUtil.currentSeconds()))
.updated(Math.toIntExact(DateUtil.currentSeconds()))
.build();
smsRecordList.add(smsRecord);
}
return smsRecordList;
}
}
注:hades
是基于Groovy
实现的,虽然看起来就是Java
代码。但是,这里不能用lombok
和最好别用Java
的lambda
。
如上的代码,我如果使用了lombok
去生成Logger
对象,这会在代码执行时会报错:
经过一轮验证之后,我们觉得这代码没啥问题了。正常是要走发布流程,把新写的代码发布上线生效的,接入了hades
的话,就可以动态生效了。
2、接入hades规则引擎
目前hades
提供两个客户端(apollo
和nacos
),你项目用哪个分布式配置中心,你就引入哪个,后期有可能还会新增别的客户端。
<!--如果你用apollo,则引入该dependency-->
<dependency>
<groupId>io.github.ZhongFuCheng3y</groupId>
<artifactId>hades-apollo-starter</artifactId>
<version>1.0.2</version>
</dependency>
<!--如果你用nacos,则引入该dependency-->
<dependency>
<groupId>io.github.ZhongFuCheng3y</groupId>
<artifactId>hades-nacos-starter</artifactId>
<version>1.0.2</version>
</dependency>
你也可以引入hades-core
包,继承BaseHadesConfig
,自行实现获取配置和配置实时通知的逻辑。这里我就不再多说了,先回到apollo
和nacos
这两个客户端吧。
3、使用apollo接入
当我们的本身项目环境使用的是apollo
时,我们就用hades-apollo-starter
包。于是在项目需要引入以下pom
:
<!--如果你用apollo,则引入该dependency-->
<dependency>
<groupId>io.github.ZhongFuCheng3y</groupId>
<artifactId>hades-apollo-starter</artifactId>
<version>1.0.2</version>
</dependency>
接入apollo
本身就会需要指定以下配置:
app.id=austin
apollo.bootstrap.enabled=true
apollo.meta=192.0.0.1
所以这不是接入hades
的重点,因为你项目本身就已经接入了apollo
了(至少你需保证你的项目跟apollo
是通的)。
而接入hades
在hades-apollo-starter
下需要有以下的配置:
hades.main.config.enabled=true
hades.main.config.file-name=hades
这儿的hades.main.config.file-name
其实指的就是apollo
的namespace
。于是乎,我们需要在austin
这个app.id
下创建namespace
,名为hades
。
注:使用hades
中,创建出来的所有namespace
配置格式都需要是txt
!
然后,往hades
这个namespace
填充值,如下:
{
"instanceNames": [
"YunPianSmsScript"
],
"updateTime": "2023年3月20日10:26:0133"
}
然后创建出YunPianSmsScript
这个namespace
,填入我们本地已经写好的代码:
到这一步,启动项目就会有以下日志打印出来:
INFO com.java3y.hades.core.utils.GroovyUtils - Groovy解析:class=[YunPianSmsScript]语法通过
INFO c.j.hades.core.service.bootstrap.BaseHadesConfig - bean:[com.java3y.austin.handler.script.impl.YunPianSmsScript]已注册到Spring IOC中
INFO com.java3y.hades.starter.config.ApolloStarter - 分布式配置中心配置[hades]监听器已启动
项目设计之初就考虑到这种情况了,所以在代码上我是通过ScriptName
去得到Bean
,然后去调用对应的方法的。
那么,当我在页面选中的是云片发送渠道,在没有重启发布的情况下, 就可以直接调用对应的逻辑了(就是YunPianSmsScript
的代码)。如果修改了YunPianSmsScript
的代码,那先在apollo
发布YunPianSmsScript
的代码,然后手动把hades
主配置改了,只要改时间updateTime
就好了。
4、使用nacos接入
当我们的本身项目环境使用的是nacos
时,我们就用hades-nacos-starter
包。于是在项目需要引入以下pom
:
<!--如果你用nacos,则引入该dependency-->
<dependency>
<groupId>io.github.ZhongFuCheng3y</groupId>
<artifactId>hades-nacos-starter</artifactId>
<version>1.0.3</version>
</dependency>
接入apollo
本身就会需要指定以下配置:
nacos.config.server-addr=${austin.nacos.addr.ip:austin-nacos}:${austin.nacos.addr.port:8848}
nacos.config.username=${austin.nacos.username:nacos}
nacos.config.password=${austin.nacos.password:nacos}
nacos.config.namespace=${austin.nacos.namespace:60e2b165-d830-4163-a0e9-b97ec2f7164c}
nacos.config.enabled=${austin.nacos.enabled}
所以这不是接入hades
的重点,因为你项目本身就已经接入了nacos
了(至少你需保证你的项目跟nacos
是通的)。而接入hades
在hades-nacos-starter
下需要有以下的配置:
hades.main.config.enabled=true
hades.main.config.file-name=hades
hades.main.config.group-name=hades
这儿的hades.main.config.file-name
其实指的就是nacos
的dataId
。于是乎,我们需要在60e2b165-d830-4163-a0e9-b97ec2f7164c
这个namespace
下创建dataId
,名为hades
,group-name
也为hades
注:使用hades
中,创建出来的所有dataId
配置格式都需要是text
!然后,往hades
这个dataId
填充值,如下:
{
"instanceNames": [
"YunPianSmsScript"
],
"updateTime": "2023年3月20日10:26:0133"
}
然后创建出YunPianSmsScript
这个dataId
,填入我们本地已经写好的代码:
到这一步,启动项目就会有以下日志打印出来:
INFO com.java3y.hades.core.utils.GroovyUtils - Groovy解析:class=[YunPianSmsScript]语法通过
INFO c.j.hades.core.service.bootstrap.BaseHadesConfig - bean:[com.java3y.austin.handler.script.impl.YunPianSmsScript]已注册到Spring IOC中
INFO com.java3y.hades.starter.config.ApolloStarter - 分布式配置中心配置[hades]监听器已启动
项目设计之初就考虑到这种情况了,所以在代码上我是通过ScriptName
去得到Bean
,然后去调用对应的方法的。
那么,当我在页面选中的是云片发送渠道,在没有重启发布的情况下, 就可以直接调用对应的逻辑了(就是YunPianSmsScript
的代码)。如果修改了YunPianSmsScript
的代码,那先在nacos
发布YunPianSmsScript
的代码,然后手动把hades
主配置改了,只要改时间updateTime
就好了。
05、最佳实践
如果云片YunPianSmsScript
这个脚本逻辑确定要接入长期使用了,建议在下一次发布的时候,将其带上。(毕竟脚本是易动的,而固定的逻辑下来的应该要在项目中的程序代码里的)
这时当发布过后,需要把hades
主配置手动更新下,把YunPianSmsScript
给删掉:
{
"instanceNames": [],
"updateTime": "2023年3月20日10:26:0133"
}
既然能在已发布的应用上,动态新增一个SpringBean
,这个SpringBean
还能多次动态修改其逻辑。
那自然在已发布的应用上,动态修改一个已有SpringBean
的逻辑,也是能做到的。(灵活性会带来风险,我是建议每次改这种代码逻辑,是要走beta
/pre
环境的,最后才上prod
)
如果想学Java项目的,我还是
强烈推荐
我的开源项目消息推送平台Austin,可以用作
毕业设计
,可以用作
校招
,可以看看
生产环境是怎么推送消息
的。
时间不等人,犹豫就会败北 。
仓库地址:https://gitee.com/zhongfucheng/austin
现在报名还 480/年 , 从4月10号起改为580/年 。 文档资料和答疑服务有效期 一年 。
我就不搞报名前几名优惠多少,或者定高价打个折什么的营销了, 就是一口价 ,不整那些虚的 。
如果你想报名,可以加我的微信weixin403686131,加的时候记得备注 报名。
没备注或备注错误 ,不会通过好友请求的哟!