1. プロセス内クラスローディング

プロセス内クラスローディング 

デフォルトでは、sbtはrunタスクとtestタスクを独自のJVMインスタンス内で実行します。外部のjavaコマンドの実行を、独立したClassLoader内でタスクを呼び出すことでエミュレートします。フォーキングと比較して、このアプローチは起動レイテンシと総実行時間を削減します。JVMの再利用によるパフォーマンスの向上はわずかです。アプリケーションの依存関係のクラスローディングとリンクは、多くのアプリケーションの起動時間を支配します。sbtは、実行間でロードされたクラスの一部を再利用することで、この起動レイテンシを削減します。これは、JavaのClassLoaderの標準的な委譲モデルに従って、階層化されたClassLoaderを作成することで実現します。プロジェクト固有のクラスファイルとjarファイルが常に含まれる最外層は、実行間で破棄されます。しかし、内側のレイヤーは再利用できます。

sbt 1.3.0以降、sbtが階層化されたClassLoaderインスタンスを生成する際に取る特定のアプローチを設定できるようになりました。これはclassLoaderLayeringStrategyを介して指定されます。3つの可能な値があります。

  1. ScalaLibrary - 最外層の親は、アプリケーションのクラスパス上に存在する場合、Scala標準ライブラリとScala reflectライブラリをロードできます。これはデフォルトの戦略です。sbt 1.3.0より前のバージョンで提供される階層化されたClassLoaderと最も似ています。
  2. AllLibraryJars - Scalaライブラリ層と最外層の間に、すべての依存関係jarのための追加の層を追加します。ターボモードが有効になっている場合のデフォルト戦略です。この戦略は、ScalaLibraryと比較して、起動時間と総実行時間を大幅に向上させることができます。ライブラリのいずれかに変更可能なグローバル状態がある場合、結果は一貫しない可能性があります。なぜなら、ScalaLibraryとは異なり、グローバル状態は実行間で保持されるからです。ライブラリがJavaシリアライゼーションを使用する場合、AllLibraryJarsは避けるべきです。
  3. 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 runReflectionExampleを実行すると、コンテキストクラスローダーがスレッドをバックアップするFutureのスレッドであり、プロジェクトクラスをロードできないScalaライブラリクラスローダーであるため、ClassNotFoundExceptionが発生して失敗します。Flatに階層化戦略を変更せずにこの制限を回避するには、次のことができます。

  1. ClassLoader.loadClassの代わりにClass.forNameを使用します。jvmは、Class.forNameを使用してクラスをロードする場合、暗黙的に呼び出し元のクラスのローダーを使用します。この場合、ReflectionExampleは呼び出し元のクラスであり、両方ともプロジェクトのクラスパスの一部であるため、Fooと同じクラスローダーにあります。
  2. ロードのためのクラスローダーを提供します。上記の例では、val cl = Thread.currentThread.getContextClassLoaderval 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)
}