Java使用的可以使用的脚本执行引擎

最近在开发工作流系统,其中涉及到大量动态规则的执行场景------比如流程节点的条件判断、任务流转的规则校验、动态计算字段值等。如果把这些规则硬编码到Java代码中,后续维护和扩展会非常麻烦,每次修改规则都需要重新编译、部署项目。

这种场景下,引入脚本执行引擎就非常合适了。脚本引擎允许我们将动态规则以脚本的形式(如Groovy、JavaScript等)存储在数据库或配置文件中,系统运行时动态加载并执行脚本,无需修改核心代码即可实现规则的更新。

本文就梳理一下Java生态中常用的脚本执行引擎,分析它们的特点、适用场景,并结合工作流系统的实际需求给出使用示例,希望能给有类似开发需求的同学提供参考。

一、Java内置脚本引擎:ScriptEngineManager

首先要提的是Java SE自带的脚本引擎框架,它通过javax.script包提供统一的API,支持JavaScript、Ruby、Python等多种脚本语言(具体支持的语言取决于JVM的实现,默认情况下JDK会自带JavaScript引擎)。

优势:无需引入额外依赖,API统一,上手成本低;适合简单的动态规则场景。

劣势:性能一般,对复杂脚本的支持不够友好,高级特性较少。

1.1 核心API介绍

  • ScriptEngineManager:脚本引擎管理器,用于创建和获取脚本引擎实例。

  • ScriptEngine:脚本引擎核心接口,提供执行脚本、绑定变量、获取执行结果等方法。

  • Bindings:用于存储脚本执行时需要的变量(将Java对象传递给脚本)。

  • ScriptContext:脚本执行的上下文,管理变量作用域(如全局作用域、局部作用域)。

1.2 工作流场景示例(JavaScript)

需求:工作流节点流转时,判断当前用户的角色是否为"审批人"且提交的金额是否小于10000,满足条件则允许流转。

java 复制代码
import javax.script.*;

public class BuiltInScriptEngineDemo {
    public static void main(String[] args) throws ScriptException {
        // 1. 创建脚本引擎管理器
        ScriptEngineManager manager = new ScriptEngineManager();
        // 2. 获取JavaScript引擎
        ScriptEngine engine = manager.getEngineByName("JavaScript");
        
        // 3. 绑定工作流上下文变量(用户角色、提交金额)
        Bindings bindings = engine.createBindings();
        bindings.put("userRole", "审批人");
        bindings.put("submitAmount", 8000);
        
        // 4. 定义流转规则脚本(返回布尔值)
        String flowRuleScript = "userRole === '审批人' && submitAmount < 10000;";
        
        // 5. 执行脚本并获取结果
        Object result = engine.eval(flowRuleScript, bindings);
        boolean canPass = (boolean) result;
        
        System.out.println("节点是否允许流转:" + canPass); // 输出:true
    }
}

说明:通过绑定变量将工作流上下文传递给脚本,脚本执行结果作为规则判断依据。这种方式无需修改Java代码,只需修改脚本内容即可调整流转规则。

二、Groovy脚本引擎

Groovy是基于JVM的动态语言,语法与Java非常接近,无缝兼容Java类库,并且提供了更简洁的语法和丰富的高级特性(如闭包、动态类型、元编程等)。在工作流、规则引擎等场景中,Groovy的使用频率非常高。

优势:语法简洁、与Java兼容性好、性能优于内置JavaScript引擎、支持复杂业务逻辑;有成熟的类库和工具支持。

劣势:需要引入额外依赖;相比Java有一定的学习成本(但成本较低)。

2.1 引入依赖(Maven)

xml 复制代码
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>3.0.17</version>
    <type>pom</type>
</dependency>

2.2 工作流场景示例(动态计算任务优先级)

需求:工作流任务的优先级由多个因素动态计算得出,规则为:紧急程度(1-5)* 2 + 参与人数 * 1 + 超时风险系数(0-3)。

java 复制代码
import groovy.lang.GroovyShell;
import groovy.lang.Binding;

public class GroovyScriptEngineDemo {
    public static void main(String[] args) {
        // 1. 创建绑定变量(工作流任务信息)
        Binding binding = new Binding();
        binding.setVariable("urgencyLevel", 4); // 紧急程度
        binding.setVariable("participantCount", 3); // 参与人数
        binding.setVariable("timeoutRisk", 2); // 超时风险系数
        
        // 2. 创建Groovy脚本执行器
        GroovyShell groovyShell = new GroovyShell(binding);
        
        // 3. 定义优先级计算脚本
        String priorityScript = """
                // 优先级计算规则
                def calculatePriority() {
                    return urgencyLevel * 2 + participantCount * 1 + timeoutRisk;
                }
                calculatePriority(); // 执行方法并返回结果
                """;
        
        // 4. 执行脚本并获取结果
        Object result = groovyShell.evaluate(priorityScript);
        int taskPriority = (int) result;
        
        System.out.println("工作流任务优先级:" + taskPriority); // 输出:4*2+3+2=13
    }
}

说明:Groovy支持多行脚本和函数定义,适合复杂的业务规则计算。由于语法与Java接近,Java开发人员可以快速上手,同时其动态特性又能满足规则灵活调整的需求。

三、JRuby & Jython

如果团队中有人熟悉Ruby或Python语言,也可以选择JRuby(Ruby的JVM实现)或Jython(Python的JVM实现)作为脚本执行引擎,它们同样可以无缝调用Java类库,实现与Java程序的交互。

3.1 JRuby(Ruby语法)

依赖引入:

xml 复制代码
<dependency>
    <groupId>org.jruby</groupId>
    <artifactId>jruby</artifactId>
    <version>9.4.6.0</version>
</dependency>

简单示例(判断流程节点是否可跳过):

java 复制代码
import org.jruby.embed.ScriptingContainer;

public class JRubyDemo {
    public static void main(String[] args) {
        ScriptingContainer container = new ScriptingContainer();
        // 绑定变量
        container.put("nodeType", "抄送节点");
        container.put("hasSkipPermission", true);
        // 执行Ruby脚本
        String script = "nodeType == '抄送节点' && hasSkipPermission ? true : false";
        boolean canSkip = (boolean) container.runScriptlet(script);
        System.out.println("节点是否可跳过:" + canSkip); // 输出:true
    }
}

3.2 Jython(Python语法)

依赖引入:

xml 复制代码
<dependency>
    <groupId>org.python</groupId>
    <artifactId>jython-standalone</artifactId>
    <version>2.7.3</version>
</dependency>

简单示例(计算任务预计完成时间):

java 复制代码
import org.python.util.PythonInterpreter;

public class JythonDemo {
    public static void main(String[] args) {
        try (PythonInterpreter interpreter = new PythonInterpreter()) {
            // 绑定变量
            interpreter.set("baseHours", 8); // 基础耗时(小时)
            interpreter.set("difficulty", 1.5); // 难度系数
            // 执行Python脚本
            interpreter.exec("estimatedTime = baseHours * difficulty");
            // 获取结果
            double estimatedTime = interpreter.get("estimatedTime", Double.class);
            System.out.println("任务预计完成时间:" + estimatedTime + "小时"); // 输出:12.0
        }
    }
}

优势:支持Ruby/Python语法,适合熟悉这两种语言的团队;生态丰富,有大量现成的脚本库可用。

劣势:性能相对Groovy略差;在Java生态中的集成度不如Groovy;版本更新相对缓慢。

四、JavaScript脚本执行引擎(独立引擎:Nashorn/GraalJS)

前面提到的Java内置ScriptEngine默认支持的JavaScript引擎,在JDK 8及之前为Nashorn,JDK 11起Nashorn被移除,官方推荐使用GraalJS作为替代。GraalJS是基于GraalVM的高性能JavaScript引擎,不仅完全兼容ECMAScript标准,还支持与Java的深度交互,性能远超传统Nashorn,是工作流系统中JavaScript脚本执行的优选方案。

优势:完全兼容JavaScript标准、支持ES6+特性、与Java交互流畅、GraalJS性能优异;适合熟悉JavaScript的团队,可复用前端脚本逻辑。

劣势:GraalJS需引入额外依赖;JDK版本切换时需注意引擎适配。

4.1 主流JavaScript引擎对比(Nashorn vs GraalJS)

特性 Nashorn GraalJS
JDK支持 JDK 8-10(JDK 11移除) 全JDK版本支持(需引入依赖)
ECMAScript兼容 支持ES5.1,部分ES6特性 支持ES6+全特性
性能 一般 高性能(接近原生JavaScript引擎)
扩展性 有限 支持多语言交互、沙箱隔离

4.2 GraalJS工作流场景示例(复杂流转规则判断)

依赖引入(Maven):

xml 复制代码
<dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js</artifactId>
    <version>23.1.1</version>
</dependency>
<dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js-scriptengine</artifactId>
    <version>23.1.1</version>
</dependency>

需求:工作流节点流转时,需判断多条件组合:用户角色为"审批人"或"管理员"、提交金额小于10000、任务创建时间在24小时内,满足所有条件则允许自动流转。

java 复制代码
import org.graalvm.polyglot.Context;
import org.graalvm.polyglot.Value;

import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;

public class GraalJSDemo {
    public static void main(String[] args) {
        // 1. 准备工作流上下文数据
        String userRole = "审批人";
        double submitAmount = 8500;
        LocalDateTime createTime = LocalDateTime.now().minusHours(12); // 12小时前创建
        LocalDateTime now = LocalDateTime.now();
        long hoursDiff = ChronoUnit.HOURS.between(createTime, now); // 时间差(小时)

        // 2. 创建GraalJS上下文(支持沙箱模式,限制资源访问)
        try (Context context = Context.newBuilder("js")
                .allowAllAccess(false) // 禁用所有外部访问,开启沙箱
                .allowHostAccess(true) // 允许访问Java主机对象
                .build()) {
            // 3. 向脚本上下文绑定Java变量
            context.getBindings("js").putMember("userRole", userRole);
            context.getBindings("js").putMember("submitAmount", submitAmount);
            context.getBindings("js").putMember("hoursDiff", hoursDiff);

            // 4. 定义复杂流转规则脚本(ES6语法)
            String flowRuleScript = """
                    // 多条件组合判断
                    const allowAutoFlow = userRole === '审批人' || userRole === '管理员'
                        && submitAmount < 10000
                        && hoursDiff < 24;
                    allowAutoFlow; // 返回判断结果
                    """;

            // 5. 执行脚本并获取结果
            Value result = context.eval("js", flowRuleScript);
            boolean allowAutoFlow = result.asBoolean();

            System.out.println("是否允许自动流转:" + allowAutoFlow); // 输出:true
        }
    }
}

说明:GraalJS支持ES6+语法,可编写更简洁、易维护的复杂规则脚本。通过沙箱模式可限制脚本的资源访问权限,降低安全风险,非常适合工作流这类需要动态执行用户自定义规则的场景。

五、各引擎对比与工作流系统选型建议

为了方便大家根据实际需求选择,整理了各引擎的核心特性对比:

引擎类型 核心优势 核心劣势 工作流场景适配度
Java内置ScriptEngine 无依赖、API统一、上手快 性能一般、复杂逻辑支持差、ES版本兼容有限 ★★★☆☆(简单规则判断,JDK8及以下环境)
Groovy Java兼容好、语法简洁、性能优、支持复杂逻辑 需引入依赖、少量学习成本 ★★★★★(首选,适合各类动态规则、计算场景)
JRuby/Jython 支持Ruby/Python语法、生态丰富 性能一般、集成度一般、更新慢 ★★★☆☆(团队熟悉对应语言时可选)
GraalJS(JavaScript) ES6+兼容、性能优异、支持沙箱、前端脚本复用 需引入依赖、GraalVM相关配置有一定复杂度 ★★★★☆(熟悉JavaScript团队、需复用前端逻辑场景)

选型建议:

  1. 如果是简单的规则判断(如节点流转条件、字段合法性校验),且不想引入额外依赖,可选择Java内置的ScriptEngine(JavaScript)。

  2. 如果涉及复杂的业务规则计算(如任务优先级、动态评分、多条件组合判断),优先选择Groovy------它与Java的兼容性最好,性能也能满足大部分工作流场景的需求,是工作流系统的主流选择。

  3. 如果团队中有Ruby/Python开发者,且需要复用现有脚本资源,可选择JRuby或Jython,但需注意性能问题。

  4. 如果是动态文本生成场景(如任务通知、表单模板),可单独选用Velocity、FreeMarker等模板引擎,它们与脚本执行引擎定位不同,前者专注文本渲染,后者专注逻辑执行,需根据场景区分使用。

六、工作流系统中使用脚本引擎的注意事项

  1. 安全风险:脚本引擎允许执行动态代码,存在代码注入风险。建议:① 对输入的脚本内容进行校验;② 限制脚本执行的权限(如禁止访问文件系统、网络);③ 采用沙箱模式执行脚本。

  2. 性能优化:脚本执行会有一定的性能开销,尤其是频繁执行的场景。建议:① 对常用脚本进行缓存(如缓存编译后的脚本对象);② 避免在脚本中执行复杂的循环或IO操作;③ 对脚本执行时间设置超时限制,防止无限循环。

  3. 调试与监控:动态脚本的调试难度较大,建议:① 记录脚本执行日志(包括输入参数、执行结果、异常信息);② 提供脚本测试工具,方便开发和运维人员调试;③ 监控脚本执行性能(如执行时间、成功率)。

  4. 版本管理:脚本规则可能会多次修改,建议对脚本进行版本管理,记录每次修改的内容、时间和责任人,便于回滚和追溯。

七、总结

在工作流系统开发中,脚本执行引擎是实现规则动态化的关键技术之一,它能有效降低系统的耦合度,提高规则的可维护性和扩展性。

本文介绍的几种引擎各有优劣:Java内置ScriptEngine适合简单场景,Groovy适合复杂业务规则,JRuby/Jython适合特定技术栈团队,GraalJS适合熟悉JavaScript的团队。实际开发中,建议根据团队技术栈、业务复杂度和性能需求选择合适的引擎。

最后,再次提醒大家注意脚本执行的安全风险和性能优化,做好调试监控和版本管理工作,确保系统稳定运行。

如果大家在工作流系统中使用脚本引擎有其他经验或问题,欢迎在评论区交流讨论!

参考资料:

(注:文档部分内容可能由 AI 生成)

相关推荐
幻云20102 小时前
Next.js指南:从入门到精通
开发语言·javascript·人工智能·python·架构
老马识途2.02 小时前
java处理接口返回的json数据步骤 包括重试处理,异常抛出,日志打印,注意事项
java·开发语言
2***d8852 小时前
Spring Boot中的404错误:原因、影响及处理策略
java·spring boot·后端
c***69302 小时前
Springboot项目:使用MockMvc测试get和post接口(含单个和多个请求参数场景)
java·spring boot·后端
6***A6632 小时前
Springboot中SLF4J详解
java·spring boot·后端
五阿哥永琪2 小时前
Hutool中常用的工具类&真实项目的黄金组合
java
CCPC不拿奖不改名2 小时前
网络与API:从HTTP协议视角理解网络分层原理+面试习题
开发语言·网络·python·网络协议·学习·http·面试
xun-ming2 小时前
Redis实战之7种数据结构
java
5***84642 小时前
Spring Boot的项目结构
java·spring boot·后端