规则引擎设计思路

规则引擎设计思路

前言

本文代码于GitHub - felixlyd/rule-liteflow: 基于liteflow实现的规则demo

思路

打造规则引擎时,规则引擎的接口报文需要很灵活,需要很高的拓展性,请告诉我该如何设计

在设计规则引擎接口报文时,一种可能的方法是使用一种结构化的数据格式,如 JSON 或 XML。这样,你可以在不改变接口定义的情况下添加或删除报文中的字段,从而增加拓展性。此外,还可以考虑使用一种灵活的数据类型,例如字典或映射,以允许开发人员在不改变接口定义的情况下添加额外的字段。

结构化的数据例如JSON在规则引擎收到后,需要转换为java对象,如何设计这个java对象以实现较高的拓展性

在将 JSON 数据转换为 Java 对象时,一种可能的方法是使用一个抽象类来定义 Java 对象的基本结构,并使用继承关系来为不同类型的 JSON 数据定义具体的 Java 对象。这样,你可以在不改变抽象类的情况下增加新类型的 JSON 数据,从而增加拓展性。此外,还可以考虑为抽象类添加一个字段来存储额外的字段,从而允许开发人员在不改变现有 Java 对象的情况下添加额外的字段。

假设有额外的字段内容,json格式的请求数据也转换为了如你所说的java对象,那么后续servcie层依旧需用到这个额外字段,service层又应该如何设计呢?

如果你想在 service 层使用来自请求数据的额外字段,一种可能的方法是定义一个泛型类来作为 service 的基类,并使用继承关系来为不同类型的请求数据定义具体的 service 类。这样,你可以在不改变基类的情况下增加新类型的请求数据,从而增加拓展性。此外,还可以考虑在基类中添加一个字段来存储额外的字段,并在具体的 service 类中提供访问这个字段的方法。这样,service 类就可以通过这个方法来访问额外的字段。

规则引擎中有一个重要的内容是上下文设计,上下文设计中一般包括请求数据,业务数据和最终的响应数据,我应该如何设计上下文对象,让它有较高的可用性和拓展性

在设计上下文对象时,一种可能的方法是定义一个抽象类来表示上下文对象的基本结构,并使用继承关系来为不同的业务场景定义具体的上下文对象。这样,你可以在不改变抽象类的情况下增加新的业务场景,从而增加拓展性。此外,你还可以为抽象类定义一些通用的方法,例如访问请求数据、业务数据和响应数据的方法,从而提高上下文对象的可用性。

假设上下文对象是一个抽象类,A业务场景和B业务场景的请求数据、业务数据和响应数据有相同的字段有也不同的字段,我应该如何定义它们的上下文对象?请举例说明

如果 A 业务场景和 B 业务场景的请求数据、业务数据和响应数据有相同的字段,你可以在抽象类中定义一些通用的字段,并在 A 业务场景和 B 业务场景的上下文对象中继承这些通用字段。如果 A 业务场景和 B 业务场景的请求数据、业务数据和响应数据有不同的字段,你可以在 A 业务场景和 B 业务场景的上下文对象中分别定义这些不同的字段。 例如,如果 A 业务场景和 B 业务场景都需要访问用户 ID 和交易金额,你可以在抽象类中定义两个通用字段:

public abstract class AbstractContext {
    private String userId;
    private Double amount;

    // getters and setters
}

然后,你可以在 A 业务场景的上下文对象中继承这两个通用字段,并定义一些额外的字段:

public class AContext extends AbstractContext {
    private String product;
    private String location;

    // getters and setters
}

同理,你可以在 B 业务场景的上下文对象中继承这两个通用字段,并定义一些额外的字段:

public class BContext extends AbstractContext {
    private String category;
    private String date;

    // getters and setters
}

这样,你就可以为 A 业务场景和 B 业务场景定

那么相当于我每次新增一个业务场景,都需要新增或修改上下文对象。我希望的是设计一个通用的上下文对象,能够覆盖大多数业务场景,应该如何设计?

如果你希望设计一个通用的上下文对象来覆盖大多数业务场景,你可以考虑定义一个抽象类,并使用结构化的数据类型(如字典或映射)来存储所有的请求数据、业务数据和响应数据。这样,开发人员就可以在不改变上下文对象的情况下,随时为不同的业务场景添加额外的字段。 例如,你可以定义一个抽象类,并在该类中定义三个映射对象:

public abstract class AbstractContext {
    private Map<String, Object> request;
    private Map<String, Object> data;
    private Map<String, Object> response;

    // getters and setters
}

然后,你可以在具体的业务场景中继承这个抽象类,并使用这三个映射对象来存储请求数据、业务数据和响应数据。例如:

public class AContext extends AbstractContext {
    public AContext() {
        request.put("user_id", "123456");
        request.put("amount", 100.0);

        data.put("product", "book");
        data.put("location", "New York");

        response.put("status", "success");
        response.put("message", "Transaction completed");
    }
}

这样,开发人员就可以在不改变上下文对象的情况下,为 A 业务场景添加任意数量的请求数据、业务数据和响应数据。 此外,你还可以为抽象类定义一些通用的方法,例如在

例如在

例如,你可以为抽象类定义一些通用的方法,例如在三个映射对象中添加、删除或更新字段的方法,或者获取指定字段的值的方法。例如:

public abstract class AbstractContext {
    private Map<String, Object> request;
    private Map<String, Object> data;
    private Map<String, Object> response;

    public void addRequestField(String key, Object value) {
        request.put(key, value);
    }

    public void removeRequestField(String key) {
        request.remove(key);
    }

    public void updateRequestField(String key, Object value) {
        request.put(key, value);
    }

    public Object getRequestField(String key) {
        return request.get(key);
    }

    // other methods for data and response
}

这样,开发人

这样,开发人

这样,开发人员就可以在 A 业务场景的上下文对象中调用这些方法来访问请求数据、业务数据和响应数据。例如:

AContext context = new AContext();
context.addRequestField("product_id", "abc123");
context.updateRequestField("amount", 120.0);
Object productId = context.getRequestField("product_id");

这样,你就可以为大多数业务场景提供一个通用的上下文对象,并提高它的可用性和拓展性。

好,上下文对象的设计我明白了。现在我想问另一个问题,在规则引擎中,可能需要外部数据,例如去查某个客户有多少钱需要访问外部接口。我一个业务场景中,可能有三个规则都需要知道客户有多少钱,但我不应该访问三次外部接口,这样效率太低了。我应该怎么设计,才能够仅需访问一次外部接口呢?

在规则引擎中,如果多个规则需要访问同一个外部接口,你可以考虑将外部数据放入上下文对象中,并在规则执行过程中访问上下文对象来获取数据。这样,外部接口只需要被访问一次,以获取需要的数据,然后将数据放入上下文对象中,供多个规则使用。 例如,你可以将上下文对象定义为如下所示:

public class CustomerContext extends AbstractContext {
    private String customerId;
    private Double balance;

    public CustomerContext(String customerId) {
        this.customerId = customerId;

        // Call external API to get customer balance
        this.balance = getCustomerBalance(customerId);
    }

    public String getCustomerId() {
        return customerId;
    }

    public Double getBalance() {
        return balance;
    }

    private Double getCustomerBalance(String

如下所示

如下所示:

public class CustomerContext extends AbstractContext {
    private String customerId;
    private Double balance;

    public CustomerContext(String customerId) {
        this.customerId = customerId;

        // Call external API to get customer balance
        this.balance = getCustomerBalance(customerId);
    }

    public String getCustomerId() {
        return customerId;
    }

    public Double getBalance() {
        return balance;
    }

    private Double getCustomerBalance(String customerId) {
        // Call external API to get customer balance
        return 100.0;
    }
}

然后,你可以在规则执行过程中访问上下文对象来获取客户的信息:

CustomerContext context = new CustomerContext("123456");
String customerId = context.getCustomerId();
Double balance = context.getBalance();

这样,外部接口就只需要在创建上下文对象时

这样?

是的,你可以通过这种方式来仅访问一次外部接口,以获取所需的数据。在创建上下文对象时,你可以通过调用外部接口来获取需要的数据,并将数据存储到上下文对象中。然后,在规则执行过程中,你可以通过访问上下文对象来获取这些数据,而无需再次调用外部接口。 这样,你就可以在规则引擎中仅访问一次外部接口,提高程序的效率。

按你所说的去调用外部接口,需要提前知道规则脚本中需要哪些外部接口的数据。而实际上,规则脚本并没有办法提前写好,而是在规则引擎系统上线后,业务人员增加规则脚本的时候,系统才知道引用了外部接口的数据。我可以提前准备好调用外部接口的代码,但如何设计才能够让系统感知到规则脚本中的那些需要调用外部接口的数据,并且仅访问一次外部接口

在这种情况下,你可以在上下文对象中添加一个方法,用于动态获取外部接口的数据。例如:

public class CustomerContext extends AbstractContext {
    private String customerId;
    private Map<String, Object> data;

    public CustomerContext(String customerId) {
        this.customerId = customerId;
        this.data = new HashMap<>();
    }

    public String getCustomerId() {
        return customerId;
    }

    public Object getExternalData(String key) {
        if (!data.containsKey(key)) {
            // Call external API to get data and put it in the map
            data.put(key, getExternalDataFromApi(key));
        }
        return data.get(key);
    }

    private Object getExternalDataFromApi(String key) {
        // Call external API and return the data
        return 100.0;
    }
}

这样,在规则执行过程中,你可以调用 getExternalData 方法来获取外部接口的数据,如果数据在内存中已经存在,则直接返回;否则,调用外部接口获取数据,并将数据存储在内存中,供下次使用。 这样,你就可以在规则脚本中动态获取需要的外部数据,同时保证数据仅被访问一次,提高程序的效率。

这样写代码就会变成,我每次实现一个外部接口的调用,都需要将外部接口的响应数据的字段作为key写到这个getExternalData方法中。如此一来,外部接口发生变化后,我也需要修改我的代码。相当于我的代码随外部系统的变化而变化,这是很差的程序设计。希望你能提出更好的建议

是的,如果每次实现一个外部接口时都需要在代码中手动添加对应的字段作为key,这样的确不是很优秀的设计方式。 一种更好的方式是使用反射机制,让程序在运行时自动获取外部接口返回的数据字段。例如,你可以将上下文对象定义为如下所示:

public class CustomerContext extends AbstractContext {
    private String customerId;
    private Map<String, Object> data;

    public CustomerContext(String customerId) {
        this.customerId = customerId;
        this.data = new HashMap<>();
    }

    public String getCustomerId() {
        return customerId;
    }

    public Object getExternalData(String key) {
        if (!data.containsKey(key)) {
            // Call external API to get data and put it in the map
            data.put(key, getExternalDataFromApi(key));
        }
        return data.get(key);
    }

    private Object getExternalDataFromApi(String key) {
        // Call external API and return the data
        ExternalDataResponse response = callExternalApi(key);

        // Use reflection to get the field value from the response object
        try {
            Field field = response.getClass().getDeclaredField(key);
            field.setAccessible(true);
            return field.get(response);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            // Handle exception
            return null;
        }
    }
}

这样,在规则执行过程中,

这样,在规则执行过程中?

对,在规则执行过程中,你可以通过调用 getExternalData 方法来获取外部接口返回的数据。例如:

CustomerContext context = new CustomerContext("123456");
Object balance = context.getExternalData("balance");

在这种情况下,程序会自动调用外部接口并获取响应的数据,然后使用反射机制从响应数据对象中获取 balance 字段的值,并将其返回给调用方。 这样,你就不需要在代码中手动添加外部接口返回的字段作为key,程序会自动根据实际情况来获取数据,从而避免了因外部

从而避免了?

从而避免了因外部接口变化而导致的代码变更。 例如,如果外部接口的返回数据发生了变化,例如增加了一个新的字段,那么程序会自动识别新增的字段,并且可以通过调用 getExternalData 方法来获取新增字段的值。 这样,你的代码就不需要因为外部接口的变化而发生改变,更加灵活和稳定。

实际场景中,规则脚本中可能既需要内部数据又需要外部数据。内部数据通常指请求数据或者是根据请求数据查询数据库获得的数据,外部数据则是上面我们讨论的调用外部接口获取的数据。因此,设计上下文时,不能仅用getExternalData方法来获取数据。或许需要将请求数据、数据库获取的数据、外部数据分开?请告诉我应该如何设计

在设计规则脚本中获取数据时,需要考虑到数据的来源和使用方式。如果需要获取请求数据或数据库中的数据,可以将这些数据作为规则脚本的输入参数,在规则脚本中直接使用。如果需要获取外部数据,可以在规则脚本中调用特定的方法(例如getExternalData)来获取外部数据,并将获取到的数据作为方法的返回值使用。这样做可以保证数据的来源和使用方式的一致性,并且可以更好地管理数据。

业务人员在写规则脚本时,并不知道这些数据到底是内部数据还是外部数据。

那么,为了方便业务人员写规则脚本,可以在规则脚本的文档中详细说明数据的来源和使用方式,包括哪些数据是内部数据,哪些是外部数据,如何获取这些数据等内容。这样,业务人员在写规则脚本时就可以根据文档中的信息来使用这些数据。此外,为了进一步简化业务人员的操作,可以为规则脚本提供一些辅助工具,例如数据管理工具或调试工具,来帮助业务人员管理和使用数据。

我想通过良好的设计来使得业务人员无需关心这些数据是内部数据还是外部数据

如果想让业务人员在写规则脚本时无需关心数据的来源和使用方式,那么可以通过提供一个统一的、易于使用的数据访问接口来实现。这个接口可以封装内部数据和外部数据的获取方式,并统一提供给业务人员使用。业务人员可以通过这个接口来获取所需的数据,而无需关心数据的来源和使用方式。这样,业务人员就可以专注于业务逻辑的实现,而无需关心数据方面的细节。

封装这样的接口,我认为需要增加一张数据库的表,来存储数据字段名称和数据来源类型

是的,为了封装一个统一的数据访问接口,可以考虑在数据库中增加一张表来存储数据字段名称和数据来源类型。这张表可以用来管理所有可用的数据字段,并且提供一种方便的方式来查询数据字段的来源类型。这样,在封装统一的数据访问接口时,可以根据数据字段的来源类型来获取对应的数据,而无需关心具体的实现细节。

实现

术语定义

名词概念 概念描述
liteflow组件-component liteflow框架组件,可以由java实现,继承liteflow组件类或者添加了liteflow组件注解,可交由spring管理;也可以由groovy、JavaScript等脚本语言实现
liteflow规则链-chain liteflow规则流程,由一或多个特定逻辑顺序编排的liteflow组件实现

系统概述

系统体系结构

规则引擎架构设计示意图

功能模块

按照微服务思想,划分不同功能模块,并单独部署

规则引擎功能模块设计示意图

程序设计详细描述

规则判定设计说明

在controller层级提供一个入口,根据交易码来调用service层。每个service层调用一个或者多个liteflow规则链(chain)。每个chain由多个按特定逻辑顺序编排的liteflow组件组成。liteflow组件可以由java类实现,也可以基于脚本语言实现(脚本语言可选QLExpress、javaScript、Groovy,这里选择Groovy),脚本以字符串的形式存储在数据库表中。在chain中,有一个上下文java对象贯穿每一个liteflow组件。

规则判定具体实现示意图

请求报文在controller层组装为请求业务对象,传入service层。在service层中,请求业务对象数据组装为上下文对象,传入chain。按照chain的特定逻辑顺序执行liteflow组件中的代码时,从上下文对象获取对应的输入数据,并将输出数据放入上下文对象中。chain执行完毕后,service层从上下文对象中获取响应数据并组装响应业务对象,再由controller层组装为响应报文。

规则判定数据流转示意图

请求对象设计

请求对象设计为JSON的结构化数据。 反序列化时,会用泛型类接收请求报文的body,而header和其他元信息(时间、流水)会用具体的字段属性接收。 接收请求报文后,会对请求报文的字段属性进行校验。设计必需属性校验,例如流水号、渠道码等属性的校验。必需属性校验的相关配置可以存入到数据库中,在程序启动时以缓存的形式加载到程序中。

响应对象设计

响应对象设计为两类,一类只有相对简略的最终的结果,一类包含详细的每个规则的输出结果,具体返回哪一种数据,由请求报文中的结果属性字段判定。 响应对象为按一定模板设计的JSON结构化数据。模板里肯定包括流水号、渠道码等必须属性。

上下文对象设计

上下文对象由请求数据、上下文数据、响应数据三部分数据组成。这些数据均设计为JSON的结构化数据,根据key值来调用getter和setter。

外部数据获取设计说明

liteflow组件在使用外部数据时,如果是在同一个chain之中,则认为是使用同一外部数据,即同一个chain只会外呼外部接口一次来获取同一个外部数据。判定依据为数据源的名称,即便是调用同一外部接口,不同的数据源名称则会发起一次新的调用。

**实现方式1:**在执行chain中的组件之前,扫描组件所需的输入数据,如果有外部数据,则先进行外呼,将外呼响应数据存储到内存中。(推荐使用) **实现方式2:**执行chain,遇到外部数据时,如果内存中有,直接使用;如果没有,先进行外呼。

对于规则而言,规则本身并不知道所需数据是外部数据、内部数据、数据库数据或者是配置文件数据。因此,在配置规则前,需要先配置数据信息,这些配置信息存入数据库中,在程序启动时以缓存的形式加载到程序中。

数据库数据获取设计说明

数据库数据、配置文件数据获取的设计均参照外部数据获取的设计。

运用消息中心

规则引擎的响应数据除了返回给渠道,也会推送给消息队列。消息队列中的数据会持久化于数据库中,用于监控、交易追踪、数据核查等。消息生产者-规则引擎与消息消费者解耦,可以减少规则引擎的响应时间。

规则链设计

在liteflow中,规则链中可以嵌套规则链,每个规则链由按特定顺序编排的若干liteflow组件组成。liteflow组件可以用java类或规则脚本实现。规则链则按liteflow语法配置化实现。规则脚本和规则链持久化在数据库中,在程序启动时以缓存的形式加载到程序中。 在设计数据表和后管接口时,要考虑规则链修改、逻辑删除的权限,例如,某个规则链正在使用,是否能修改或者逻辑删除。

数据库设计

前缀规范:

  • tmp_ 临时表
  • v_ 版本表
  • his_ 历史表
  • r_ 关系表
  • b_xxx_ xxx业务表
  • d_ 字典表(地区表、机构表等基本不被业务逻辑修改的表)

规则脚本表 liteflow_script

规则脚本表存储了规则脚本的ID、内容、类型,以及时间、人员等元数据 type分为6种:

  • script:普通脚本节点,脚本里无需返回。
  • switch_script:选择脚本节点,脚本里需要返回选择的节点Id。
  • if_script:条件脚本节点,脚本里需要返回true/false。
  • for_script:数量循环节点,脚本里需要返回数值,表示循环次数。
  • while_script:条件循环节点,脚本里需要返回true/false,表示什么条件才继续循环。
  • break_script:退出循环节点,脚本里需要返回true/false,表示什么时候退出循环。

status分为3种:

  • enabled:启用
  • disabled:禁用
  • deleted:逻辑删除

状态的改变不会增加新版本

规则脚本版本表 v_liteflow_script

每次新增和修改规则脚本的描述、内容、类型等字段,则会新增版本记录

规则链表 liteflow_chain

规则链表存储了规则链的ID、内容、层级,以及时间、人员等元数据 chain_level考虑为两级:

  • lv1:第1级
  • lv2:第2级

status分为3种:

  • enabled:启用
  • disabled:禁用
  • deleted:逻辑删除

规则链版本表 v_liteflow_chain

每次新增和修改规则链的描述、内容、层级等字段,则会新增版本记录

规则链-规则链关系表 r_chain_chain

记录了规则链和规则链的关系,保证了当规则链被使用时,无法逻辑删除和禁用。

脚本-规则链关系表 r_script_chain

记录了脚本和规则链的关系,保证了当脚本被使用时,无法逻辑删除和禁用。

规则脚本数据字段表 script_field

记录了规则脚本中使用到的数据字段信息,以及数据字段的来源信息以及在来源中对应的字段名。

数据源配置属性表 data_source_properties

记录了数据源的配置信息,外部数据、数据库数据等的特殊配置信息例如外部数据中涉及的响应数据等,以及数据库数据中涉及的表名、表字段名等,用json字符串存储。 source_type有以下几类:

  • 00:请求数据,来源于请求报文的数据
  • 01:外部数据,来源于请求外部接口获得的响应报文中的数据
  • 02:数据库数据,来源于本数据库中数据表的字段数据
  • 03:配置文件数据,来源于配置文件中的字段数据

数据源JSON设置表 d_source_json_settings

数据源存储的json字符串必须按照一定格式配置,因此这里增加一张字典表来记录数据源json配置信息 数据源json设置限制条件不能过于苛刻,source_json_settings示例: 数据库数据配置:

{
	"table_name":"string(100)",
	"field_names":"list(100)",
	"search_condition_fields":"list(20)"
}

外部数据配置:

{
	"service_code":"string(100)",
	"response_fields":"list(100)",
	"request_fields":"list(100)"
}

配置文件数据配置:

{
	"yaml_name":"string(1000)",
	"field_names":"list(100)"
}

这里有一个潜在问题,嵌套字段应该如何设计json设置?

API 设计

讨论点

数据表设计中是否需要考虑审核业务?

需要,后续完善

是否引入内存数据库redis?

为了提升响应速度,许多配置信息会在程序启动时以缓存的形式加载到程序中。当配置信息过多时,例如说超过了程序运行的机器内存的一半,可能需要引入redis。 需要