kotlin 1.1 でjavax.script API(JSR-223)がサポートされています。
https://kotlinlang.org/docs/reference/whatsnew11.html#javaxscript-support
これにより、kotlin上でkotlin(script)ソースをコンパイルして実行することができます。
公式にサンプルソースがありますので参照ください。
https://github.com/JetBrains/kotlin/tree/master/libraries/examples/kotlin-jsr223-local-example
以下、おおまかに説明します。
下記内容は kotlin 1.2.10 で確認しています。
目次
- 準備
- 実装
2.1 単純な実行
2.2 変数を渡す
2.3 コンパイル
2.4 eval - 応用例:簡易テンプレートエンジン
1. 準備
Gradleプロジェクトを前提とします。
通常のkotlinプロジェクトにkotlin-script-utilパッケージを依存に加えます。
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.2.10"
}
}
apply plugin: 'kotlin'
repositories {
mavenCentral()
}
dependencies {
compile 'org.jetbrains.kotlin:kotlin-stdlib-jre8'
compile 'org.jetbrains.kotlin:kotlin-script-util' // <--- 追加
}
compileKotlin {
kotlinOptions.jvmTarget = '1.8'
}
SPI(Service Provider Interface)という仕組みを採用しています。src/main/resources/META-INF/services/javax.script.ScriptEngineFactory
というファイルを作成し、以下の1行を書きます。
これでFactoryを見つけることができます。
org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory
2. 実装
2.1 単純な実行
まずは簡単な例を実行します。
import javax.script.ScriptEngineManager
fun main(args: Array<String>) {
val engine = ScriptEngineManager().getEngineByExtension("kts")
engine.eval("println(\"Hello Kotlin\")") // Hello Kotlin
}
ScriptEngineManager().getEngineByExtension("kts")
でScriptEngine
を取得します。
(ScriptEngineManager().getEngineByName("kotlin")
でも取得できます。)
実態はorg.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngine
になります。
ScriptEngine#eval()
でkotlinコードを評価します。
さらに、eval()
は評価した結果を受け取ることができます。
fun main(args: Array<String>) {
val engine = ScriptEngineManager().getEngineByExtension("kts")
val route2 = engine.eval("kotlin.math.sqrt(2.0)") // kotlin.mathは1.2以上
println(route2) // 1.4142135623730951
}
2.2 変数を渡す
スクリプトに変数を渡すことができます。
(ただし、あまり使い勝手が良くありません。)
fun main(args: Array<String>) {
val engine = ScriptEngineManager().getEngineByExtension("kts")
val bindings = engine.createBindings()
bindings["x"] = "Hello!"
engine.eval("""
val xxx = bindings["x"]
println(xxx)
""", bindings) // Hello!
}
engine.createBindings()
でBindings
インスタンスを取得し、MutableMap<String, Any?>
のように値を入れます。
eval()
の第2引数にBindings
インスタンスを指定し、スクリプトのなかでbindings
という変数で受け取ります。
スクリプトの中のbindings
という変数名は固定で、Map<String, Any?>
型になっており、bindings["変数名"]
で取得できる値はAny?
型になるため、適切な型にキャストする必要があります。
fun main(args: Array<String>) {
val engine = ScriptEngineManager().getEngineByExtension("kts")
val bindings = engine.createBindings()
bindings["x"] = "Hello!"
engine.eval("""
val xxx = bindings["x"] as String
println(xxx.substring(1)) // as Stringがないとエラーになる
""", bindings) // Hello!
}
2.3 コンパイル
上記の例ではeval()
でコンパイルと実行を同時に行なっていましたが、事前にコンパイルだけ行い、コンパイルした結果を再利用することができます。
import javax.script.Compilable
fun main(args: Array<String>) {
val engine = ScriptEngineManager().getEngineByExtension("kts")
val compiled = (engine as Compilable).compile("""
val xxx = bindings["x"]
println(xxx)
""")
val bindings = engine.createBindings()
bindings["x"] = "Hello!"
compiled.eval(bindings)
}
engine
をCompilable
にキャストしてcompile()
でスクリプトをコンパイルしてCompiledScript
インスタンスを生成します。
その後、eval()
で評価します。
スクリプトを再利用する場合はこのCompiledScript
インスタンスをキャッシュしておくと良いでしょう。
2.4 eval
スクリプトの中でスクリプトを組み立てて評価するeval()
というメソッドが使えます。
あまりいい例が思いつきませんでしたが、公式も同じような例でした。
fun main(args: Array<String>) {
val engine = ScriptEngineManager().getEngineByExtension("kts")
engine.eval("""
val x = 1
val y = eval("${'$'}x + 1")
println(y) // 2
""")
}
3. 応用例:簡易テンプレートエンジン
以下のように、この技術を使って簡易テンプレートエンジンを作ることができます。
https://github.com/sdeleuze/kotlin-script-templating
要は最終的に文字列を返すスクリプトを用意し、それを評価すれば完成です。
ロジックは${}
で記述します。式を返す必要があるので注意が必要です。
変数はbindings
で受け取るため、変数宣言が必要になります。
import jsr223example.*
val name = bindings["name"] as String
@Suppress("UNCHECKED_CAST")
val users = bindings["users"] as List<User>
val condition = true
// ここからテンプレート
"""
Hello! ${name}
${if (condition) {
"""
Hello!
"""
} else ""}
${users.map {
"""
Hello! ${it.name}
"""}.joinToString("\n")}
""" // ここまで
- if文はelse句がないと値を返さないためエラーになるのでelseが必須です。
- for文は値を返さないので使用できません。その代わり、
map
やjoinToSting
などを使います。 -
bindings
はスクリプト上では未定義の変数なので、Intellijのエディタ上ではエラーになります。それを回避するため、別ファイルで変数をlateinit宣言してimportすることで見た目上はエラーになりません。
package jsr223example
// Hack for Intellij
lateinit var bindings: Map<String, Any?>
実用で使用するのは難しいかもしれませんが、ちょっとしたツールなどには使えるかもしれません。
以上になります。
上記のソースは
https://github.com/tiibun/jsr223example
に置いています。