Scala:如何跨应用程序使用全局配置案例 class

Scala : How to use Global Config case class across application

我是 scala 的新手,刚开始使用我的 scala 第一个应用程序。

我已经在资源文件夹下定义了我的配置文件,application.conf

  projectname{
     "application" {
     "host":127.0.0.1
     "port":8080
    }
 }

我写了一个配置解析器文件来从配置文件解析到案例class

    case class AppConfig (config: Config) {
      val host = config.getString("projectname.application.host")
      val port = config.getInt("projectname.application.port")
    }

在我的 grpc 服务器文件中,我已将配置声明为

    val config = AppConfig(ConfigFactory.load("application.conf"))

我想跨应用程序使用这个配置变量,而不是每次都加载 application.conf 文件。

我想要一个 bootstrap 函数来一次性解析此配置,使其在整个应用程序中可用

您可以使用 PureConfig 自动执行此操作。

为您添加 Pure Config build.sbt

libraryDependencies += "com.github.pureconfig" %% "pureconfig" % "0.11.0"

并重新加载 sbt shell 并更新您的依赖项。

现在,假设您有以下 resource.conf 文件:

host: example.com
port: 80
user: admin
password: admin_password

您可以定义一个名为 AppConfig:

的案例 class
case class AppConfig(
    host: String,
    port: Int,
    user: String,
    password: String
)

并使用 loadConfig 方法创建一个实例,填充应用程序配置:

import pureconfig.generic.auto._

val errorsOrConfig: Either[ConfigReaderFailures, AppConfig] = pureconfig.loadConfig[AppConfig]

这 returns 要么是错误,要么是您的 AppConfig,具体取决于配置本身中的值。
例如,如果上面 port 的值将是 eighty,而不是 80,您将得到一个详细的错误,说第二个配置行(带有 port: eighty)包含一个字符串,但唯一有效的预期类型是数字:

ConfigReaderFailures(
    ConvertFailure(
        reason = WrongType(
        foundType = STRING,
        expectedTypes = Set(NUMBER)
    ),
    location = Some(
        ConfigValueLocation(
           new URL("file:~/repos/example-project/target/scala-2.12/classes/application.conf"),
           lineNumber = 2
        )
    ),
    path = "port"
    )
)

如果您想获得 AppConfig 而不是 Either,可以使用 loadConfigOrThrow

在您的应用程序启动时(尽可能靠近您的主函数)加载一次此配置后,您可以使用依赖注入将其传递给所有其他 classes,只需通过在构造函数中传递 AppConfig。

如果您想使用 MacWire, as Krzysztof suggested in one of his options, you can see my answer here.

将您的逻辑 class(和其他服务)与配置案例 class 连接起来

普通示例(没有 MacWire),如下所示:

package com.example

import com.example.config.AppConfig

object HelloWorld extends App {
 val config: AppConfig = pureconfig.loadConfigOrThrow[AppConfig]
 val logic = new Logic(config)
}

class Logic(config: AppConfig) {
   // do something with config
}

AppConfig 定义在 AppConfig.scala

package com.example.config

case class AppConfig(
    host: String,
    port: Int,
    user: String,
    password: String
)

作为奖励,当您在 IDE 中使用此配置变量时,您将获得代码补全。

此外,您的配置可能是根据 supported types 构建的,例如 String、Boolean、Int 等,但也可能是根据支持的类型构建的其他 classes(这是因为 case class 表示一个包含数据的值对象),以及支持类型的列表和选项。
这允许您 "class up" 一个复杂的配置文件并获得代码完成。例如,在 application.conf:

name: hotels_best_dishes
host: "https://example.com"
port: 80
hotels: [
  "Club Hotel Lutraky Greece",
  "Four Seasons",
  "Ritz",
  "Waldorf Astoria"
]
min-duration: 2 days
currency-by-location {
  us = usd
  england = gbp
  il = nis
}
accepted-currency: [usd, gbp, nis]
application-id: 00112233-4455-6677-8899-aabbccddeeff
ssh-directory: /home/whoever/.ssh
developer: {
  name: alice,
  age: 20
}

然后在您的代码中定义一个配置案例class:

import java.net.URL
import java.util.UUID
import scala.concurrent.duration.FiniteDuration
import pureconfig.generic.EnumCoproductHint
import pureconfig.generic.auto._

case class Person(name: String, age: Int)

sealed trait Currency
case object Usd extends Currency
case object Gbp extends Currency
case object Nis extends Currency

object Currency {
  implicit val currencyHint: EnumCoproductHint[Currency] = new EnumCoproductHint[Currency]
}

case class Config(
  name: String,
  host: URL,
  port: Int,
  hotels: List[String],
  minDuration: FiniteDuration,
  currencyByLocation: Map[String, Currency],
  acceptedCurrency: List[Currency],
  applicationId: UUID,
  sshDirectory: java.nio.file.Path,
  developer: Person
)

并加载它:

val config: Config = pureconfig.loadConfigOrThrow[Config]

您尝试实现的模式称为依赖注入。来自 Martin Fowler 关于此主题的 post

The basic idea of the Dependency Injection is to have a separate object, an assembler, that populates a field in the lister class with an appropriate implementation for the finder interface.

在依赖注入工具中注册此配置实例,例如 Guice

class AppModule(conf: AppConfiguration) extends AbstractModule {
  override def configure(): Unit = {
    bind(classOf[AppConfiguration]).toInstance(conf)
  }
}

....

// somewhere in the code
import com.google.inject.Inject

class FooClass @Inject() (config: AppConfiguration)

您应该将字段定义为案例的参数 class。

final case class AppConfig(host: String, port: Int)

然后重载伴生对象的 apply 方法

object AppConfig {
  def apply(config: Config): AppConfig = {
    val host = config.getString("projectname.application.host")
    val port = config.getInt("projectname.application.port")
    AppConfig(host, port)
  } 
}

然而,使用 case classes 处理配置的最简单方法是使用 pureconfig.

I want to use this config variable across application, rather than loading application.conf file everytime.

就放在object里就好了,比如

object MyConfig {
  lazy val config = AppConfig(ConfigFactory.load("application.conf"))
}

I want to have one bootstrap function which will parse this config one time, making it available across application

一旦调用 MyConfig.config,它只会加载一次 - 因为 object 是一个 Singleton。所以不需要特殊的bootstrap。

有一些可能性可以解决您的问题:

  1. 使用像 guice 这样的运行时依赖注入框架。您可以使用 extension for scala.

  2. 使用implicits来处理。您只需要创建一个对象,它将保存您的隐式配置:

    object Implicits {
       implicit val config = AppConfig(ConfigFactory.load("application.conf"))
    }
    

    然后您可以在需要时将 implicit config: Config 添加到您的参数列表中:

    def process(n: Int)(implicit val config: Config) = ??? //as method parameter
    
    case class Processor(n: Int)(implicit val config: AppConfig) //or as class field
    

    并像这样使用它:

    import Implicits._
    
    process(5) //config passed implicitly here
    
    Processor(10) //and here
    

    它的一大优点是您可以手动通过 config 进行测试:

    process(5)(config)
    

    这种方法的缺点是,在你的应用程序中有很多隐式解析,会使编译变慢,但如果你的应用程序不庞大,这应该不是问题。

  3. 将 config 设置为您的 类 字段(称为构造函数注入)。

    class Foo(config: Config).
    

    然后您可以 wire-up 手动添加您的依赖项,例如:

    val config: AppConfig = AppConfig()
    
    val foo = Foo(config) //you need to pass config manually to constructors in your object graph
    

    或者你可以使用一个可以为你自动化的框架,比如 macwire:

    val config = wire[AppConfig]
    val foo = wire[Foo]
    
  4. 您可以使用名为 cake-pattern 的模式。它适用于 small-sized 应用程序,但您的应用程序越大,这种方法就越笨拙。

什么是 NOT 一个好的方法是像这样使用全局单例:

object ConfigHolder {
    val Config: AppConfig = ???
}

然后像这样使用它:

def process(n: Int) = {
    val host = ConfigHolder.Config.host // anti-pattern
}

这很糟糕,因为它使模拟您的测试配置变得非常困难,并且整个测试过程变得笨拙。

在我看来,如果你的应用不是很大,你应该使用 implicits。

如果您想了解有关此主题的更多信息,请查看 this great guide