預設情況下,sbt 會在其自身的 JVM 實例中執行 run
和 test
任務。它通過在隔離的 ClassLoader
中調用任務來模擬執行外部 Java 命令。與forking相比,此方法減少了啟動延遲和總執行時間。僅僅重用 JVM 所帶來的效能提升是有限的。類別載入和應用程式相依性的連結會佔用許多應用程式的啟動時間。sbt 通過在執行之間重複使用某些已載入的類別來減少啟動延遲。它通過建立一個分層的 ClassLoader
來實現這一點,該 ClassLoader
遵循 Java ClassLoader 的標準委派模型。最外層(始終包含專案特定的類別檔案和 jar 檔案)會在執行之間被丟棄。然而,內層可以被重複使用。
從 sbt 1.3.0 開始,可以配置 sbt 用於產生分層 ClassLoader
實例的特定方法。它通過 classLoaderLayeringStrategy
指定。有三個可能的值:
ScalaLibrary
- 最外層的父層能夠載入 Scala 標準函式庫以及 Scala 反射函式庫,前提是它位於應用程式類別路徑上。這是預設策略。它最類似於 sbt 版本 < 1.3.0 提供的分層 ClassLoader
。AllLibraryJars
- 在 Scala 函式庫層和最外層之間,為所有相依性 jar 檔案新增了一個額外層。啟用 Turbo 模式時,它是預設策略。與 ScalaLibrary
相比,此策略可以顯著提高啟動和總執行時間的效能。如果任何函式庫具有可變的全域狀態,則結果可能不一致,因為與 ScalaLibrary
不同,全域狀態會在執行之間保持。當任何函式庫使用 Java 序列化時,應避免使用 AllLibraryJars
。fullClasspath
金鑰所指定的完整類別路徑會載入到最外層。如果在 ScalaLibrary
遇到任何問題,或者如果應用程式要求所有類別都載入到同一個 ClassLoader
中(對於某些 Java 序列化的使用情況,可能是這種情況),請考慮使用此選項來代替 fork。可以在不同的組態中設定 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
,則會失敗並出現 ClassNotFoundException
,因為後續線程的內容類別載入器是 Scala 函式庫類別載入器,無法載入專案類別。要解決此限制,而無需將分層策略更改為 Flat
,可以執行以下操作:
Class.forName
而不是 ClassLoader.loadClass
。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)
}