springboot中如何整合groovy实现一个轻量级规则引擎(代码片段)

wen-pan wen-pan     2023-02-24     797

关键词:

详细实现以及使用教程以及压测结果分析见:https://gitee.com/mr_wenpan/basis-enhance/blob/master/enhance-boot-groovy-engine/README.md

一、项目功能说明

该工程(enhance-boot-groovy-engine)主要是利用【springboot + groovy】对groovy动态加载脚本功能进行了封装和集成,使得在springboot项目中能够更加简单方便的使用groovy在不重启的情况下来动态的加载外部脚本,可以看做是一个基于groovy封装的轻量级【规则引擎】。

1、为什么选择groovy

  • 在实际的平台化项目中,为了提升平台抽象能力使用更多场景,引入了规则引擎。如一个用户购买了500元商品可以获得10元红包,异或是购买了指定会场商品获取50元优惠券等。在不通互动场景中活动的规则是不同的,如果通过JAVA去实现,每次有新的规则要求,都要发布一次,这样成本就太高了。
  • 如果项目里只需要使用到轻量级的规则脚本,那么此时去引入像Drools这样的重量级规则引擎,那么有些杀鸡用牛刀的感觉。
  • groovy对Java的支持性非常好,完全可以使用Java语法去编写groovy脚本,对于Java开发者来说几乎没有学习成本。

2、项目结构

  • 项目大体分为两部分,core部分代表了功能的核心实现,loader部分代表了对脚本来源的扩展支持从不同来源加载脚本
  • demo-enhance-groovy-engine中是对该项目的一些使用demo

3、项目特点

  • 基于springboot来整合的groovy,使用者仅需要引入【maven坐标】,配置好【application.yml】参数即可方便使用
  • 提供了【从classpath下加载脚本】、【从Redis加载脚本】、【从MySQL数据库加载脚本】这三种脚本来源选项,脚本加载源可以方便的替换(仅需要更改application.yml配置即可)
  • 使用【GroovyClassLoader + InvokerHelper + 缓存parse好的Class对象】来解决频繁load groovy脚本为Class从而导致方法区OOM问题。同时通过缓存脚本信息来避免每次执行脚本都需要重新编译而带来的性能消耗,保证脚本执行的高效。
  • 提供EngineExecutor方便的来执行脚本,同时提供多种执行脚本的方式,仅需要传入能定位脚本的唯一key和脚本里需要的参数即可方便的调用指定的脚本进行执行了
  • 项目中提供了非常多的【可配置项】以及【可替换组件】,使用方可以根据项目需要调整配置项参数值,也可以自己实现某一个组件,然后注册到容器中,进而替换原有组件。
  • 项目中使用【caffeine】来缓存脚本项,并设置过期时间,并且项目里提供了定时刷新(刷新间隔周期可配置)本地缓存里的脚本项的异步线程,可以及时的将【本地缓存的脚本项】和【数据源中的脚本项】进行对比,一旦发现数据源中的脚本发生了变更则及时刷新本地缓存中的脚本项,并且替换原脚本对应的Class。
  • 项目中提供了很多xxxxxHelper,这些helper将本项目的核心功能进行透出,使用方可以通过这些helper来进行定制化操作
    • ApplicationContextHelper】提供了操作spring容器的一些功能,可以借助该helper方便的对IOC容器进行操作
    • RefreshScriptHelper】提供了动态刷新本地内存中的脚本的能力,可以通过该helper提供的方法来手动的刷新本地内存的脚本(支持单个刷新和批量刷新),比如:新增或修改了某个脚本后想立即让该脚本生效。
    • RegisterScriptHelper】提供了动态向数据源和本地缓存里注册脚本的能力,可以通过该helper来动态的向数据源和本地缓存中修改脚本或注册新的groovy脚本
  • 项目中使用caffeine缓存来缓存脚本,指定了最大缓存的脚本条数(可配置),当脚本条数达到最大缓存条数后通过淘汰算法淘汰不常用的脚本,以保证不会因为加载过多脚本导致内存占用过高。并且可以配置缓存条目的有效期,可以淘汰失效的或者长时间不用的脚本项,当再次使用到时再从数据源加载脚本并缓存。
  • 项目提供的能力需要在application.yml中通过配置开启,如果引入了本项目的maven依赖,但是没有在application.yml中显示开启功能,那么本项目的功能不会生效。
  • 脚本动态刷新部分参考zuul网关的filter源码进行编写

二、开启配置

1、下载源码并打包到自己的maven仓库

源码地址:https://gitee.com/mr_wenpan/basis-enhance/blob/master/enhance-boot-data-redis/README.md

执行命令:mvn clean install(需要切换到该项目目录下执行)

2、pom中引入依赖坐标

特别说明

  • enhance-groovy-engine-core】是核心依赖,必须要引入,其他的按需求选配即可。
  • 如果是从classpath下加载脚本,那么只需要引入【enhance-groovy-classpath-loader】即可
  • 如果是从redis中加载脚本,那么需要引入【enhance-groovy-redis-loader】,由于是从Redis中读取脚本,所以Redis的核心依赖不能少【spring-boot-starter-data-redis】和 【commons-pool2】(如果项目中已经有Redis了,那可以不引入这两个)
  • 如果是从MySQL中加载脚本,那么需要引入【enhance-groovy-mysql-loader】以及连接MySQL所需要的的依赖
<!--核心依赖-->
<dependency>
  <artifactId>enhance-groovy-engine-core</artifactId>
  <groupId>org.basis.enhance</groupId>
  <version>1.0-SNAPSHOT</version>
</dependency>

<!--++++++++++++++++++++++++++++++++以下三种loader,按需选配即可++++++++++++++++++++++++++++++++-->

<!--++++++++++++++++++++++++++++++++++第一种:基于Redis的loader++++++++++++++++++++++++++++++++++-->
 <!--加载Redis下的groovy脚本loader依赖-->
<dependency>
  <artifactId>enhance-groovy-redis-loader</artifactId>
  <groupId>org.basis.enhance</groupId>
  <version>1.0-SNAPSHOT</version>
</dependency>
<!--redis核心依赖-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--Apache的 common-pool2(至少是2.2)提供连接池,供redis客户端使用-->
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
</dependency>

<!--++++++++++++++++++++++++++++++++++第二种:基于Classpath的loader++++++++++++++++++++++++++++++++++-->
<!--加载classpath下的groovy脚本loader依赖-->
<dependency>
  <artifactId>enhance-groovy-classpath-loader</artifactId>
  <groupId>org.basis.enhance</groupId>
  <version>1.0-SNAPSHOT</version>
</dependency>

<!--++++++++++++++++++++++++++++++++++第三种:基于MySQL的loader++++++++++++++++++++++++++++++++++-->
<!--加载MySQL下的groovy脚本loader依赖-->
<dependency>
  <artifactId>enhance-groovy-mysql-loader</artifactId>
  <groupId>org.basis.enhance</groupId>
  <version>1.0-SNAPSHOT</version>
</dependency>
<!--以下是mysql相关依赖-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
  <groupId>com.zaxxer</groupId>
  <artifactId>HikariCP</artifactId>
</dependency>
<!--mybatis依赖-->
<dependency>
  <groupId>org.mybatis.spring.boot</groupId>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <version>2.0.1</version>
</dependency>

3、配置application.yml

以下以从Redis加载脚本为例来演示配置

server:
  port: 1234

spring:
  application:
    name: customer-console
  # redis基础配置
  redis:
    host: $SPRING_REDIS_HOST:xxx-host
    port: $SPRING_REDIS_PORT:6379
    password: $SPRING_REDIS_PASSWORD:xxxx@123
    database: $SPRING_REDIS_DATABASE:2

enhance:
  groovy:
    engine:
      # 脚本检查更新周期(单位:秒),(默认300L)
      pollingCycle: 10
      # 开启功能
      enable: true
      # 缓存过期时间(默认600L分钟)
      cacheExpireAfterWrite: 10
      #缓存初始容量(默认100)
      cacheInitialCapacity: 10
      # 缓存最大容量(默认500)
      cacheMaximumSize: 20
      # 开启基于Redis加载groovy脚本
      redis-loader:
        # 命名空间,可以和应用名称保持一致即可,主要是为了区分不同的应用
      	namespace: customer-console
      	# 开启基于Redis的loader
      	enable: true

三、使用介绍

1、Redis中导入groovy脚本

  • 当然你也可以项目启动好后通过RegisterScriptHelper来注册脚本,这里采用预先加载好脚本到Redis来演示
  • 对应的脚本都在org.enhance.groovy.infra.groovy目录下,自己按需设定脚本key即可

①、脚本内容

  • key: customer-console
  • hashKey: change-order
  • 脚本:
package org.enhance.groovy.api.dto


import org.slf4j.Logger
import org.slf4j.LoggerFactory

class ChangeOrderInfo extends Script 

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    Object run() 
        // 调用方法
        changeOrderInfo();
    

    // 修改订单信息
    OrderInfoDTO changeOrderInfo() 
        String newOrderAmount = "20000";
        // 获取参数
        OrderInfoDTO orderInfoDTO = orderInfo;
        logger.info("即将修改订单金额,原金额为:, 修改后的金额为:", orderInfoDTO.getOrderAmount(), newOrderAmount);
        orderInfoDTO.setOrderAmount("2000");
        // 返回修改后的结果
        return orderInfoDTO;
    

  • key: customer-console
  • hashKey: change-product
  • 脚本:
package org.enhance.groovy.api.dto

import org.basis.enhance.groovy.entity.ExecuteParams
import org.slf4j.Logger
import org.slf4j.LoggerFactory

class ChangeProductInfo extends Script 

    private final Logger logger = LoggerFactory.getLogger(getClass());

    // 修改商品信息
    ProductInfoDTO changeProduct(ExecuteParams executeParams) 
        // 获取product对象
        ProductInfoDTO productInfo = (ProductInfoDTO) executeParams.get("productInfo");
        Double newOrderAmount = 20000D;
        logger.info("即将修改商品金额,原金额为:, 修改后的金额为:", productInfo.getPrice(), newOrderAmount);
        // 商品价格修改为newOrderAmount
        productInfo.setPrice(newOrderAmount);
        // 返回修改后的对象
        return productInfo;
    

    @Override
    Object run() 
        return null
    

  • key: customer-console
  • hashKey: get-context
  • 脚本:
package org.enhance.groovy.infra.groovy

import org.enhance.groovy.api.dto.ProductInfoDTO
import org.enhance.groovy.app.service.ProductService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationContext

/**测试从spring ioc容器中获取bean,并调用bean的方法*/
class GetApplicationContext extends Script 

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    Object run() 
        // 调用方法
        ApplicationContext context = getContext();
        // 获取容器中的bean
        ProductService productService = context.getBean(ProductService.class);
        // 调用bean的方法
        Random random = new Random();
        ProductInfoDTO productInfoDTO = productService.getProductById(random.nextInt(1000));
        logger.info("productInfoDTO is : ", productInfoDTO);

        // 调用bean 的修改方法
        productService.updateProduct(productInfoDTO);
        logger.info("updated productInfoDTO is : ", productInfoDTO);
        return productInfoDTO;
    

    // 获取spring容器
    ApplicationContext getContext() 
        // 获取spring IOC容器
        ApplicationContext context = applicationContext;
        return context;
    

2、启动项目并调用脚本

   /**
     * scriptName只要能唯一定位到脚本即可
     * 测试@link EngineExecutor#execute(ScriptQuery, ExecuteParams)
     * 请求URL:http://localhost:1234/v1/load-from-redis/change-order?scriptName=change-order
     */
    @GetMapping("/change-order")
    public String changeOrderInfo(String scriptName) 
        // 构建参数
        OrderInfoDTO orderInfoDTO = new OrderInfoDTO();
        orderInfoDTO.setOrderAmount("1000");
        orderInfoDTO.setOrderName("测试订单");
        orderInfoDTO.setOrderNumber("BG-123987");
        ExecuteParams executeParams = new ExecuteParams();
        executeParams.put("orderInfo", orderInfoDTO);

        // 执行脚本
        EngineExecutorResult executorResult = engineExecutor.execute(new ScriptQuery(scriptName), executeParams);
        String statusCode = executorResult.getExecutionStatus().getCode();
        if("200".equals(statusCode))
            log.info("脚本执行成功......");
         else 
            log.info("脚本执行失败......");
        
        log.info("changeOrderInfo=========>>>>>>>>>>>执行结果:", executorResult);

        return "success";
    

springboot中如何整合groovy实现一个轻量级规则引擎(代码片段)

...说明该工程(enhance-boot-groovy-engine)主要是利用【springboot+groovy】对groovy动态加载脚本功能进行了封装和集成,使得在springboot项目中能够更加简单方便的使用groovy在不重启的情况下来动态的加载外部脚本,可以... 查看详情

springboot整合groovy脚本,实现动态编程(代码片段)

点击关注公众号,实用技术文章及时了解Groovy简介Groovy是增强Java平台的唯一的脚本语言。它提供了类似于Java的语法,内置映射(Map)、列表(List)、方法、类、闭包(closure)以及生成器。脚本语... 查看详情

springboot整合rocketmq实现入门案例

...习了Spring整合RocketMQ的第一个案例!现在我们来学习SpringBoot如何整合RocketMQ实现更加简单的使用!文章目录1创建maven项目2配置文件3生产者4消费者5测试1创建maven项目创建一个maven项目。引入sp 查看详情

springboot2系列教程|整合thymeleaf

...eaf,并整合Thymeleaf开发一个简陋版的学生信息管理系统。SpringBoot提供了大量模板引擎,包含Freemarker、Groovy、Thymeleaf、Velocity以及Mustache,SpringBoot中推荐使用Thymeleaf作为模板引擎,因为Thymeleaf提供了完美的SpringMVC支持。Thymeleaf是... 查看详情

[springboot系列]springboot如何整合ssmp

文章目录基于SpringBoot实现SSMP整合基于SpringBoot实现SSMP整合SpringBoot之所以好用,就是它能方便快捷的整合其他技术,这里我们先介绍四种技术的整合:整合JUnit整合MyBatis整合MyBatis-Plus整合Druid整合JUnitSpringBoot技术的定位用于简化... 查看详情

groovy在eclipse中如何实现语法提示

我在编写groovy的时候希望使用alt+/产生语法提示你试过吗?  groovy在eclipse中要实现语法提示,需要安装插件。  Groovy的插件继承自Java插件并添加了对Groovy项目的支持。它可以处理Groovy代码,以及混合的Groovy和Java代码,甚至... 查看详情

springboot整合vue并实现前后端贯穿调用

一、前言  作为一个后端程序员,前端知识多少还是要了解一些的,vue能很好的实现前后端分离,且更便于我们日常中的调试,还具备了轻量、低侵入性的特点,所以我觉得是很有必要了解的一门前端技术。  这篇文章先记... 查看详情

精通springboot--整合redis实现缓存

今天我们来讲讲怎么在springboot中整合redis实现对数据库查询结果的缓存。首先第一步要做的就是在pom.xml文件添加spring-boot-starter-data-redis。要整合缓存,必不可少的就是我们要继承一个父类CachingConfigurerSupport。我们先看看这个类... 查看详情

springboot实现多数据源整合mybatis版(代码片段)

前言本篇博客只讲如何从零到壹地再SpringBoot项目中实现多数据源配置,不谈源码(后续上SpringBoot自动配置等源码)。本篇博客内容基于SpringBootVersion2.5.1。背景一个项目中需要连接多个数据源,我们需要在业务层... 查看详情

springboot实现多数据源整合mybatis版(代码片段)

前言本篇博客只讲如何从零到壹地再SpringBoot项目中实现多数据源配置,不谈源码(后续上SpringBoot自动配置等源码)。本篇博客内容基于SpringBootVersion2.5.1。背景一个项目中需要连接多个数据源,我们需要在业务层... 查看详情

springboot中使用groovy脚本的各种方式

springboot中使用groovy脚本的各种方式准备工作1.环境2.groovy运行一、动态执行groovy脚本1.groovy通过import的方式直接调用java类2.groovy通过GroovyShell预设对象调用JAVA对象的方法3.controller中执行groovyscript4.将所有SpringBean作为groovy变量二、... 查看详情

springboot整合elasticsearch实现海量级数据搜索

参考技术A今天给大家讲讲SpringBoot框架整合Elasticsearch实现海量级数据搜索。在上篇ElasticSearch文章中,我们详细的介绍了ElasticSearch的各种api使用。实际的项目开发过程中,我们通常基于某些主流框架平台进行技术开发,比如SpringB... 查看详情

springboot整合elasticsearch实现多版本的兼容

前言在上一篇学习SpringBoot中,整合了Mybatis、Druid和PageHelper并实现了多数据源的操作。本篇主要是介绍和使用目前最火的搜索引擎ElastiSearch,并和SpringBoot进行结合使用。ElasticSearch介绍ElasticSearch是一个基于Lucene的搜索服务器,其... 查看详情

springboot整合mybatis案例详解(代码片段)

SpringBoot约定大于配置的特点,让框架的配置非常简洁,与传统的SSM框架相比,简化了大量的XML配置,使得程序员可以从繁琐的配置中脱离出来,将精力集中在业务代码层面,提高了开发效率,并且后期的维护成本也大大降低。... 查看详情

springboot整合springseesion实现redis缓存

参考技术A使用SpringBoot开发项目时我们经常需要存储Session,因为Session中会存一些用户信息或者登录信息。传统的web服务是将session存储在内存中的,一旦服务挂了,session也就消失了,这时候我们就需要将session存储起来,而Redis就... 查看详情

springboot整合shiro+jwt实现认证及权限校验

序言本文讲解如何使用SpringBoot整合Shiro框架来实现认证及权限校验,但如今的互联网已经成为前后端分离的时代,所以本文在使用SpringBoot整合Shiro框架的时候会联合JWT一起搭配使用。ShiroShiro是apache旗下一个开源框架,... 查看详情

springboot进阶之整合shiro鉴权框架(四)

...有的难点都离不开「基础知识」的铺垫。目前正在出一个SpringBoot长期系列教程,从入门到进阶,篇幅会较多~「大佬可以绕过~」如果你是一路看过来的,很高兴你能够耐心看完。之前带大家学了Springboot基础部分,对基本的使用有... 查看详情

(十三)如何实现事务(代码片段)

前面介绍了SpringBoot中的整合Mybatis并实现增删改查。不清楚的朋友可以看看之前的文章:https://www.cnblogs.com/zhangweizhong/category/1657780.html。SpringBoot整合完Mybatis,有个特别重要的功能之前忘记讲了:那就是SpringBoot如何实现事物控制... 查看详情