1. 處理中類別載入

處理中類別載入 

預設情況下,sbt 會在其自身的 JVM 實例中執行 runtest 任務。它通過在隔離的 ClassLoader 中調用任務來模擬執行外部 Java 命令。與forking相比,此方法減少了啟動延遲和總執行時間。僅僅重用 JVM 所帶來的效能提升是有限的。類別載入和應用程式相依性的連結會佔用許多應用程式的啟動時間。sbt 通過在執行之間重複使用某些已載入的類別來減少啟動延遲。它通過建立一個分層的 ClassLoader 來實現這一點,該 ClassLoader 遵循 Java ClassLoader 的標準委派模型。最外層(始終包含專案特定的類別檔案和 jar 檔案)會在執行之間被丟棄。然而,內層可以被重複使用。

從 sbt 1.3.0 開始,可以配置 sbt 用於產生分層 ClassLoader 實例的特定方法。它通過 classLoaderLayeringStrategy 指定。有三個可能的值:

  1. ScalaLibrary - 最外層的父層能夠載入 Scala 標準函式庫以及 Scala 反射函式庫,前提是它位於應用程式類別路徑上。這是預設策略。它最類似於 sbt 版本 < 1.3.0 提供的分層 ClassLoader
  2. AllLibraryJars - 在 Scala 函式庫層和最外層之間,為所有相依性 jar 檔案新增了一個額外層。啟用 Turbo 模式時,它是預設策略。與 ScalaLibrary 相比,此策略可以顯著提高啟動和總執行時間的效能。如果任何函式庫具有可變的全域狀態,則結果可能不一致,因為與 ScalaLibrary 不同,全域狀態會在執行之間保持。當任何函式庫使用 Java 序列化時,應避免使用 AllLibraryJars
  3. Flat - 不使用分層。任務的 fullClasspath 金鑰所指定的完整類別路徑會載入到最外層。如果在 ScalaLibrary 遇到任何問題,或者如果應用程式要求所有類別都載入到同一個 ClassLoader 中(對於某些 Java 序列化的使用情況,可能是這種情況),請考慮使用此選項來代替 fork。

可以在不同的組態中設定 classLoaderLayeringStrategy。例如,要在 Test 組態中使用 AllLibraryJars 策略,請將

Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.AllLibraryJars

新增到 build.sbt 檔案中。假設 build.sbt 檔案沒有其他變更,則 run 任務仍將使用 ScalaLibrary 策略。

疑難排解 

當與分層類別載入器一起使用時,Java 反射可能會導致問題,因為通過反射載入另一個類別的類別方法可能無法存取要載入的類別。如果類別是使用 Class.forNameThread.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,則會失敗並出現 ClassNotFoundException,因為後續線程的內容類別載入器是 Scala 函式庫類別載入器,無法載入專案類別。要解決此限制,而無需將分層策略更改為 Flat,可以執行以下操作:

  1. 使用 Class.forName 而不是 ClassLoader.loadClass。JVM 隱式使用呼叫類別的載入器來載入使用 Class.forName 的類別。在這種情況下,ReflectionExample 是呼叫類別,它將與 Foo 位於同一個類別載入器中,因為它們都是專案類別路徑的一部分。
  2. 提供用於載入的類別載入器。在上面的範例中,可以通過將 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)
}