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

news2024/11/27 5:38:22

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代码片达到动态变更规则的特性。下面给个模版进行参考,如果有需要再根据自己的需求进行相应的修改:

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<>();

    @Resource
    public CommonScriptDao commonScriptDao;


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

    @SneakyThrows
    public 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) {
    // 如果条件为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.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 {


    @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的。

6、总结

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

一、图解C#教程

一、堆和栈 程序运行时&#xff0c;数据存储在内存中。 使用堆和栈来存储数据 1、栈 栈是一个内存数组&#xff0c;先进后出原则。 可以存储&#xff1a;某些类型变量的值&#xff1b;程序当前执行环境&#xff1b;传递给方法的参数&#xff1b; 入栈&#xff1a;把数据放…

【超级详细解释】力扣每日一题 134.加油站 48. 旋转图像

134.加油站 力扣 这是一个很好的问题。这个思路其实基于一种贪心策略。我们从整个路径的油量变化来理解它&#xff0c;结合一个直观的“最低点法则”&#xff0c;来确保找到正确的起点。 问题的核心&#xff1a;油量差值的累积 对于每个加油站&#xff0c;我们有两个数组&…

精选算法入门——day2

精选算法入门——day2 题目一题干解题思路一解题思路二解题思路三思路三代码 题目二题干解题思路代码 题目三题干解题思路一代码解题思路二代码解题思路三代码 题目四题干解题思路代码 题目一 题干 数组中有一个数字出现的次数超过数组长度的一半&#xff0c;请找出这个数字。…

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

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

ssm基于JAVA的酒店管理系统的设计与实现

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 第1章 绪论 1 1.1 选题动因 1 1.2 目的和意义 1 1.3 论文结构安排 2 第2章 开发环境与技术 3 2.1 S…

【AI知识点】置信区间(Confidence Interval)

置信区间&#xff08;Confidence Interval, CI&#xff09; 是统计学中用于估计总体参数的范围。它给出了一个区间&#xff0c;并且这个区间包含总体参数的概率等于某个指定的置信水平&#xff08;通常是 90%、95% 或 99%&#xff09;。与点估计不同&#xff0c;置信区间通过区…

i春秋云境靶场之CVE-2022-26965

1.环境搭建 提示我们后台存在rce,也就是命令执行漏洞 2.访问环境 cm - cmshttp://eci-2zeh0yf0ohu88wr26unq.cloudeci1.ichunqiu.com/ 我们可看到admin,我们点击&#xff0c;发现是一个登录页面&#xff0c;我们输入弱口令admin,登录成功 3.文件上传 我们在选项——选择主题…

C++:图的遍历

一、简介 图的遍历通常有深度优先遍历和广度优先遍历两种方式&#xff0c;这两种遍历次序对无向图和有向图都使用。 本文分别介绍基于邻接矩阵和邻接表的图的深度优先遍历和广度优先遍历&#xff0c;对于邻接矩阵和邻接表不熟悉的可翻阅&#xff1a;C&#xff1a;图的存储结构及…

dockerpull

20241006更新&#xff0c;亲测可用。 注意&#xff1a;这个方法随时可能会失效。 编辑配置文件&#xff0c;修改镜像源&#xff1a; vi /etc/docker/daemon.json {"registry-mirrors": ["https://do.nark.eu.org","https://dc.j8.work","…

TypeScript 第三部分 扩展

1. 声明文件 主要作用&#xff1a; 类型声明&#xff1a;为库或模块提供类型信息。全局声明&#xff1a;为全局作用域中的类型和变量提供声明。类型兼容性&#xff1a;确保第三方库或自定义代码的类型正确性。代码提示与检查&#xff1a;在开发环境中提供更好的代码提示和类型…

Sollong手机——一站式Web3生态解决方案

从定义上讲&#xff0c;Web3公司也属于互联网公司&#xff0c;不过与传统互联网公司相比&#xff0c;他们有一个很明显的特征&#xff1a;他们不断尝试做去中心化的事&#xff0c;一步步将数据和金融的控制权从美联储&#xff08;央行和金融机构&#xff09;、苹果&#xff08;…

2024/10/6周报

文章目录 摘要Abstract广西的一些污水处理厂工艺解析1. A/O工艺&#xff08;厌氧-缺氧-好氧工艺&#xff09;2. 氧化沟工艺3. MBR工艺&#xff08;膜生物反应器&#xff09;4. SBR工艺&#xff08;序批式活性污泥法&#xff09;5. 生物接触氧化法 其它补充一体化改良氧化沟工艺…

Linux的基础指令(下)

压缩包 这里不为打包和压缩做仔细的区分&#xff1b; 打包&#xff1a; 文件合并&#xff1b; 主要目的是在文件传输&#xff0c;移动时&#xff0c;能有效减少文件的缺失&#xff1b; 压缩&#xff1a;为了减小文件体积&#xff0c;内存&#xff1b; 主要目的是减小使用体…

在JS中定义和使用Vector2

概述 Vector2是GDSCript中表示二维向量的类型&#xff0c;你会发现无论在任何编程语言中&#xff0c;只要你想很好的实现2D绘图以及几何和物理相关&#xff0c;Vector2是你必须要实现的一个类。我之前学C时就写过一个C的版本。 本篇就介绍我自己在JavaScript中定义的Vector2类…

基于ssm 框架的java 开发语言的 在线教育学习平台系统设计与实现 源码 论文

博主介绍&#xff1a;专注于Java&#xff08;springboot ssm springcloud等开发框架&#xff09; vue .net php phython node.js uniapp小程序 等诸多技术领域和毕业项目实战、企业信息化系统建设&#xff0c;从业十五余年开发设计教学工作 ☆☆☆ 精彩专栏推荐订阅☆☆☆…

AI周报(9.29-10.5)

AI应用-Elayne公司临终规划和自动化遗产结算 创业公司Elayne成立于2023年&#xff0c;由Adria Ferrier和Jake Grafenstein共同创立&#xff0c;Adria Ferrier担任CEO&#xff0c;总部位于科罗拉多州丹佛市。 Elayne公司专注于遗产规划和结算领域&#xff0c;通过人工智能技术…

实验4 循环结构

1、判断素数 【问题描述】从键盘输入一个大于1的正整数&#xff0c;判断是否为素数 【输入形式】输入一个正整数 【输出形式】输出该数是否为素数 【样例输入】10 【样例输出】10 is not a prime number 【样例说明】样例2 输入&#xff1a;-10 输出&#xff1a;error! #de…

实景三维赋能矿山安全风险监测预警

随着科技的不断进步&#xff0c;实景三维技术在矿山安全风险监测预警中的应用越来越广泛&#xff0c;它为矿山安全管理带来了革命性的变革。 一、矿山安全现状 矿山作为国家重要的能源和原材料基地&#xff0c;其安全生产直接关系到国民经济的发展和社会的稳定。然而&#xf…

【前端vue2 + element ui】Dialog 对话框:.vue组件跳转

【前端vue2 element ui】Dialog 对话框&#xff1a;.vue组件跳转 写在最前面一、父组件调用1、<template>1.1 跳转位置1.2 弹窗调用 2、<script>2.1 import2.2 export2.3 methods 二、子组件调用1、<template>2、<script>2.1 export2.2 watch和method…

不可错过!CMU最新《生成式人工智能大模型》课程:从文本、图像到多模态大模型

1. 课程简介 从生成图像和文本到生成音乐和艺术&#xff0c;生成模型一直是人工智能的关键挑战之一。本课程将探讨推动生成模型和基础模型&#xff08;Foundation Models&#xff09;最近进展的机器学习和人工智能技术。学生将学习、开发并应用最先进的算法&#xff0c;使机器…