通过 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.utils;


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.mapper.CommonScriptMapper;
import org.example.entity.CommonScriptEntity;
import org.example.utils.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<>();

    @Resource
    public CommonScriptMapper commonScriptMapper;


    @PostConstruct
    public void init() {
        refresh();
    }

    @SneakyThrows
    public void refresh() {
        synchronized (GroovyScriptVar.class) {
            // 从数据库中加载脚本
            List<CommonScriptEntity> list = commonScriptMapper.findAll();
            SCRIPT_MAP.clear();
            if(CollectionUtils.isEmpty(list)) return;
            for (CommonScriptEntity 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.entity;

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 CommonScriptEntity {

    @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_数据库访问层

当然,这里 mapper 实现是基于MybatisPlus 的:

package org.example.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import org.apache.ibatis.annotations.Select;
import org.example.entity.CommonScriptEntity;

import java.util.List;

public interface CommonScriptMapper extends BaseMapper<CommonScriptEntity> {

    @Select("select * from common_script")
    List<CommonScriptEntity> findAll();
}

7_GroovyExecService通用接口

所有的 Groovy 脚本执行服务按照如下接口作为规范进行实现:

package org.example.service;

public interface GroovyExecService {
    /**
     * 执行脚本的方法
     */
    void exec();

    /**
     * 重新加载脚本引擎
     */
    void refresh();
}

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

4、测试

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

if ("COMPLETED" == status) {
    // 如果条件为COMPLETED
    return "条件为已完成"
}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.utils.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 {


    @Resource
    private GroovyScriptVar groovyScriptVar;


    @Override
    public 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);
    }

    @Override
    public 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脚本存储在数据库等中间件中,我们可以实现诸如动态配置、动态路由以及动态业务逻辑的功能,极大地提高了项目的可扩展性和可维护性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【机器学习】智驭未来:探索机器学习在食品生产中的革新之路

&#x1f4dd;个人主页&#x1f339;&#xff1a;Eternity._ &#x1f339;&#x1f339;期待您的关注 &#x1f339;&#x1f339; ❀目录 &#x1f50d;1. 引言&#xff1a;探索机器学习在食品生产中的革新之路&#x1f4d2;2. 机器学习在食品质量控制中的应用&#x1f31e;实…

MySQL之复合查询与内外连接

目录 一、多表查询 二、自连接 三、子查询 四、合并查询 五、表的内连接和外连接 1、内连接 2、外连接 前面我们讲解的mysql表的查询都是对一张表进行查询&#xff0c;即数据的查询都是在某一时刻对一个表进行操作的。而在实际开发中&#xff0c;我们往往还需要对多个表…

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

Day41&#xff1a;Python爬取猫眼电影网站的电影信息 1. 项目背景 在本项目中&#xff0c;我们将使用 Python 爬虫技术从猫眼电影网站抓取电影信息。猫眼电影是一个知名的电影信息平台&#xff0c;提供了丰富的电影相关数据。通过这个练习&#xff0c;您将深入学习如何抓取动…

Android Compose的基本使用

前言: Compose这个东西呢,好处我没发现,坏处就是学习成本和低版本兼容. 不过,看在官方力推的份儿上,有空就学一下吧. 当初的kotlin,很多人说鸡肋(包括我)!现在不也咔咔用纯kotlin做项目吗?哈哈哈哈. 未来的事情,谁说得清呢? 首先创建一个专用的Compose项目 对没错!看到E…

大数据新视界 --大数据大厂之 从 Druid 和 Kafka 到 Polars:大数据处理工具的传承与创新

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

【笔记】数据结构12

文章目录 2013年408应用题41方法一方法二 看到的社区的一个知识总结&#xff0c;这里记录一下。 知识点汇总 2013年408应用题41 解决方法&#xff1a; 方法一 &#xff08;1&#xff09;算法思想 算法的策略是从前向后扫描数组元素&#xff0c;标记出一个可能成为主元素的元…

elasticsearch ES DBA常用语句

一、 查看集群状态 curl -uelastic 连接串:端口/_cluster/health?pretty 集群健康有三种状态&#xff1a;green,yellow,red green&#xff1a;所有主要分片、复制分片都可用yellow&#xff1a;所有主要分片可用&#xff0c;但不是所有复制分片都可用red&#xff1a;不是所有…

根据拍摄时间一键将图片分组

说在前面 最近裸辞出去玩了两个多月&#xff0c;旅行的过程中少不了拍照&#xff0c;祖国的大好河山太美了&#xff0c;一趟旅行下来产出了1w多张图片&#xff0c;所以回来后总要整理一下图片&#xff0c;我这边想要的是根据拍摄时间来对图片进行分组,所以回到家后我就写了这样…

通信工程学习:什么是ICP网络内容服务商

ICP&#xff1a;网络内容服务商 ICP&#xff0c;全称Internet Content Provider&#xff0c;即网络内容服务商&#xff0c;是指那些通过互联网向用户提供各种类型内容服务的组织或个人。ICP在数字化时代扮演着至关重要的角色&#xff0c;它们不仅是信息的传播者&#xff0c;更是…

Linux高级编程_29_信号

文章目录 进程间通讯 - 信号信号完整的信号周期信号的编号信号的产生发送信号1 kill 函数(他杀)作用&#xff1a;语法&#xff1a;示例&#xff1a; 2 raise函数(自杀)作用&#xff1a;示例&#xff1a; 3 abort函数(自杀)作用&#xff1a;语法&#xff1a;示例&#xff1a; 4 …

苏州 数字化科技展厅展馆-「世岩科技」一站式服务商

数字化科技展厅展馆设计施工是一个综合性强、技术要求高的项目&#xff0c;涉及到众多方面的要点。以下是对数字化科技展厅展馆设计施工要点的详细分析&#xff1a; 一、明确目标与定位 在设计之初&#xff0c;必须明确展厅的目标和定位。这包括确定展厅的主题、目标受众、展…

详解正确创建好SpringBoot项目后但是找不到Maven的问题

目录 问题 解决步骤&#xff1a; 找到File->Project Structure... 设置SDK 设置SDKs 问题 刚刚在使用IDEA专业版创建好SpringBoot项目后&#xff0c;发现上方导航栏的运行按钮是灰色的&#xff0c;而且左侧导航栏的pom.xml的图标颜色也不是正常的&#xff0c;与此同时我…

PIKACHU | PIKACHU 靶场 XSS 后台配置

关注这个靶场的其他相关笔记&#xff1a;PIKACHU —— 靶场笔记合集-CSDN博客 PIKACHU 自带了一个 XSS 平台&#xff0c;可以辅助我们完成 XSS 攻击&#xff0c;但是该后台需要配置数据库以后才能使用。本教程&#xff0c;就是教大家如何配置 PIKACHU XSS 平台的。 PIKACHU XS…

在线教育的未来:SpringBoot技术实现

1系统概述 1.1 研究背景 随着计算机技术的发展以及计算机网络的逐渐普及&#xff0c;互联网成为人们查找信息的重要场所&#xff0c;二十一世纪是信息的时代&#xff0c;所以信息的管理显得特别重要。因此&#xff0c;使用计算机来管理微服务在线教育系统的相关信息成为必然。开…

Hadoop大数据入门——Hive-SQL语法大全

Hive SQL 语法大全 基于语法描述说明 CREATE DATABASE [IF NOT EXISTS] db_name [LOCATION] path; SELECT expr, ... FROM tbl ORDER BY col_name [ASC | DESC] (A | B | C)如上语法&#xff0c;在语法描述中出现&#xff1a; []&#xff0c;表示可选&#xff0c;如上[LOCATI…

基于深度学习的乳腺癌分类识别与诊断系统

温馨提示&#xff1a;文末有 CSDN 平台官方提供的学长 QQ 名片 :) 1. 项目简介 乳腺癌是全球最常见的癌症之一&#xff0c;早期诊断对于治疗效果至关重要。近年来&#xff0c;深度学习技术在医学图像分析领域取得了显著进展&#xff0c;能够从大量的医学影像数据中自动学习和提…

【Android 14源码分析】WMS-窗口显示-第一步:addWindow

忽然有一天&#xff0c;我想要做一件事&#xff1a;去代码中去验证那些曾经被“灌输”的理论。                                                                                  – 服装…

网络安全概述:从认知到实践

一、定义 网络安全&#xff0c;即致力于保护网络系统所涵盖的硬件、软件以及各类数据&#xff0c;切实保障其免遭破坏、泄露或者篡改等不良情形的发生。 二、重要性 个人层面&#xff1a;着重于守护个人隐私以及财产安全&#xff0c;为个人在网络世界中的各项活动提供坚实的保…

Redis篇(Redis原理 - 数据结构)(持续更新迭代)

目录 一、动态字符串 二、intset 三、Dict 1. 简介 2. Dict的扩容 3. Dict的rehash 4. 知识小结 四、ZipList 1. 简介 2. ZipListEntry 3. Encoding编码 五、ZipList的连锁更新问题 六、QuickList 七、SkipList 八、RedisObject 1. 什么是 redisObject 2. Redi…

使用JavaScript写一个网页端的四则运算器

目录 style(内联样式表部分) body部分 html script 总的代码 网页演示 style(内联样式表部分) <style>body {font-family: Arial, sans-serif;display: flex;justify-content: center;align-items: center;height: 100vh;background-color: #f0f0f0;}.calculator {…