7.snail-job的工作流任务

前言

前段时间忙项目,这个snail-job系列也停更了有段时间了。接续前缘,按照之前的思路,继续探索未知的snail-job领域。不过这段时间snail-job也没闲着,非常活跃的经历了多次升级,目前最新的版本已经来到了v1.4.0版本。最直观的感受是服务端的后台报警信息少了很多。可能与通讯模式默认改为Grpc有关。这里也为开原作者献上我的敬意!

本节主要是针对工作流任务进行测试学习。在前面6节的学习后,我们搭建一个普通定时任务已经完全没有问题了,而这个工作流其实就是把多个普通定时进行编排,从而形成一个有前后执行顺序甚至是有条件的流程任务。

抓重点

第一段叽里呱啦说了一大段,真正有营养的就最后一句话。拆解这句话,可以归纳:

  1. 工作流是普通定时任务的集合
  2. 工作流有前后执行顺序
  3. 工作流在执行某个任务可以加条件

本节目标1

这里实现一个假设的业务场景:一个是微信的账单收集任务,统计某一天的总金额;另一个是支付宝的账单收集任务,同样也是统计某一天的总金额;等这两个任务完成后再执行一个会汇总账单总金额的定时任务。

基础的流程就如上图这么简单的业务流程。后面可能随着深入学习,此图可能会有所变化。

目标1实现

客户端开发环境

  • JDK版本:openjdk-21.0.2
  • snail-job版本:1.2.0

客户端Maven依赖

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- snail-job 客户端依赖 -->
    <dependency>
        <groupId>com.aizuda</groupId>
        <artifactId>snail-job-client-starter</artifactId>
        <version>1.4.0</version>
    </dependency>
    <!-- snail-job 重试相关依赖 -->
    <dependency>
        <groupId>com.aizuda</groupId>
        <artifactId>snail-job-client-retry-core</artifactId>
        <version>1.4.0</version>
    </dependency>
    <!-- snail-job 客户端核心依赖 -->
    <dependency>
        <groupId>com.aizuda</groupId>
        <artifactId>snail-job-client-job-core</artifactId>
        <version>1.4.0</version>
    </dependency>
</dependencies>

客户端代码

Dto对象

这个对象用于在几个定时任务的上下文中进行传输。

arduino 复制代码
@Data
public class BillDto {
    /**
     * 账单ID
     */
    private Long billId;
    /**
     * 账单渠道
     */
    private String billChannel;
    /**
     * 账单日期
     */
    private String billDate;
    /**
     * 账单金额
     */
    private BigDecimal billAmount;
​
}

微信账单任务

该任务就是用于微信账单的统计任务。该任务从工作流的上下文中获得一个settlementDate清算日期值,如果是sysdate就设置当前天为账单日期。并且把BillDto放到上下文中,供后面的任务使用。代码如下:

less 复制代码
@Component
@JobExecutor(name = "wechatBillTask")
public class WechatBillTask {
    public ExecuteResult jobExecute(JobArgs jobArgs) throws InterruptedException {
        BillDto billDto = new BillDto();
        billDto.setBillId(123456789L);
        billDto.setBillChannel("wechat");
        // 从上下文中获得清算日期并设置,如果上下文中清算日期
        // 是sysdate设置为当前日期;否则取管理页面设置的值
        String settlementDate = (String) jobArgs.getWfContext().get("settlementDate");
        if(StrUtil.equals(settlementDate, "sysdate")) {
            settlementDate = DateUtil.today();
        }
        billDto.setBillDate(settlementDate);
        billDto.setBillAmount(new BigDecimal("1234.56"));
        // 把billDto对象放入上下文进行传递
        jobArgs.appendContext("wechat", JSONUtil.toJsonStr(billDto));
        SnailJobLog.REMOTE.info("上下文: {}", jobArgs.getWfContext());
        return ExecuteResult.success(billDto);
    }
}

补充说明:

我这里采用注解方式来实现定时任务。如果想通过实现类方式请自行参考前面的几篇文章。

支付宝账单任务

支付宝账单任务同微信账单任务。

less 复制代码
@Component
@JobExecutor(name = "alipayBillTask")
public class AlipayBillTask {
    public ExecuteResult jobExecute(JobArgs jobArgs) throws InterruptedException {
        BillDto billDto = new BillDto();
        billDto.setBillId(23456789L);
        billDto.setBillChannel("alipay");
        // 设置清算日期
        String settlementDate = (String) jobArgs.getWfContext().get("settlementDate");
        if(StrUtil.equals(settlementDate, "sysdate")) {
            settlementDate = DateUtil.today();
        }
        billDto.setBillDate(settlementDate);
        billDto.setBillAmount(new BigDecimal("2345.67"));
        // 把billDto对象放入上下文进行传递
        jobArgs.appendContext("alipay", JSONUtil.toJsonStr(billDto));
        SnailJobLog.REMOTE.info("上下文: {}", jobArgs.getWfContext());
        return ExecuteResult.success(billDto);
    }
}

汇总账单任务

该任务就是从上下文取得微信、支付宝账单内容,把dto中的金额进行汇总。代码如下:

ini 复制代码
@Component
@JobExecutor(name = "summaryBillTask")
public class SummaryBillTask {
    public ExecuteResult jobExecute(JobArgs jobArgs) throws InterruptedException {
        // 获得微信账单
        BigDecimal wechatAmount = BigDecimal.valueOf(0);
        String wechat = (String) jobArgs.getWfContext("wechat");
        if(StrUtil.isNotBlank(wechat)) {
            BillDto wechatBillDto = JSONUtil.toBean(wechat, BillDto.class);
            wechatAmount = wechatBillDto.getBillAmount();
        }
        // 获得支付宝账单
        BigDecimal alipayAmount = BigDecimal.valueOf(0);
        String alipay = (String) jobArgs.getWfContext("alipay");
        if(StrUtil.isNotBlank(alipay)) {
            BillDto alipayBillDto = JSONUtil.toBean(alipay, BillDto.class);
            alipayAmount = alipayBillDto.getBillAmount();
        }
        // 汇总账单
        BigDecimal totalAmount = wechatAmount.add(alipayAmount);
        SnailJobLog.REMOTE.info("总金额: {}", totalAmount);
        return ExecuteResult.success(totalAmount);
    }
}

服务端配置

定时任务配置

  1. 微信对账单任务

    配置项 配置内容
    任务名称 微信对账单
    状态 启用
    任务类型 集群
    自定义执行器 wechatBillTask
    路由策略 轮询
    阻塞策略 丢弃
    触发类型 工作流
    超时时间 60秒
    最大重试次数 0
    重试间隔 1秒

    说明:

    这里很关键的一个配置项是触发类型。当设置为工作流触发时,阻塞策略以工作流的策略为准,所以这里配置什么已经不重要了。

  2. 支付宝对账单任务

    配置项 配置内容
    任务名称 支付宝对账单
    自定义执行器 alipayBillTask

    其他配置项同上面的微信对账单任务

  3. 汇总账单金额任务

    配置项 配置内容
    任务名称 汇总账单金额
    自定义执行器 summaryBillTask

    其他配置项同上面的微信对账单任务

工作流配置

  1. 工作流总体配置

    说明:

    • 这里是对整个工作流的配置,这里设置的触发类型和间隔时间,实际上是调用具体定时任务的时间

    • 执行超时时间,是所有任务执行完成的超时时间。所以最好根据实际情合理的设置。

    • 阻塞策略,同理也是该工作流下所有任务的阻塞策略。

      阻塞策略 策略功能 解释
      丢弃 放弃创建新工作流批次 你都没干完,我不干
      覆盖 关闭还在执行的工作流批次,执行新工作流批次 你别干了,我来干
      并行 不管正在执行的工作流批次,直接执行新的工作流批次 直接开干
      恢复 执行正在执行中的工作流批次中的失败任务(非重试) 继续把失败的任务干完
    • 工作流上下文,可以通过点击减号去掉不用设置。该例中是需要的。

    • 节点状态。这里关闭或者开启该工作流。

  2. 微信对账单任务

    说明:

    • 优先级:同一级中数字越小越先执行

    • 失败策略:

      类型 说明
      跳过 工作流会跳过当前失败任务,继续向下执行,并且若最终执行完成, 工作流状态会是处理成功。
      阻塞 工作流会被阻塞在当前任务阶段,等待手动重试。若重试成功, 工作流还会自动向下执行未执行完的任务。
    • 节点状态:

      类型 说明
      开启 任务流调度会经过该节点。
      关闭 任务流调度会跳过该节点。

      就目前的1.4.0版本而言,如果把一个节点状态设置为关闭后,记得要把失败策略手动设置为跳过。不然工作流还是会阻塞住。这个开发团队后面可能会进行调整。

  3. 支付宝账单任务

  4. 汇总账单金额任务

  5. 整体配置后效果图

验证测试

启动服务端和客户端后,查看工作流的执行批次。

本节目标2

这里想使用工作流中的决策节点。这里增加一个需求,要按照上下文中的channel渠道如果是wechat走微信对账单任务;其他走支付宝对账单任务。

目标2实现

客户端代码

不用改动

工作流配置

增加微信任务条件

表达式类型包括:SpEl、Aviator和QL。我这里就知道SpEl,就拿这个试试。不过受到安全限制,这里的SpEl上下文是用SimpleEvaluationContext。我这里列出和StandardEvaluationContextd的区别。

特性 SimpleEvaluationContext StandardEvaluationContext
设计目的 限制功能以提高安全性,适用于简单场景 提供完整功能,但可能引入安全风险
默认注册的 Java 类型 自动注册 java.lang 包下的所有类
允许 T() 类型引用 默认禁用 默认启用
适用场景 数据绑定、简单表达式 复杂表达式、需要访问静态方法/类

说明:

  1. 表达式类型: 这里我仅仅会SpEl表达式
  2. 条件表达式:这里就是判断上下文中的channel是不是等于wechat
  3. 模拟上下文:这里单纯用来测试验证用的,这里设置的值,并不会影响实际上下文中的值

添加任务节点

在上面的微信渠道任务条件下,把微信对账单任务添加进来。这个和前面目标1介绍的方式是一样的。

支付宝的任务节点添加到其他情况这个分支中,汇总账单金额任务不变,最后的整体流程如下:

验证测试

  1. 渠道是wechat

    less 复制代码
    2025-03-28 16:46:21 [snail-grpc-server-5] INFO  c.a.s.c.job.core.client.JobEndPoint
     - 批次:[8260] 任务调度成功. 
    2025-03-28 16:46:21 [snail-job-job-8,260-1] INFO  c.mayuanfei.workflow.WechatBillTask
     - 上下文: {channel=wechat, settlementDate=sysdate}
    2025-03-28 16:46:21 [snail-job-job-8,260-1] INFO  c.a.s.c.j.c.e.JobExecutorFutureCallback
     - 任务执行成功 taskBatchId:[8260] [{"status":1,"result":{"billId":123456789,"billChannel":"wechat","billDate":"2025-03-28","billAmount":1234.56},"message":"任务执行成功"}]
    2025-03-28 16:46:22 [snail-job-grpc-server-executor-11] INFO  c.a.s.c.c.rpc.server.GrpcInterceptor
     - method invoked: UnaryRequest/unaryRequest cast:0ms
    2025-03-28 16:46:22 [snail-grpc-server-6] INFO  c.a.s.c.job.core.client.JobEndPoint
     - 批次:[8262] 任务调度成功. 
    2025-03-28 16:46:22 [snail-job-job-8,262-1] INFO  c.mayuanfei.workflow.SummaryBillTask
     - 总金额: 1234.56
    2025-03-28 16:46:22 [snail-job-job-8,262-1] INFO  c.a.s.c.j.c.e.JobExecutorFutureCallback
     - 任务执行成功 taskBatchId:[8262] [{"status":1,"result":1234.56,"message":"任务执行成功"}]

    从测试结果看,只有微信任务和汇总任务执行了。

  2. 渠道是alipay

    less 复制代码
    2025-03-28 16:49:25 [snail-grpc-server-11] INFO  c.a.s.c.job.core.client.JobEndPoint
     - 批次:[8276] 任务调度成功. 
    2025-03-28 16:49:25 [snail-job-job-8,276-1] INFO  c.mayuanfei.workflow.AlipayBillTask
     - 上下文: {channel=alipay, settlementDate=sysdate}
    2025-03-28 16:49:25 [snail-job-job-8,276-1] INFO  c.a.s.c.j.c.e.JobExecutorFutureCallback
     - 任务执行成功 taskBatchId:[8276] [{"status":1,"result":{"billId":23456789,"billChannel":"alipay","billDate":"2025-03-28","billAmount":2345.67},"message":"任务执行成功"}]
    2025-03-28 16:49:25 [snail-job-grpc-server-executor-23] INFO  c.a.s.c.c.rpc.server.GrpcInterceptor
     - method invoked: UnaryRequest/unaryRequest cast:0ms
    2025-03-28 16:49:25 [snail-grpc-server-12] INFO  c.a.s.c.job.core.client.JobEndPoint
     - 批次:[8277] 任务调度成功. 
    2025-03-28 16:49:25 [snail-job-job-8,277-1] INFO  c.mayuanfei.workflow.SummaryBillTask
     - 总金额: 2345.67
    2025-03-28 16:49:25 [snail-job-job-8,277-1] INFO  c.a.s.c.j.c.e.JobExecutorFutureCallback
     - 任务执行成功 taskBatchId:[8277] [{"status":1,"result":2345.67,"message":"任务执行成功"}]

    从测试结果看,只有支付宝和汇总任务执行了。

本节目标3

这里想使用工作流中的回调通知。这里增加一个需求,就是在汇总账单金额任务执行完成后调用一个接口,把最后汇总的金额通知给这个接口。

目标3实现

接受回调的Controller

less 复制代码
@Slf4j
@RestController
@RequestMapping("/workflow")
public class WorkflowCallbackController {
​
    @PostMapping("/callback")
    public void callback(@RequestBody CallbackParamsDTO callbackParams, 
                         @RequestHeader HttpHeaders headers) {
        // callbackParams 对象可以获取到当前回调通知之前的上下文内容
        // secret 是当回调通知的秘钥,用于鉴权
        String secret = headers.getFirst("secret");
        log.info("callback: {}, secret:{}", callbackParams, secret);
        log.info("完成任务推送到监控");
    }
}

说明:

  • CallbackParamsDTO可以接受回调的参数。当然这里需要引用snail-job的包,如果是通知到其他服务,这里可以用字符串接收。
  • header中有秘钥的信息。通过该秘钥可以鉴权。
  • 回调可以做很多业务以为的事情。这里就是结合实际情况进行运用了。可能更多情况是不需要用到回调的,但是如果当你需要某个工作流执行完成后,再做一些额外的事情。那么这个webhook就很有用了。

增加回调通知

  1. 调出添加对话框

  2. 添加回调通知

    说明

    • webhook: 回调地址
    • 请求类型:可以是json或者form表单
    • 秘钥:通讯秘钥
    • 回调通知状态:这里估计是页面写错误。这里的工作流状态其实应该是通知的状态。

总结

  • 工作流其实就是把多个普通定时进行编排,从而形成一个有前后执行顺序甚至是有条件的流程任务。
  • 上下文传获得参数通过jobArgs.getWfContext()
  • 上下文添加参数通过jobArgs.appendContext()
  • 如果一个定时任务的触发方式是工作流,那么这个定时任务的阻塞策略以工作流的策略为准
  • 工作流的任务节点,如果想关闭该节点。记得几点状态选关闭时,失败策略要选跳过。
  • 添加节点的SpEl采用的上下文是SimpleEvaluationContext所以无法引用很多java内置方法。
  • 回调通知获得秘钥通过:headers.getFirst("secret");
  • 回调通知接收可以通过CallbackParamsDTO这个snail-job内置对象;也可以通过字符串接收。
相关推荐
why1516 小时前
腾讯(QQ浏览器)后端开发
开发语言·后端·golang
浪裡遊6 小时前
跨域问题(Cross-Origin Problem)
linux·前端·vue.js·后端·https·sprint
声声codeGrandMaster6 小时前
django之优化分页功能(利用参数共存及封装来实现)
数据库·后端·python·django
呼Lu噜6 小时前
WPF-遵循MVVM框架创建图表的显示【保姆级】
前端·后端·wpf
bing_1586 小时前
为什么选择 Spring Boot? 它是如何简化单个微服务的创建、配置和部署的?
spring boot·后端·微服务
学c真好玩7 小时前
Django创建的应用目录详细解释以及如何操作数据库自动创建表
后端·python·django
Asthenia04127 小时前
GenericObjectPool——重用你的对象
后端
Piper蛋窝7 小时前
Go 1.18 相比 Go 1.17 有哪些值得注意的改动?
后端
excel7 小时前
招幕技术人员
前端·javascript·后端
盖世英雄酱581367 小时前
什么是MCP
后端·程序员