通过 Groovy 实现业务逻辑的动态变更

Groovy

  • 1、需求的提出
  • 2、为什么是Groovy
  • 3、设计参考
    • 1_引入Maven依赖
    • 2_GroovyEngineUtils工具类
    • 3_GroovyScriptVar类
    • 4_脚本规则表设计
    • 5_对应的实体类
    • 6_数据库访问层
    • 7_GroovyExecService通用接口
  • 4、测试
  • 5、其他的注意事项
  • 6、总结

1、需求的提出

在我们日常的开发过程中,经常会遇到一些功能的逻辑变更很频繁的需求,相信大部分小伙伴都会通过加 if 判断来解决(毕竟这样最简单,也可能是因为项目赶时间等一系列客观因素)。长此以往,我们项目代码中可能充斥着大量的 if 代码段,可读性不高。当然网上有很多方式消除 if…else… 代码的方法,比如说使用恰当的设计模式、使用规则引擎等等。

上述所说的可读性不高,不是本文分享内容解决的重点问题,逻辑变更很频繁的需求,还带来的另一个问题就是需要经常重启服务。今天我们分享的就是利用Groovy脚本在Spring Boot项目中实现 动态编程 动态编程 动态编程解决这一问题,使业务逻辑的动态化,极大地提升了开发效率和灵活性

2、为什么是Groovy

Groovy语言作为一种基于JVM的动态语言,它可以编译为与Java相同的字节码,然后将字节码文件交给JVM去执行,并且可以与Java类无缝地互操作。

Groovy可以透明地与Java库和代码交互,可以 使用 J a v a 所有的库 使用Java所有的库 使用Java所有的库,并且有着简洁灵活的语法、动态类型和闭包等特性。

Groovy无缝地集成了Java的强大功能,并提供了许多额外的特性,如DSL(领域特定语言)的支持和元编程能力,使得开发者能够以更加简洁和优雅的方式表达复杂的逻辑。

Groovy也可以直接将源文件解释执行。它还极大地清理了Java中许多冗长的代码格式。

Groovy尚未成为主流的开发语言,但是它已经在测试(由于其简化的语法和元编程功能)和构建系统中占据了一席之地。

既支持 面向对象 编程也支持 面向过程 编程,即可以作为 编程语言 也可以作为 脚本语言

D S L DSL DSL 其实是 Domain Specific Language 的缩写,中文翻译为领域特定语言(下简称 DSL);

而与 DSL 相对的就是 G P L GPL GPL,是General Purpose Language 的简称,即通用编程语言,也就是我们非常熟悉的Java、Python 以及 C 语言等等。

3、设计参考

在与我们项目整合时,可以这样进行设计:基于简单的 CURD 功能将 G r o o v y 代码片 Groovy代码片 Groovy代码片放入数据库中进行管理,通过切换对应的 G r o o v y 代码片 Groovy代码片 Groovy代码片达到动态变更规则的特性。下面给个模版进行参考,如果有需要再根据自己的需求进行相应的修改(前端也可以使用模版引擎比如 F r e e m a r k Freemark Freemark):

1_引入Maven依赖

<dependency><groupId>org.codehaus.groovy</groupId><artifactId>groovy-all</artifactId><version>2.4.5</version>
</dependency>

2_GroovyEngineUtils工具类

package org.example.util;import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.codehaus.groovy.jsr223.GroovyScriptEngineImpl;import javax.script.*;
import java.util.Map;
import java.util.Objects;@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class GroovyEngineUtils{private static final GroovyScriptEngineImpl GROOVY_ENGINE = (GroovyScriptEngineImpl) new ScriptEngineManager().getEngineByName("groovy");/*** 编译脚本* @param script* @return* @throws ScriptException*/public static CompiledScript compile(String script) throws ScriptException {return GROOVY_ENGINE.compile(script);}/*** 执行脚本* @param compiledScript * @param args 脚本参数* @return*/public static Object eval(CompiledScript compiledScript, Map<String, Object> args) {try {return compiledScript.eval(getScriptContext(args));} catch (ScriptException e) {log.error(" exec GroovyEngineUtils.eval error!!!", e);}return null;}/*** 执行脚本* @param script 脚本* @return*/public static Object eval(String script) {try {return GROOVY_ENGINE.eval(script);} catch (ScriptException e) {log.error(" exec GroovyEngineUtils.eval error!!!", e);}return null;}/*** 执行脚本* @param script 脚本* @param args 脚本参数* @return*/public static Object eval(String script, Map<String, Object> args) {try {return GROOVY_ENGINE.eval(script, getScriptContext(args));} catch (ScriptException e) {log.error(" exec GroovyEngineUtils.eval error!!!", e);}return null;}private static ScriptContext getScriptContext(Map<String, Object> args) {ScriptContext scriptContext = new SimpleScriptContext();if (Objects.nonNull(args)) {args.forEach((k, v) -> scriptContext.setAttribute(k, v, ScriptContext.ENGINE_SCOPE));}return scriptContext;}
}

3_GroovyScriptVar类

GroovyScriptVar 类主要作用是在项目启动时,缓存编译后的脚本,也提供了刷新脚本的功能,这样在业务逻辑变更的时候,我们可以通过刷新功能,重新加载脚本。防止了我们重启服务

package org.example.config;import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.example.dao.CommonScriptDao;
import org.example.pojo.CommonScript;
import org.example.util.GroovyEngineUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import javax.script.CompiledScript;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;@Component
@Slf4j
public class GroovyScriptVar {// 缓存编译后的脚本private static final Map<String, CompiledScript> SCRIPT_MAP = new ConcurrentHashMap<>();@Resourcepublic CommonScriptDao commonScriptDao;@PostConstructpublic void init() {refresh();}@SneakyThrowspublic void refresh() {synchronized (GroovyScriptVar.class) {// 从数据库中加载脚本List<CommonScript> list = commonScriptDao.findAll();SCRIPT_MAP.clear();if(CollectionUtils.isEmpty(list)) return;for (CommonScript script : list) {SCRIPT_MAP.put(script.getUniqueKey(), GroovyEngineUtils.compile(script.getScript()));}log.info(" Groovy脚本初始化,加载数量:{}",list.size());}}public CompiledScript get(String uniqueKey){return SCRIPT_MAP.get(uniqueKey);}}

4_脚本规则表设计

create table common_script
(id               int auto_increment comment '主键标识'primary key,unique_key       varchar(32)            null comment '唯一标识',script           mediumtext             null comment '脚本',creator          varchar(128)           null comment '创建人名称',create_date      datetime               null comment '创建时间',last_update_date datetime               null comment '更新时间'
)comment '脚本规则';

5_对应的实体类

与上述的表结构对应即可

package org.example.pojo;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDateTime;@TableName("common_script") // 指定表名
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CommonScript {@TableId(value="id",type = IdType.AUTO)//字段不一致时,通过value值指定table中主键字段名称private Long id;private String uniqueKey;private String script;private String creator;private LocalDateTime createDate;private LocalDateTime lastUpdateDate;
}

6_数据库访问层

当然,这里的 dao 是基于MybatisPlus 的:

package org.example.dao;import com.baomidou.mybatisplus.core.mapper.BaseMapper;import org.apache.ibatis.annotations.Select;
import org.example.pojo.CommonScript;import java.util.List;public interface CommonScriptDao extends BaseMapper<CommonScript> {@Select("select * from common_script")List<CommonScript> findAll();
}

7_GroovyExecService通用接口

package org.example.service;public interface GroovyExecService {/*** 执行脚本的方法*/void exec();/*** 重新加载脚本引擎*/void refresh();
}

Groovy 脚本在项目中使用核心逻辑到这基本就已经完成了。

4、测试

测试脚本,定义一个非常简单的脚本根据不同状态,打印不同返回值:

if ("COMPLETED" == status) {// 如果条件为COMPLETEDreturn "条件为已完成"
}else{return "条件为未完成"
}

测试service:

package org.example.service.impl;import lombok.extern.slf4j.Slf4j;
import org.example.config.GroovyScriptVar;
import org.example.service.GroovyExecService;
import org.example.util.GroovyEngineUtils;
import org.springframework.stereotype.Service;import javax.annotation.Resource;
import javax.script.CompiledScript;
import java.util.HashMap;
import java.util.Map;@Slf4j
@Service
public class GroovyExecServiceImpl implements GroovyExecService {@Resourceprivate GroovyScriptVar groovyScriptVar;@Overridepublic void exec() {Map<String, Object> args=new HashMap<>();args.put("status","COMPLETED");CompiledScript compiledScript = groovyScriptVar.get("test");Object result = GroovyEngineUtils.eval(compiledScript, args);log.info("/// 脚本执行结果{}",(String)result);}@Overridepublic void refresh() {groovyScriptVar.refresh();}
}

执行效果:可以看到项目在启动过程中已经加载了一个脚本并且成功调用了exec方法:

在这里插入图片描述

5、其他的注意事项

如果Groovy脚本没有做好权限控制,将会成为攻击你系统最有力的武器!!!

如果Groovy脚本用不好,还会导致 O O M OOM OOM,最终服务器宕机——毕竟Groovy脚本中创建的对象也是在JVM堆中的。

下面附上一个 Groovy 脚本通用的封装方法方法,可以通过参数传递配置。为了使这个方法更加通用和健壮,对代码进行一些优化和封装。以下是一个改进后的通用封装方法,并附带详细的注释说明:

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;/*** 执行 Groovy 脚本中的指定方法并返回结果。* * @param templateScript 要执行的 Groovy 脚本字符串* @param methodName 要调用的方法名* @param params 传递给方法的参数数组* @return 方法执行的结果* @throws Exception 如果方法调用失败或其他异常*/
public static Object invokeGroovyMethod(String templateScript, String methodName, Object... params) throws Exception {// 创建 Groovy 脚本的变量绑定Binding groovyBinding = new Binding();// 使用 GroovyShell 解析脚本字符串GroovyShell groovyShell = new GroovyShell(groovyBinding);Script script = groovyShell.parse(templateScript);// 检查方法是否存在if (!script.getMetaClass().respondsTo(script, methodName).isEmpty()) {// 调用指定的方法并传递参数Object result = script.invokeMethod(methodName, params);// 清除 Groovy 脚本缓存groovyShell.getClassLoader().clearCache();return result;} else {throw new NoSuchMethodException("方法 " + methodName + " 不存在于脚本中。");}
}

步骤说明:

1. Binding 对象

  • Binding groovyBinding = new Binding();:创建一个 Binding 对象,用于将变量绑定到 Groovy 脚本中。

2. GroovyShell 对象

  • GroovyShell groovyShell = new GroovyShell(groovyBinding);:创建一个 GroovyShell 对象,用于解析和执行 Groovy 脚本。

3.解析 Groovy 脚本

  • Script script = groovyShell.parse(templateScript);:将传入的 Groovy 脚本字符串解析为 Groovy 脚本对象。

4. 检查方法是否存在

  • if (!script.getMetaClass().respondsTo(script, methodName).isEmpty()) {:检查脚本中是否存在指定名称的方法。如果方法存在,则继续执行;否则抛出 NoSuchMethodException 异常。

5. 调用方法

  • Object result = script.invokeMethod(methodName, params);:调用 Groovy 脚本中的指定方法,并传递参数。

6. 清除 Groovy 缓存

  • groovyShell.getClassLoader().clearCache();:清除 Groovy 脚本的类加载器缓存,以避免内存泄漏问题。

7. 返回结果

  • return result;:返回方法执行的结果。

使用示例:

public static void main(String[] args) {String groovyScript = "def methodName(config) { return '执行结果: ' + config }";String methodName = "methodName";String configParam = "配置参数";try {Object result = invokeGroovyMethod(groovyScript, methodName, configParam);System.out.println(result);  // 输出: 执行结果: 配置参数} catch (Exception e) {e.printStackTrace();}
}

通过这种方式,可以将 Groovy 脚本执行的逻辑封装到一个通用的方法中,并且在调用时更加简洁和安全。

本文的所有示例都是基于JDK8来操作的,如果你使用了8以上的版本可能会出现异常——因为JDK9模块化之后类加载器也进行了改变,而加载脚本引擎还是需要相应的 C l a s s L o a d e r ClassLoader ClassLoader的,所以需要更高版本的Groovy依赖。

6、总结

通过Groovy脚本在Spring Boot项目中实现动态编程,将Groovy脚本存储在数据库等中间件中,我们可以实现诸如动态配置、动态路由以及动态业务逻辑的功能,极大地提高了项目的可扩展性和可维护性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/439912.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

嵌入式知识点复习(一)

国庆倒数第二天&#xff0c;进行嵌入式课堂测试的复习&#xff1a; 第一章 绪论 1.1 嵌入式系统的概念 嵌入式系统定义 嵌入式系统定位 嵌入式系统形式 嵌入式系统三要素 嵌入式系统与桌面通用系统的区别 1.2 嵌入式系统的发展历程 微处理器的演进历史 单片机的演进历史 …

【易社保-注册安全分析报告】

前言 由于网站注册入口容易被黑客攻击&#xff0c;存在如下安全问题&#xff1a; 1. 暴力破解密码&#xff0c;造成用户信息泄露 2. 短信盗刷的安全问题&#xff0c;影响业务及导致用户投诉 3. 带来经济损失&#xff0c;尤其是后付费客户&#xff0c;风险巨大&#xff0c;造…

【Python】数据可视化之聚类图

目录 clustermap 主要参数 参考实现 clustermap sns.clustermap是Seaborn库中用于创建聚类热图的函数&#xff0c;该函数能够将数据集中的样本按照相似性进行聚类&#xff0c;并将聚类结果以矩阵的形式展示出来。 sns.clustermap主要用于绘制聚类热图&#xff0c;该热图通…

用manim实现Gram-Schmidt正交化过程

在线性代数中&#xff0c;正交基有许多美丽的性质。例如&#xff0c;由正交列向量组成的矩阵(又称正交矩阵)可以通过矩阵的转置很容易地进行反转。此外&#xff0c;例如&#xff1a;在由彼此正交的向量张成的子空间上投影向量也更容易。Gram-Schmidt过程是一个重要的算法&#…

Python Tips6 基于数据库和钉钉机器人的通知

说明 起因是我第一版quant程序的短信通知失效了。最初认为短信是比较即时且比较醒目的通知方式&#xff0c;现在看来完全不行。 列举三个主要问题&#xff1a; 1 延时。在早先还能收到消息的时候&#xff0c;迟滞就很严重&#xff0c;几分钟都算短的。2 完全丢失。我手机没有…

AI 时代:产品经理不“AI”就出局?

即便你没想去做“AI 产品经理”&#xff0c;那你也不能成为一个不会用 AI 的产品经理。 产品经理肯定是所有互联网从业者中&#xff0c;最先捕捉到 AI 趋势的岗位。 但只知道 AI、关注 AI 还不够&#xff0c;仔细审视一下&#xff1a;你自己的工作&#xff0c;被 AI 提效了么…

《Windows PE》4.1导入表

导入表顾名思义&#xff0c;就是记录外部导入函数信息的表。这些信息包括外部导入函数的序号、名称、地址和所属的DLL动态链接库的名称。Windows程序中使用的所有API接口函数都是从系统DLL中调用的。当然也可能是自定义的DLL动态链接库。对于调用方&#xff0c;我们称之为导入函…

安防监控/视频系统EasyCVR视频汇聚平台如何过滤134段的告警通道?

视频汇聚/集中存储EasyCVR安防监控视频系统采用先进的网络传输技术&#xff0c;支持高清视频的接入和传输&#xff0c;能够满足大规模、高并发的远程监控需求。平台支持国标GB/T 28181协议、部标JT808、GA/T 1400协议、RTMP、RTSP/Onvif协议、海康Ehome、海康SDK、大华SDK、华为…

STM32定时器(TIM)

目录 一、概述 二、定时器的类型 三、时序 四、定时器中断基本结构 五、定时器定时中断代码 六、定时器外部时钟代码 一、概述 TIM(Timer)定时器 定时器可以对输入的时钟进行计数&#xff0c;并在计数值达到设定值时触发中断16位计数器、预分频器、自动重装寄存器的时基…

力扣刷题 | 两数之和

目前主要分为三个专栏&#xff0c;后续还会添加&#xff1a; 专栏如下&#xff1a; C语言刷题解析 C语言系列文章 我的成长经历 感谢阅读&#xff01; 初来乍到&#xff0c;如有错误请指出&#xff0c;感谢&#xff01; 给定一个整数数组 nums 和…

网站建设完成后,切勿让公司官网成为摆设

在当今这个数字化时代&#xff0c;公司官网已经成为企业展示形象、传递信息、吸引客户的重要平台。然而&#xff0c;许多企业在网站建设完成后&#xff0c;往往忽视了对官网的持续运营和维护&#xff0c;导致官网逐渐沦为摆设&#xff0c;无法发挥其应有的作用。为了确保公司官…

15分钟学 Python 第40天:Python 爬虫入门(六)第一篇

Day40 &#xff1a;Python 爬取豆瓣网前一百的电影信息 1. 项目背景 在这个项目中&#xff0c;我们将学习如何利用 Python 爬虫技术从豆瓣网抓取前一百部电影的信息。通过这一练习&#xff0c;您将掌握网页抓取的基本流程&#xff0c;包括发送请求、解析HTML、存储数据等核心…

jvisualvm学习

系列文章目录 JavaSE基础知识、数据类型学习万年历项目代码逻辑训练习题代码逻辑训练习题方法、数组学习图书管理系统项目面向对象编程&#xff1a;封装、继承、多态学习封装继承多态习题常用类、包装类、异常处理机制学习集合学习IO流、多线程学习仓库管理系统JavaSE项目员工…

ssm服装店销售管理系统

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 摘 要 I Abstract II 第1章 绪论 1 1.1研究背景 1 1.2研究意义 1 1.3国内外研究现状 2 1.3.1国外研…

提高顾客满意度,餐饮业如何开展客户调研?

餐饮行业需明确调研目的&#xff0c;选择合适工具&#xff0c;设计问卷&#xff0c;收集并分析数据&#xff0c;持续追踪优化。通过客户调研&#xff0c;提升服务质量、顾客满意度和竞争力&#xff0c;利用ZohoSurvey等工具实现高效调研。 一、明确调研目的 进行客户调研前&am…

【hot100-java】【将有序数组转换为二叉搜索树】

二叉树篇 中序遍历实现 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right…

yolo自动化项目实例解析(七)自建UI--工具栏选项

在上一章我们基本实现了关于预览窗口的显示&#xff0c;现在我们主要完善一下工具栏菜单按键 一、添加任务ui 先加个ui页面&#xff0c;不想看ui的复制完这个文件到ui目录下转下py直接从第二步开始看 vi ui/formpy.ui <?xml version"1.0" encoding"UTF-8&q…

Mysql数据库--删除和备份、约束类型

目录 1.删除操作 1.1表的删除操作 1.2数据库备份 2.约束 2.1基本概况 2.2not null约束演示 2.3unique约束演示 2.4default约束演示 2.5primary key约束演示 2.6foreign key约束演示 2.7check约束演示 1.删除操作 1.1表的删除操作 delete from 表名 where 条件…

kubeadm部署k8s

1.1 安装Docker [rootk8s-all ~]# wget -O /etc/yum.repos.d/docker-ce.repo https://mirrors.huaweicloud.com/docker-ce/linux/centos/docker-ce.repo [rootk8s-all ~]# sed -i sdownload.docker.commirrors.huaweicloud.com/docker-ce /etc/yum.repos.d/docker-ce.repo [ro…

数据结构双向链表和循环链表

目录 一、循环链表二、双向链表三、循环双向链表 一、循环链表 循环链表就是首尾相接的的链表&#xff0c;就是尾节点的指针域指向头节点使整个链表形成一个循环&#xff0c;这就弥补了以前单链表无法在后面某个节点找到前面的节点&#xff0c;可以从任意一个节点找到目标节点…