最近在找如何让kotlin检查自身代码更改,找了一圈发现找不到, 于是找到了很多脚本引擎. 脚本引擎相当于使用一个寄生语言, 而使用脚本引擎的语言相当于一个宿主语言. 脚本引擎有什么用呢?
✔ 首先,可以使用其它编程语言来执行代码片段. 包括
javascript,R语言,还有python之类.✔ 然后,可以使用
脚本引擎来编排可视化工作流. 比如要想实现像coze和dify工作流中的代码执行节点.✔ 第三, 可以用来执行复杂的DAG依赖任务,比如maven,gradle这样的依赖任务, 还比如简单的"m3u8"视频文件下载.
✔ 第四, 可以用来识别代码更改. 代码更改之后,自动可以运行任务.
经过多次测试后,我发现脚本引擎支持的语言很多都是部分功能可用, 其它功能提供不完全.比如javascript引擎, 竟然大多引擎不提供运行命令行的功能. 找到的脚本引擎有: java自带的脚本引擎, Kotlin Multiplatform脚本引擎., Aviator脚本引擎, rhino脚本引擎, GraalVM脚本引擎, renjin脚本引擎

java自带的脚本引擎
java自身提供一个Scripting API, 可以支持第三方语言以脚本的方式运行. java实现了一个默认的javascript语言的引擎.但是java11版本以后就不自带引擎了,需要单独添加引擎依赖才能用. 比如可以通过导入nashorn依赖或者Rhino依赖来单独支持javascript.
Aviator引擎
这是google开发的表达式引擎,能够快速求值表达式, 有一套自己的语法体系. 可以通过标准的接口向寄生语言添加宿主的功能.
只需要添加一个依赖,就能使用
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>5.3.3</version>
</dependency>
kotlin代码如下
import com.googlecode.aviator.AviatorEvaluator
import com.googlecode.aviator.runtime.function.AbstractFunction
import com.googlecode.aviator.runtime.function.FunctionUtils
import com.googlecode.aviator.runtime.type.AviatorDouble
import com.googlecode.aviator.runtime.type.AviatorObject
object TestAviator {
@JvmStatic
fun main(args: Array<String>) {
//注册函数
AviatorEvaluator.addFunction(AddFunction())
println(AviatorEvaluator.execute("add(1, 2)")) // 3.0
println(AviatorEvaluator.execute("add(add(1, 2), 100)")) // 103.0
}
}
internal class AddFunction : AbstractFunction() {
override fun call(env: Map<String, Any>, arg1: AviatorObject, arg2: AviatorObject): AviatorObject {
val left = FunctionUtils.getNumberValue(arg1, env)
val right = FunctionUtils.getNumberValue(arg2, env)
return AviatorDouble(left.toDouble() + right.toDouble())
}
override fun getName(): String {
return "add"
}
}
添加功能的过程很麻烦,但是对于宿主来说很安全. 有点像咱们养的猫狗, 给食物才能吃,不给的食物不能吃.
Rhino引擎
Rhino由Mozilla基金会所管理. 它最开始被加入到jdk1.6的默认脚本引擎中, 到jdk1.8中默认引擎被Nashorn取代. 到jdk11以后所有引擎都不默认了.
现在rhino还能使用, 使用需要添加maven依赖
<!-- https://mvnrepository.com/artifact/org.mozilla/rhino -->
<dependency>
<groupId>org.mozilla</groupId>
<artifactId>rhino</artifactId>
<version>1.8.0</version>
</dependency>
可以直接使用宿主的类.
package org.xxx
import org.mozilla.javascript.Context
import java.io.File
fun main() {
var ctx = Context.enter()
var scope = ctx.initStandardObjects()
var script = """
org.xxx.MyFile("d:/tt.tt").writeText("abcc")
"abc"
"""
var result = ctx.evaluateString(scope, script, "script", 1, null)
Context.exit()
}
class MyFile(var file: String){
@JvmOverloads
fun writeText(str: String){
File(file).writeText(str)
}
}
这就更像是寄生虫, 宿主的资源就是它的资源,尽管使用.
GraalVM引擎
需要添加两个依赖:
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js</artifactId>
<version>24.2.2</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.graalvm.js</groupId>
<artifactId>js-scriptengine</artifactId>
<version>24.2.2</version>
</dependency>
这个引擎说明上写,可以通过import方式来导入".mjs"文件模块. 也可以通过打开选项的方式使用"require"导入模块,但是我测试了一下, 只能通过动态导入import方式导入".mjs"文件模块. 但是导入不了nodejs的模块.
Nashorn引擎
需要引入一个依赖,然后就可以在kotlin中使用了
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.6</version>
</dependency>
使用方法和Rhino差不多, 一个kotlin代码例子:
package org.xxx
import javax.script.ScriptEngineManager
import javax.script.ScriptException
class MyExample {
@JvmOverloads
fun hello(str: String= ""): String{
return("hello $str")
}
}
fun main(args: Array<String>) {
try {
val manager = ScriptEngineManager()
val engine = manager.getEngineByName("JavaScript")
engine.eval("""
//这里可以导入maven依赖包
var MyExample = Java.type("org.xxx.MyExample")
var rst = new MyExample().hello("world");
""");
println(engine.get("rst"))
} catch (e: ScriptException) {
e.printStackTrace()
}
}
Renjin引擎
这个是R语言运行的脚本引擎, R语言是统计学相关的语言,非常强大. 但是由于kotlin的问题,导致代码中使用美元符号要使用多次.
添加依赖就可以直接使用renjin:
<dependency>
<groupId>org.renjin</groupId>
<artifactId>renjin-script-engine</artifactId>
<version>0.9.2726</version>
</dependency>
注意下载需要添加特殊maven仓库:
<repository>
<id>temp repo</id>
<url>https://nexus.bedatadriven.com/content/groups/public/</url>
</repository>
在kotlin的使用方法:
import org.renjin.script.RenjinScriptEngineFactory
import java.io.File
import javax.script.ScriptEngine
object TryRenjin {
@Throws(Exception::class)
@JvmStatic
fun main(args: Array<String>) {
// create a script engine manager:
val factory: RenjinScriptEngineFactory = RenjinScriptEngineFactory()
// create a Renjin engine:
val engine: ScriptEngine = factory.getScriptEngine()
var result = engine.eval("""
import(org.xxx.MyFile)
file = MyFile${'$'}new("d:/tt.txt")
file${'$'}writeText("tt")
"def"
""")
println(result)
}
}
class MyFile(var file: String){
@JvmOverloads
fun writeText(str:String) {
File(file).writeText(str)
}
}
本来就一个美元符号$, 为什么要写成那么复杂的代码${'$'}.
脚本引擎提供的功能
虽然脚本引擎能够使用宿主的执行环境来执行各种功能, 但是能够提供自生语言的功能,使用起来也方便一点吧.下面列出各种引擎给出基本的功能.
| 基本功能 | Nashorn | Aviator | Rhino | GraalVM | Renjin |
|---|---|---|---|---|---|
| Date类型 | ✔ | ✔(不全) | ✔ | ✔ | ✔ |
| sleep多少秒 | ✔ | ✘ | ✔ | ✔ | ✔ |
| 运行命令行 | ✘ | ✘ | ✘ | ✘ | ✘ |
| 导入寄生依赖包 | ✘ | ✔ | ✘ | ✘ | ✔ |
| 导入宿主依赖包 | ✘ | ✘ | ✔ | ✔ | ✔ |
| 读写文件 | ✘ | ✔ | ✘ | ✘ | ✘ |
能够导入宿主依赖包 就是说寄生语言可以导入宿主语言的所有maven依赖.
导入寄生依赖包 就是说寄生语言可以使用自身import和require语句导入包