LoginSignup
23
13

More than 5 years have passed since last update.

kotlin内でkotlin scriptを実行する

Last updated at Posted at 2017-12-20

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 で確認しています。

目次

  1. 準備
  2. 実装
    2.1 単純な実行
    2.2 変数を渡す
    2.3 コンパイル
    2.4 eval
  3. 応用例:簡易テンプレートエンジン

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)
}

engineCompilableにキャストして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で受け取るため、変数宣言が必要になります。

template.kts
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文は値を返さないので使用できません。その代わり、mapjoinToStingなどを使います。
  • bindingsはスクリプト上では未定義の変数なので、Intellijのエディタ上ではエラーになります。それを回避するため、別ファイルで変数をlateinit宣言してimportすることで見た目上はエラーになりません。
ScriptUtil.kt
package jsr223example

// Hack for Intellij
lateinit var bindings: Map<String, Any?>

実用で使用するのは難しいかもしれませんが、ちょっとしたツールなどには使えるかもしれません。

以上になります。

上記のソースは
https://github.com/tiibun/jsr223example
に置いています。

23
13
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
13