一、学习背景
核心代码gitee地址:ScriptDemo: java脚本语言实现动态编程
最近学习了下动态编程的java相关技术,针对日常开发中经常有业务规则变换的场景,频繁修改代码发布极为不便,这时候可以利用java支持的动态编程技术,把不停变换的业务规则封装成其他语言的逻辑函数(或者java类本身也可以),作为不同的脚本模板,以字符串的形式存储在数据库中,甚至可以直接在前端界面编写脚本语言,不需要重新发布版本,灵活实现业务逻辑的变更。
常用的脚本语言有
ruby
go
javascript
groovy
python
scala
这里只介绍groovy、javascript(会两种就可以了,其他的语言笔者暂不熟悉,有空再研究)
二、Groovy语法引擎
1.引入groovy依赖
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.21</version> </dependency>
代码如下:
/** * 执行脚本,脚本实例化为类对象,脚本以字符串形式存储在数据库中时使用,使用时直接获取脚本模板字符串,编译成字节码,再实例化,这种方式灵活, * 为了避免频繁GC,可以将脚本对象缓存到缓存工具类中,以脚本内容作为键(这里没有使用redis,因为涉及到序列化问题,redis缓存不好处理) * 脚本内容如下 * def execute(String ruleName) { * // 动态规则的执行逻辑 * println(ruleName) * } */ @PostMapping("/executeScriptByGroovyObject") public String executeScriptByGroovyObject(@RequestBody String scriptContent) throws CompilationFailedException, IOException { // 检查groovyScriptCache中是否已存在相同脚本 if (groovyScriptCache.containsKey(scriptContent)) { // 直接从缓存中获取已实例化的Groovy对象并执行 GroovyObject groovyObject = groovyScriptCache.get(scriptContent); // 执行脚本 groovyObject.invokeMethod("execute", new Object[]{"从缓存中获取的Groovy对象测试"}); //groovyScriptCache.remove(scriptContent); return "执行缓存中的脚本"; } else { // 尝试从groovyScriptCache获取已实例化的Groovy对象 //groovy每执行一次脚本,都会生成一个脚本的class对象,如果重复执行同一脚本,则会导致生成多个class对象,频繁触发GC,影响性能 //所以这里需要缓存已实例化的脚本对象,以脚本内容作为键 GroovyObject groovyObject = groovyScriptCache.get(scriptContent); if (groovyObject == null) { // 缓存中不存在该脚本,需要编译并实例化新的Groovy对象 groovyObject = getGroovyObject(scriptContent); // 将脚本对象缓存到groovyScriptCache groovyScriptCache.put(scriptContent, groovyObject); } // 执行脚本的execute方法,传入参数 groovyObject.invokeMethod("execute", new Object[]{"从外界传入的Groovy对象测试"}); return "执行新的脚本"; } }/** * 编译并实例化Groovy脚本对象 */ private GroovyObject getGroovyObject(String scriptContent) throws CompilationFailedException, IOException { GroovyClassLoader classLoader = new GroovyClassLoader(); try (classLoader) { Class<?> groovyClass = (Class<?>) classLoader.parseClass(scriptContent); return (GroovyObject) groovyClass.getDeclaredConstructor().newInstance(); } catch (InstantiationException | IllegalAccessException | NoSuchMethodException e) { throw new RuntimeException("无法实例化Groovy脚本", e); } catch (InvocationTargetException e) { throw new RuntimeException(e); } }
代码解释
将脚本以字符串形式传入,使用GroovyClassLoader 类装载器转换为GroovyObject对象,同时为了避免频繁调用产生GC,使用缓存对象将转换后的对象缓存起来
使用apifox测试如下
package com.hl.scriptDemo.scripts
/**
* @author hulei
* @date 2024/2/7 17:43
*/
class RuleScript {
def execute(String ruleName) {
// 动态规则的执行逻辑
println("执行动态规则:"+ruleName)
}
}
此列测试传入的脚本是一个完整的groovy类字符串,会执行其中定义的def方法excute
也可以使用如下脚本
def execute(String ruleName) {
// 动态规则的执行逻辑
println("执行动态规则:"+ruleName)
}
经过比较,可以知道,脚本不需要写成一个完整的groovy类 字符串,单个方法也可以,
当然一个groovy类字符串写多个方法也是可以的
代码里指定调用不同的方法
输出结果如下
多个独立方法写在一起
运行结果如下:
由此可以看出groovy脚本形式 比较灵活
三、javascript语法引擎
此种方式和groovy差不多,只是脚本语言换成了js,就更简单了
由于jdk8以后删除了自带的javascript引擎nashorn,故使用GraalVM的Graal.js代替
GraalVM是oracle最新推出一种虚拟机,可以运行使用多种不同语言编写的程序,如JavaScript、Python、Ruby、R、在Java虚拟机(JVM)上运行的语言,如Java、Scala、Groovy、Kotlin、Clojure,或在LLVM上运行的语言,如C和C ++,可以说是非常强大了
代码如下:
/** * javaScript引擎执行脚本 * js脚本如下 * function add(a,b){ * let sum = a+b * return sum * } */ @PostMapping("/executeScriptByJavaScriptEngine") public String executeScriptByJavaScriptEngine(@RequestBody String scriptContent) throws CompilationFailedException { //jdk8以后把自带的javaScript引擎nashorn移除了,使用GraalVM的Graal.js代替 //需要自己在pom.xml中添加相关依赖依赖 try (Context context = Context.create()) { // 在JavaScript引擎中执行简单的JavaScript代码 Value result = context.eval("js", "1 + 2"); System.out.println(result.asInt()); // 调用JavaScript函数 context.eval("js", scriptContent); Value multiplyFunction = context.getBindings("js").getMember("add"); Value product = multiplyFunction.execute(3, 4); return String.valueOf(product.asInt()); } catch (Exception e) { log.error("执行JavaScript脚本失败", e); return "执行JavaScript脚本失败"; } }
脚本代码:
//加法
function add(a,b){
let sum = a+b
return sum
}//乘法
function multiply(a,b){
let multiply = a*b
return multiply
}
写了两个自定义的简单函数
代码中调用了 multiply方法的函数,结果如下
实际开发中可以灵活的把业务相关脚本语言规则直接写成函数的形式,方便调用