這破玩意是規(guī)則引擎?
前陣子卷了幾天,發(fā)了一版hades,已經(jīng)發(fā)到maven的中央倉庫了,項(xiàng)目里有example的模塊,README也已經(jīng)補(bǔ)充完整了。但是,還是有好些同學(xué)說有點(diǎn)抽象,還是不知道怎么接入和使用。
今天還是以austin為例,詳細(xì)來說下接入hades的過程。
0、需求背景
之前有聊到過,austin作為消息推送平臺,它是會接入多個(gè)短信渠道的。一方面是不同的渠道會有不同的價(jià)格,我們可能會嘗試接入發(fā)送成本更低的渠道,另一方面,有多個(gè)短信渠道可以做容災(zāi)(假設(shè)只有一個(gè)短信渠道,要是該渠道掛了,那austin就相當(dāng)于發(fā)不了短信了)
接入短信渠道這塊,在austin是有設(shè)計(jì)過的(至少可以說是面向接口編程吧),每個(gè)渠道都要實(shí)現(xiàn)SmsScript接口。

而接入短信的代碼往往很簡單,核心邏輯只是編寫代碼調(diào)用其HTTP接口去下發(fā)短信,對于整個(gè)系統(tǒng)而言都沒什么新的依賴要引入(很輕量)。
而每次接入短信(就相當(dāng)于寫一個(gè)類),我都要重啟發(fā)布上線嗎?這不靠譜吧?效率這么低?
解決方案:上規(guī)則引擎(hades)將業(yè)務(wù)代碼抽離,無須上下線即可實(shí)現(xiàn)功能。

注:比較輕的邏輯是適合用規(guī)則引擎去做這種抽離的,這會提高我們的開發(fā)效率。而如果業(yè)務(wù)是核心鏈路上的主流程或者要引入各種的SDK才能實(shí)現(xiàn)的,這種就不適合用規(guī)則引擎了。
1、本地寫好代碼
比如,我們現(xiàn)在系統(tǒng)已經(jīng)接入了騰訊云短信了,現(xiàn)在商務(wù)說云片這個(gè)渠道更便宜,讓我去接入下。這時(shí)候,我還是正常在IDE上開發(fā),加入云片這個(gè)渠道。
于是我寫出以下的代碼(實(shí)現(xiàn)了SmsScript接口,剩下就是組裝參數(shù),調(diào)用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日
?*?發(fā)送短信接入文檔: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;
????}
????/**
?????*?組裝參數(shù)
?????*
?????*?@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實(shí)現(xiàn)的,雖然看起來就是Java代碼。但是,這里不能用lombok和最好別用Java的lambda
。
如上的代碼,我如果使用了lombok去生成Logger對象,這會在代碼執(zhí)行時(shí)會報(bào)錯(cuò):

經(jīng)過一輪驗(yàn)證之后,我們覺得這代碼沒啥問題了。正常是要走發(fā)布流程,把新寫的代碼發(fā)布上線生效的,接入了hades的話,就可以動態(tài)生效了。
2、接入hades規(guī)則引擎
目前hades提供兩個(gè)客戶端(apollo和nacos),你項(xiàng)目用哪個(gè)分布式配置中心,你就引入哪個(gè),后期有可能還會新增別的客戶端。
<!--如果你用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,自行實(shí)現(xiàn)獲取配置和配置實(shí)時(shí)通知的邏輯。這里我就不再多說了,先回到apollo和nacos這兩個(gè)客戶端吧。
3、使用apollo接入
當(dāng)我們的本身項(xiàng)目環(huán)境使用的是apollo時(shí),我們就用hades-apollo-starter包。于是在項(xiàng)目需要引入以下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的重點(diǎn),因?yàn)槟沩?xiàng)目本身就已經(jīng)接入了apollo了(至少你需保證你的項(xiàng)目跟apollo是通的)。
而接入hades在hades-apollo-starter下需要有以下的配置:
hades.main.config.enabled=true
hades.main.config.file-name=hades
這兒的hades.main.config.file-name其實(shí)指的就是apollo的namespace。于是乎,我們需要在austin這個(gè)app.id下創(chuàng)建namespace,名為hades。

注:使用hades中,創(chuàng)建出來的所有namespace配置格式都需要是txt!
然后,往hades這個(gè)namespace填充值,如下:
{
????"instanceNames":?[
????????"YunPianSmsScript"
????],
????"updateTime":?"2023年3月20日10:26:0133"
}

然后創(chuàng)建出YunPianSmsScript這個(gè)namespace,填入我們本地已經(jīng)寫好的代碼:

到這一步,啟動項(xiàng)目就會有以下日志打印出來:
?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]監(jiān)聽器已啟動
項(xiàng)目設(shè)計(jì)之初就考慮到這種情況了,所以在代碼上我是通過ScriptName去得到Bean,然后去調(diào)用對應(yīng)的方法的。

那么,當(dāng)我在頁面選中的是云片發(fā)送渠道,在沒有重啟發(fā)布的情況下, 就可以直接調(diào)用對應(yīng)的邏輯了(就是YunPianSmsScript的代碼)。如果修改了YunPianSmsScript的代碼,那先在apollo發(fā)布YunPianSmsScript的代碼,然后手動把hades主配置改了,只要改時(shí)間updateTime就好了。
4、使用nacos接入
當(dāng)我們的本身項(xiàng)目環(huán)境使用的是nacos時(shí),我們就用hades-nacos-starter包。于是在項(xiàng)目需要引入以下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的重點(diǎn),因?yàn)槟沩?xiàng)目本身就已經(jīng)接入了nacos了(至少你需保證你的項(xiàng)目跟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其實(shí)指的就是nacos的dataId。于是乎,我們需要在60e2b165-d830-4163-a0e9-b97ec2f7164c這個(gè)namespace下創(chuàng)建dataId,名為hades,group-name也為hades

注:使用hades中,創(chuàng)建出來的所有dataId配置格式都需要是text!然后,往hades這個(gè)dataId填充值,如下:
{
????"instanceNames":?[
????????"YunPianSmsScript"
????],
????"updateTime":?"2023年3月20日10:26:0133"
}
然后創(chuàng)建出YunPianSmsScript這個(gè)dataId,填入我們本地已經(jīng)寫好的代碼:

到這一步,啟動項(xiàng)目就會有以下日志打印出來:
?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]監(jiān)聽器已啟動
項(xiàng)目設(shè)計(jì)之初就考慮到這種情況了,所以在代碼上我是通過ScriptName去得到Bean,然后去調(diào)用對應(yīng)的方法的。

那么,當(dāng)我在頁面選中的是云片發(fā)送渠道,在沒有重啟發(fā)布的情況下, 就可以直接調(diào)用對應(yīng)的邏輯了(就是YunPianSmsScript的代碼)。如果修改了YunPianSmsScript的代碼,那先在nacos發(fā)布YunPianSmsScript的代碼,然后手動把hades主配置改了,只要改時(shí)間updateTime就好了。

05、最佳實(shí)踐
如果云片YunPianSmsScript這個(gè)腳本邏輯確定要接入長期使用了,建議在下一次發(fā)布的時(shí)候,將其帶上。(畢竟腳本是易動的,而固定的邏輯下來的應(yīng)該要在項(xiàng)目中的程序代碼里的)
這時(shí)當(dāng)發(fā)布過后,需要把hades主配置手動更新下,把YunPianSmsScript給刪掉:
{
????"instanceNames":?[],
????"updateTime":?"2023年3月20日10:26:0133"
}
既然能在已發(fā)布的應(yīng)用上,動態(tài)新增一個(gè)SpringBean,這個(gè)SpringBean還能多次動態(tài)修改其邏輯。
那自然在已發(fā)布的應(yīng)用上,動態(tài)修改一個(gè)已有SpringBean的邏輯,也是能做到的。(靈活性會帶來風(fēng)險(xiǎn),我是建議每次改這種代碼邏輯,是要走beta/pre環(huán)境的,最后才上prod)
如果想學(xué)Java項(xiàng)目的,我還是
強(qiáng)烈推薦
我的開源項(xiàng)目消息推送平臺Austin,可以用作
畢業(yè)設(shè)計(jì)
,可以用作
校招
,可以看看
生產(chǎn)環(huán)境是怎么推送消息
的。
時(shí)間不等人,猶豫就會敗北 。
倉庫地址:https://gitee.com/zhongfucheng/austin
現(xiàn)在報(bào)名還? 480/年 , 從4月10號起改為580/年 。 文檔資料和答疑服務(wù)有效期 一年 。
我就不搞報(bào)名前幾名優(yōu)惠多少,或者定高價(jià)打個(gè)折什么的營銷了, 就是一口價(jià) ,不整那些虛的 。
如果你想報(bào)名,可以加我的微信weixin403686131,加的時(shí)候記得備注??報(bào)名。
沒備注或備注錯(cuò)誤 ,不會通過好友請求的喲!
