这破玩意是规则引擎?

共 10815字,需浏览 22分钟

 ·

2023-04-07 12:20

前阵子卷了几天,发了一版hades,已经发到maven的中央仓库了,项目里有example的模块,README也已经补充完整了。但是,还是有好些同学说有点抽象,还是不知道怎么接入和使用。

今天还是以austin为例,详细来说下接入hades的过程。

0、需求背景

之前有聊到过,austin作为消息推送平台,它是会接入多个短信渠道的。一方面是不同的渠道会有不同的价格,我们可能会尝试接入发送成本更低的渠道,另一方面,有多个短信渠道可以做容灾(假设只有一个短信渠道,要是该渠道挂了,那austin就相当于发不了短信了)

接入短信渠道这块,在austin是有设计过的(至少可以说是面向接口编程吧),每个渠道都要实现SmsScript接口。

04197023244d081a7edadbb894492a46.webp

而接入短信的代码往往很简单,核心逻辑只是编写代码调用其HTTP接口去下发短信,对于整个系统而言都没什么新的依赖要引入(很轻量)。

而每次接入短信(就相当于写一个类),我都要重启发布上线吗?这不靠谱吧?效率这么低?

解决方案:上规则引擎(hades)将业务代码抽离,无须上下线即可实现功能。

c18612545c0fdefae94e19834280d9f8.webp

注:比较轻的逻辑是适合用规则引擎去做这种抽离的,这会提高我们的开发效率。而如果业务是核心链路上的主流程或者要引入各种的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和最好别用Javalambda

如上的代码,我如果使用了lombok去生成Logger对象,这会在代码执行时会报错:

c17970c57ddd02a47052796769fc660a.webp

经过一轮验证之后,我们觉得这代码没啥问题了。正常是要走发布流程,把新写的代码发布上线生效的,接入了hades的话,就可以动态生效了

2、接入hades规则引擎

目前hades提供两个客户端(apollonacos),你项目用哪个分布式配置中心,你就引入哪个,后期有可能还会新增别的客户端

      
      <!--如果你用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,自行实现获取配置和配置实时通知的逻辑。这里我就不再多说了,先回到apollonacos这两个客户端吧。

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是通的)。

而接入hadeshades-apollo-starter下需要有以下的配置:

      
      hades.main.config.enabled=true
hades.main.config.file-name=hades

这儿的hades.main.config.file-name其实指的就是apollonamespace。于是乎,我们需要在austin这个app.id下创建namespace,名为hades

0810f55b04267bfb4be5f2324921b37d.webp

注:使用hades中,创建出来的所有namespace配置格式都需要是txt

然后,往hades这个namespace填充值,如下:

      
      {
    "instanceNames": [
        "YunPianSmsScript"
    ],
    "updateTime""2023年3月20日10:26:0133"
}
37861ebfe99daa537b6aaad1b754cd19.webp

然后创建出YunPianSmsScript这个namespace,填入我们本地已经写好的代码:

f5b7e614ffe1d3481be1c075ccd3d349.webp

到这一步,启动项目就会有以下日志打印出来:

      
       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,然后去调用对应的方法的。

c1060d896b709a639a07c56d6ac03924.webp

那么,当我在页面选中的是云片发送渠道,在没有重启发布的情况下, 就可以直接调用对应的逻辑了(就是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}
1f722f4bdf5a236253ca772be7e2f4f0.webp

所以这不是接入hades的重点,因为你项目本身就已经接入了nacos了(至少你需保证你的项目跟nacos是通的)。而接入hadeshades-nacos-starter下需要有以下的配置:

      
      hades.main.config.enabled=true
hades.main.config.file-name=hades
hades.main.config.group-name=hades

这儿的hades.main.config.file-name其实指的就是nacosdataId。于是乎,我们需要在60e2b165-d830-4163-a0e9-b97ec2f7164c这个namespace下创建dataId,名为hadesgroup-name也为hades

ffb0550d2bb689cb998475740744566b.webp

注:使用hades中,创建出来的所有dataId配置格式都需要是text!然后,往hades这个dataId填充值,如下:

      
      {
    "instanceNames": [
        "YunPianSmsScript"
    ],
    "updateTime""2023年3月20日10:26:0133"
}

然后创建出YunPianSmsScript这个dataId,填入我们本地已经写好的代码:

a6ae6bd2e776a23be51f0c6e30e52159.webp

到这一步,启动项目就会有以下日志打印出来:

      
       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,然后去调用对应的方法的。

c1060d896b709a639a07c56d6ac03924.webp

那么,当我在页面选中的是云片发送渠道,在没有重启发布的情况下, 就可以直接调用对应的逻辑了(就是YunPianSmsScript的代码)。如果修改了YunPianSmsScript的代码,那先在nacos发布YunPianSmsScript的代码,然后手动把hades主配置改了,只要改时间updateTime就好了。

c41141286e822019be6b0214f5ea45b9.webp

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加的时候记得备注  报名

没备注或备注错误 ,不会通过好友请求的哟!

浏览 163
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报