URULE规则引擎——客户端服务器模式

简介

URULE PRO有嵌入式模式,客户端服务器模式,Rest模式,本地运行模式

Rest模式

有些时候客户端环境可能比较复杂,如采用非Java语言编写的客户端,如Javascript、C++或C#等,或者是客户端不希望加入URule Pro的相关Jar包等等,但这些客户端也需要调用规则引擎进行业务规则的计算,这个时候就需要用传统的独立服务模式。

嵌入式模式

嵌入模式就是将URule Pro的Console和Core以及其依赖的第三个Jar包嵌入到当前正在开发的项目中,将其作为项目的一部分运行。在这种模式下,我们可以通过代码直接访问Console包中相关API,直接操作URule Pro的表数据

本地运行模式

这种模式非常适用于规则运行环境封闭,且需要对外部屏蔽规则设计细节的应用需要;其部署模式简单、快捷,一旦有新的知识包放入指定目录中,客户端应用会自动检测并加载新的版本。

我们主要介绍一下客户端服务器模式的部署应用 :

在客户端服务器模式下,包含URule Pro Console模块的应用被部署成一个独立的Server,在这个Server上创建规则项目,在项目中根据业务需求添加决策集、决策表、交叉决策表、决策树、评分卡、复杂评分卡、决策流等,再把这些文件打包到知识包中,最后通过HTTP协议暴露给各个客户端业务系统使用。

其运行示意图如下所示:

在客户端服务器模式下,URule Server上给客户端提供的是若干个已经构建好的知识包对象。 当客户端需要进行规则计算时,它会检查当前客户端中配置的urule.knowledgeUpdateCycle属性值,如果为0,那么就直接请求URule Server获取指知识包;如果为1,那么它会首先检查客户端本地缓存当中是否存在指定的知识包,如果存在,就取本地缓存中的,如果不存在,则到URule Server下请求指定的知识包,然后将请求到的知识包缓存到客户端内存中,这样下次就不再到URule Server上下载;如果urule.knowledgeUpdateCycle属性值大于1,那么客户端会首先检查本地缓存中是否存在指定的知识包,如果存在,那么就拿当前时间与本地缓存中的知识包的时间戳进行比较,如果小于urule.knowledgeUpdateCycle属性值,那么就直接取这个知识包,如果大于它,那么就到URule Server上通过时间戳检查当前知识包有没有更新,如果有更新则取到客户端,同时更新客户端缓存里对应的知识包;如果没有更新,那么就直接采用当前客户端缓存里的知识包。

urule.knowledgeUpdateCycle属性默认值为0,一般情况下,如果我们的客户端为一个Java Web应用,只需要将这个属性置为1即可,因为客户端为Web应用,服务端会通过推送方式让客户端知识包与服务端时刻保持一致。

如果客户端为非Web应用,那么才需要修改urule.knowledgeUpdateCycle属性值为一个大于1的数值,以实现定期检查服务端知识包是否有更新功能。

搭建Server应用

搭建URule Server应用的与安装与配置一节中介绍的完全相同,就是在一个普通的java web应用中将URule Pro Console模块和URule Pro Core添加进去。需要注意的是,在URule Server应用搭建好了之后,要保证“/urule/loadknowledge”这个URL在可以匿名访问,比如输入类似下面的地址,看看应用会有什么样的响应。

localhost:8088/urule/loadknowledge

在系统未登录的情况下,访问上述URL看到的不是一个登录页面,那么就说明“/urule/loadknowledge”这个URL在可以匿名访问,是OK的。

如果出现我们应用中的登录界面之类,那就说明“/urule/loadknowledge”这个URL被我们自己系统内部的权限模块给拦截了,需要我们在系统中对内部的权限进行配置,使得“/urule/loadknowledge”URL在可以匿名访问。做好这个工作,那么URule Server的搭建工作就完成了,下面我们看看如果配置客户端应用。

具体搭建

导入server需要的jar包

        <dependency>
            <groupId>com.bstek.urule</groupId>
            <artifactId>urule-console-pro</artifactId>
            <version>4.2.1</version>
        </dependency>
        <dependency>
            <groupId>com.bstek.urule</groupId>
            <artifactId>urule-core-pro</artifactId>
            <version>4.2.1</version>
        </dependency>

在启动类中添加注解配置

@ImportResource("classpath:urule-console-context.xml")

注册com.bstek.urule.console.URuleServlet

@Configuration
public class UruleServerConfig {
    @Bean
    public ServletRegistrationBean registerUruleServlet() {
        Map<String, String> map = new LinkedHashMap<>();
//        map.put("lastLogin","/urule/loadknowledge");
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new URuleServlet(), "/urule/*");
 //       servletRegistrationBean.setInitParameters(map);
        return servletRegistrationBean;
    }
}

配置urule数据源,我这里用的是urule内置数据源

在resources目录中新增urule-init.properties配置文件,设置urule-home目录,参考代码:

这里的路径要选择一个空文件夹

urule.home=E:/ToolIdea/uruleHome

搭建urule客户端

配置客户端应用,首先要解决的是如果将URule Pro客户端所需要的jar包添加到项目中

<dependency>
    <groupId>com.bstek.urule</groupId>
    <artifactId>urule-core-pro</artifactId>
    <version>4.2.1</version>
</dependency>

在启动类添加注解配置

@ImportResource("classpath:urule-core-context.xml")

客户端调用服务端知识包进行规则计算

  1. 客户端主动调用服务端知识包

对于URule Pro客户端应用来说,在进行规则计算时,如果客户端本地缓存没有相应的知识包,那么它会到URule Server上请求对应的知识包,要给URule Pro客户端应用配置对应的URule Server地址

在其中添加一个名为“urule.resporityServerUrl”属性,比如 urule.resporityServerUrl=http://localhost:8088/server, 就表明我们为当前URule客户端应用配置的URule Server为"http://localhost:8088/server",这样在进行规则计算时,如果客户端本地缓存没有相应的知识包,那么它会到“http://localhost:8088/server” 上请求对应的知识包。

这里的localhost:8088替换成你需要的IP比如: 192.168.18.11:8080
如果同步不成功需要检查url配置,确保url的末尾没有添加/,例如:“ http://localhost:8088/server /” 是不允许的
URule Pro中提供了动态加载Jar包及Spring配置文件功能,在客户端配置了 urule.resporityServerUrl属性后,客户端在启动时会自动到服务端检查是否有新的动态Jar文件信息需要加载,如果有则下载到客户端并加载,所以这时服务端的“ urule/dynamic/checkLatestJarsDir”和“ urule/dynamic/loadDynamicJars”这两个URL要保证匿名可访问(URULE框架默认允许匿名访问),否则客户端启动时会产生错误。
注意:当前如果客户端上配置了 urule.resporityServerUrl属性值,但这个属性对应的Server不存在,或无法访问,那么客户端在启动到服务端检查是否有新的动态Jar文件信息需要加载操作就会报错,导致客户端启用失败。

urule.resporityServerUrl属性值不匹配时找不到服务端,报错如下图

启动成功的图示

到这里,URule Pro客户端的配置就全部完成了,通过指定urule.resporityServerUrl属性值,我们的客户端无论是标准的Java Web应用还是普通的Java应用,都可以与URule Server连接,从而在运行时获取到相应的知识包。

测试如下:

使用java代码调用urule规则引擎知识包

@RequestMapping("/test2")
    public String test2() {
        try {
            //从spring中获取KnowledgeService的接口实例
            KnowledgeService service = (KnowledgeService) Utils.getApplicationContext().getBean(KnowledgeService.BEAN_ID);
            //获取知识包
            KnowledgePackage knowledgePackage = service.getKnowledge("1");
            //根据知识包获取session对象
            KnowledgeSession session = KnowledgeSessionFactory.newKnowledgeSession(knowledgePackage);
            TestData calculation = new TestData();
            calculation.setTest1("");
            session.insert(calculation);
            session.fireRules();
            return "successful";
        } catch (Exception e) {
            e.printStackTrace();
            return "error";
        }
    }

返回“successful”证明我们调用成功

如果没有配置服务端地址,测试会报下面错误

找不到知识包

  1. 服务端把知识包推送给客户端的方法

上面一种调用知识包是从客户端配置服务器端地址,主动调用,下面介绍一下服务端把知识包推送客户端,客户端被动接收。

如果我们的客户端应用是标准的Java Web应用,那么除了可以通过urule.resporityServerUrl属性指定URule Server地址,从而主动获取知识包外,还可以通过配置一个Servlet被动接收URule Server推送过来的知识包,这样模式,相比主动获取知识包方式要高效的多,同时还可以时刻与Server上的知识包保持同步。

  1. 客户端注册com.bstek.urule.KnowledgePackageReceiverServlet。

@Configuration
public class UruleClientConfig {
    @Bean
    public ServletRegistrationBean registerKnowledgeServlet() {
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(
                new KnowledgePackageReceiverServlet(), "/knowledgepackagereceiver");
        return servletRegistrationBean;
    }
}

在上面的servlet配置中,url-mapping项的“/knowledgepackagereceiver”值是固定的,不能修改为其它值,否则将无法收到URule Server上推送过来的知识包。 同时,客户端中的这个用于接收知识包推送的Servlet还要保证其可以匿名访问,也就是客户端中如果被权限框架包裹,那么要保证“/knowledgepackagereceiver”这个URL在不登录的情况下就可以访问, 否则服务端将无法把知识包推送到目标客户端。

要实现推送功能,除了要配置对应Java Web客户端上的Servlet,还需要在URule Server上对可以接收推送的客户端进行定义

  1. URule Server上客户端配置

如果当前登录用户为团队创建人员,那么在登录系统后,可以看到客户端配置入口,如下图:

我们可以添中多个客户端,这样在当前项目发布知识包时就可以自动推送到这里的客户端上,如下图:

配置完成后,进行项目的知识包管理页面,查看某个知识包的已发布知识包,在弹出窗口可以看到已发布的知识包列表,选中某条需要推送到客户端的知识包,点击右键,在弹出菜单中选择推送到客户端,如下图:

点击该菜单项,系统会尝试向这些客户端推送当前发布的知识包对象,如果这些客户端是存在的,就可以发布成功,这样对应的客户端本地缓存中就会存储这个发布的知识包对象。

测试如下,

把客户端服务器地址注释掉,然后重启客户端

在服务端没有推送之前,客户端获取知识包失败,如下图

在服务端推送知识包给客户端之后,调用如下:

  1. URULE PRO日志输出到指定文件夹

在客户端代码调用规则时,如果也要在本地查看调试信息,那么首先需要将urule.debug属性设置为true,接下来为urule.defaultHtmlFileDebugPath属性设置一个具体的已存在的目录值即可;

客户端配置如下,只需要配置urule.defaultHtmlFileDebugPath和urule.debug就可以了

urule:
#  设置为  1 表示 执行时查客户端本地缓存当中是否存在指定的知识包,如果存在,就取本地缓存中的,如果不存在,则到URule Server下请求指定的知识包,然后将请求到的知识包缓存到客户端内存中
  knowledgeUpdateCycle: 1
#配置日志输出的路径
  defaultHtmlFileDebugPath: E:/bstekPro/ideaPro/logwriter
# 开启日志
  debug: true

URULE PRO日志输出到本地, 我们需要做的就是实现一个com.bstek.urule.runtime.log.LogWriter接口的实现类,并将其配置在Spring当中,让其成为一个标准的Spring Bean

代码实现如下

package com.urule.config;


import com.bstek.urule.runtime.log.DataLog;
import com.bstek.urule.runtime.log.Log;
import com.bstek.urule.runtime.log.LogWriter;
import com.bstek.urule.runtime.log.UnitLog;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;

@Component
public class ConsoleLogWriter implements LogWriter {

    @Override
    public void write(List<Log> list) throws IOException {
        StringBuilder builder = new StringBuilder();
        this.buildLogs(builder, list);
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSSS");
        String url = "/urule-debug-" + simpleDateFormat.format(new Date()) + ".html";
        StringBuilder stringBuilder = new StringBuilder();
        stringBuilder.append("<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>URule Pro调试日志信息</title><body style='font-size:12px'>");
        stringBuilder.append(builder.toString());
        stringBuilder.append("</body></html>");
        FileOutputStream var6 = new FileOutputStream(new File(url));
        IOUtils.write(stringBuilder.toString(), var6, "utf-8");
        var6.flush();
        var6.close();
    }
    private void buildLogs (StringBuilder msg, List < Log > logs){
        for (Log log : logs) {
            if (log instanceof UnitLog) {
                msg.append("<div style=\"margin:8px;border:dashed 1px #cccccc\">");
                UnitLog unit = (UnitLog) log;
                List<Log> unitLogs = unit.getLogs();
                buildLogs(msg, unitLogs);
                msg.append("</div>");
            } else if (log instanceof DataLog) {
                DataLog dataLog = (DataLog) log;
                String htmlMsg = dataLog.getHtmlMsg();
                msg.append(htmlMsg);
            }
        }
    }
}

我们看一下接口源码

com.bstek.urule.runtime.log.LogWriter接口源码如下:

package com.bstek.urule.runtime.log;

import java.io.IOException;
import java.util.List;

/**
 * @author Jacky.gao
 * @since 2018年12月11日
 */
public interface LogWriter {
    void write(List<Log> logs) throws IOException;
}

下面是系统内置的默的输出到控制台的ConsoleLogWriter类源码,可以依照此代码来迭代日志信息。

package com.bstek.urule.console.servlet.console;

import java.io.IOException;
import java.util.List;

import com.bstek.urule.runtime.log.DataLog;
import com.bstek.urule.runtime.log.Log;
import com.bstek.urule.runtime.log.LogWriter;
import com.bstek.urule.runtime.log.UnitLog;

/**
 * @author Jacky.gao
 * @since 2017年11月28日
 */
public class ConsoleLogWriter implements LogWriter {
    private DebugMessageHolder debugMessageHolder;
    @Override
    public void write(List<Log> logs) throws IOException {
        StringBuilder sb=new StringBuilder();
        buildLogs(sb, logs);
        String key=debugMessageHolder.generateKey();
        System.out.println("Console key : "+key);
        ConsoleKeyHolder.setKey(key);
        debugMessageHolder.putDebugMessage(key, sb.toString());
    }
    private void buildLogs(StringBuilder msg,List<Log> logs) {
        for(Log log:logs){
            if(log instanceof UnitLog) {
                msg.append("<div style=\"margin:8px;border:dashed 1px #cccccc\">");
                UnitLog unit=(UnitLog)log;
                List<Log> unitLogs=unit.getLogs();
                buildLogs(msg, unitLogs);
                msg.append("</div>");
            }else if(log instanceof DataLog) {
                DataLog dataLog=(DataLog)log;
                String htmlMsg=dataLog.getHtmlMsg();
                msg.append(htmlMsg);
            }
        }
    }
    public void setDebugMessageHolder(DebugMessageHolder debugMessageHolder) {
        this.debugMessageHolder = debugMessageHolder;
    }
}

注意事项:

LogWriter接口实现后配置到Spring上下文环境里,在代码中调用要生效同样需要在session.fireRules()或session.startProcess(...)方法后加上session.writeLogFile()方法,否则将不会触发执行。
上面介绍的通过配置 urule.defaultHtmlFileDebugPath属性就会在指定目录生成日志文件的操作,实际上就是系统提供的一个默认的LogWriter接口实现类,该实现类在运行前会判断 urule.defaultHtmlFileDebugPath属性值是否为空, 如果为空,则不做任何操作,不为空,那么就认为这个值是一个目录,并尝试在这个目录中生成HTML格式的日志文件。

客户端动态配置

在实际使用时,我们可能会在诸如docker之类的容器中部署客户端应用,在这种情况下,客户端的IP地址不是固定的,此时要在服务端指定客户地址的话,采用上面的方法就行不通了。 为了兼容这种客户端IP地址动态变化的场景,URule Pro当中还提供了一个名为ClientProvider接口,通过实现这个接口来动态指定客户端IP地址,ClientProvider接口类源码如下:

package com.bstek.urule.console;

import java.util.List;

import com.bstek.urule.console.database.model.UrlConfig;

/**
* @author Jacky.Gao
* @since 2021年9月26日
*/
public interface ClientProvider {
    List<UrlConfig> loadClients(String groupId);
}

实现了ClientProvider接口后,只需要将实现类配置到Spring上下文当中,使之成为一个标准的Spring Bean即可,这样引擎在获取配置的客户端信息时就会尝试从这个实现类中获取。

一旦我们实现了该接口,并将其配置到Spring中,重启应用,在客户端配置页面就可以看到该接口实现类中加载的客户端列表,同时该页面也不再提供客户端信息的编辑功能。

Spring-Cloud框架

例如在spring-cloud中我们可以通过DiscoveryClient的getInstances获得对应服务的ip地址列表 参考代码:

public class MemberClientProvider implements ClientProvider {

    @Override
    public List<UrlConfig> loadClients(String arg0) {
        List<ServiceInstance> instances = Utils.getApplicationContext().getBean(DiscoveryClient).getInstances("consul-member");
        List<UrlConfig> clients = new ArrayList<UrlConfig>();
        for (ServiceInstance instance:instances) {
            UrlConfig client = new UrlConfig();
            client.setName(instance.getHost());
            client.setType(UrlType.client);
            client.setGroupId("bstek");
            client.setUrl("http:"+instance.getHost()+":"+instance.getPort());
            clients.add(client)j;
        }
        return clients;
    }

}

如下配置:

@Component
public class MemberClientProvider implements ClientProvider {

    @Override
    public List<UrlConfig> loadClients(String s) {
//        启动这行代码需要注册中心
//        List<ServiceInstance> instances = Utils.getApplicationContext().getBean(DiscoveryClient).getInstances("consul-member");
        List<UrlConfig> clients = new ArrayList<UrlConfig>();

            UrlConfig client = new UrlConfig();
            client.setName("localhost");
            client.setType(UrlType.client);
            client.setGroupId("bstek");
            client.setUrl("http://localhost:8099/server/");
            clients.add(client);

        return clients;
    }
}

测试:

K8S

如果不是spring-cloud框架,直接K8S部署,可以通过K8S提供的client-java的SDK获取到IP地址列表,详细办法参考K8S官方文档。 一篇简单的参考文档:

http://events.jianshu.io/p/d8a073f8d416
前面我们介绍了在URule Pro客户端中配置“ urule.knowledgeUpdateCycle”属性的含义及目的,对于客户端为Web应用类型,一旦我们在URule Server中配置了客户端地址,那么知识包每次发布都会被推送到这些客户端。这样在生产环境下,对于客户端来说,“ urule.knowledgeUpdateCycle”属性值只要设置成1即可,也就是每次都在客户端本地缓存中查找目标知识包是否存在,如果存在直接使用。如果知识包有更新,URule Server会动推送到目标客户端,对于客户端来说直接从本地缓存中就可以取到最新的知识包,这样可以剔除所有不必要的客户端与服务端的交互,节省网络资源,同时可以大幅提高客户端规则计算性能。
我们在对URule Pro进行性能测试时,一定要将“urule.knowledgeUpdateCycle”属性值只要设置成1,因为“urule.knowledgeUpdateCycle”属性值默认为0,所以每次执行规则都会编译一次规则包,这样会带来很差的测试结果,所以这点非常关键; 在客户端中,将“urule.knowledgeUpdateCycle”设置成1后,检验该属性是否生效最好的方法是在客户端调用服务端知识包时观察控制台是否有类似“Load knowledgepackage [ packageId ] from remote...”这样的字符输出,如果有类似输出,则说明urule.knowledgeUpdateCycle=1未生效, 我们需要进一步检查配置,找到未生效的原因。 同时,名为"urule.debug"的属性也会对性能产生一点影响,这个属性值默认为true,表示允许进行调试信息输出,这样对于性能测试来说无疑会造成影响,所以在性能测试或生产环境中建议将“urule.debug”属性设置为false,这样可以减少不必要的调试信息输出,可显著提升性能。

总结:

这种客户端服务器模式,能更好的应用在微服务架构里面。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值