デフォルトでは、sbtはrun
タスクとtest
タスクを独自のJVMインスタンス内で実行します。外部のjavaコマンドの実行を、独立したClassLoader
内でタスクを呼び出すことでエミュレートします。フォーキングと比較して、このアプローチは起動レイテンシと総実行時間を削減します。JVMの再利用によるパフォーマンスの向上はわずかです。アプリケーションの依存関係のクラスローディングとリンクは、多くのアプリケーションの起動時間を支配します。sbtは、実行間でロードされたクラスの一部を再利用することで、この起動レイテンシを削減します。これは、JavaのClassLoaderの標準的な委譲モデルに従って、階層化されたClassLoader
を作成することで実現します。プロジェクト固有のクラスファイルとjarファイルが常に含まれる最外層は、実行間で破棄されます。しかし、内側のレイヤーは再利用できます。
sbt 1.3.0以降、sbtが階層化されたClassLoader
インスタンスを生成する際に取る特定のアプローチを設定できるようになりました。これはclassLoaderLayeringStrategy
を介して指定されます。3つの可能な値があります。
ScalaLibrary
- 最外層の親は、アプリケーションのクラスパス上に存在する場合、Scala標準ライブラリとScala reflectライブラリをロードできます。これはデフォルトの戦略です。sbt 1.3.0より前のバージョンで提供される階層化されたClassLoader
と最も似ています。AllLibraryJars
- Scalaライブラリ層と最外層の間に、すべての依存関係jarのための追加の層を追加します。ターボモードが有効になっている場合のデフォルト戦略です。この戦略は、ScalaLibrary
と比較して、起動時間と総実行時間を大幅に向上させることができます。ライブラリのいずれかに変更可能なグローバル状態がある場合、結果は一貫しない可能性があります。なぜなら、ScalaLibrary
とは異なり、グローバル状態は実行間で保持されるからです。ライブラリがJavaシリアライゼーションを使用する場合、AllLibraryJars
は避けるべきです。Flat
- 階層化は使用されません。タスクのfullClasspath
キーで指定された完全なクラスパスが最外層にロードされます。ScalaLibrary
で問題が発生した場合、またはアプリケーションがすべてのクラスを同じClassLoader
にロードする必要がある場合(Javaシリアライゼーションの一部の使用法の場合など)、フォークの代替として使用することを検討してください。classLoaderLayeringStrategy
は、さまざまな構成で設定できます。たとえば、Test
構成でAllLibraryJars
戦略を使用するには、
Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars
をbuild.sbt
ファイルに追加します。build.sbt
ファイルへの他の変更がないと仮定すると、run
タスクは引き続きScalaLibrary
戦略を使用します。
Javaのリフレクションは、階層化されたクラスローダーで使用する場合に問題を引き起こす可能性があります。なぜなら、リフレクションを介して別のクラスをロードするクラスメソッドが、ロードされるクラスにアクセスできない可能性があるからです。これは、Class.forName
またはThread.currentThread.getContextClassLoader.loadClass
を使用してクラスがロードされる場合に特に可能性が高いです。次の例を考えてみましょう。
package example
import scala.concurrent.{ Await, Future }
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration.Duration
object ReflectionExample {
def main(args: Array[String]): Unit = Await.result(Future {
val cl = Thread.currentThread.getContextClassLoader
println(cl.loadClass("example.Foo"))
}, Duration.Inf)
}
class Foo
sbtのデフォルトのScalaLibrary
戦略を使用してsbt run
でReflectionExample
を実行すると、コンテキストクラスローダーがスレッドをバックアップするFutureのスレッドであり、プロジェクトクラスをロードできないScalaライブラリクラスローダーであるため、ClassNotFoundException
が発生して失敗します。Flat
に階層化戦略を変更せずにこの制限を回避するには、次のことができます。
ClassLoader.loadClass
の代わりにClass.forName
を使用します。jvmは、Class.forName
を使用してクラスをロードする場合、暗黙的に呼び出し元のクラスのローダーを使用します。この場合、ReflectionExample
は呼び出し元のクラスであり、両方ともプロジェクトのクラスパスの一部であるため、Foo
と同じクラスローダーにあります。val cl = Thread.currentThread.getContextClassLoader
をval cl = getClass.getClassLoader
に置き換えることで実行できます。ケース(2)の場合、名前ルックアップがライブラリによって実行される場合、ルックアップを実行するライブラリメソッドにClassLoader
パラメーターを追加できます。たとえば、
object Library {
def lookup(name: String): Class[_] =
Thread.currentThread.getContextClassLoader.loadClass(name)
}
は次のように書き直すことができます。
object Library {
def lookup(name: String): Class[_] =
lookup(name, Thread.currentThread.getContextClassLoader)
def lookup(name: String, loader: ClassLoader): Class[_] =
loader.loadClass(name)
}