Scala:如何在整个应用程序中使用Global Config案例类

时间:2019-05-20 08:08:07

标签: scala case-class

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

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

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

我编写了一个配置解析器文件,用于从配置文件解析到案例类

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

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

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

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

我想拥有一个引导功能,它将一次解析此配置,使其可在整个应用程序中使用

5 个答案:

答案 0 :(得分:4)

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

通过以下方式向您build.sbt添加Pure Config:

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

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

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

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

您可以定义一个名为AppConfig的案例类:

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

并使用loadConfig方法创建由应用程序配置填充的实例:

import pureconfig.generic.auto._

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

这将返回错误或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"
    )
)

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

在应用程序启动时(尽可能接近主函数)加载此配置一次之后,只需在构造函数中传递AppConfig即可使用依赖项注入将其传递给所有其他类。

如果您想使用MacWire与config case类连接您的Logic类(和其他服务),如Krzysztof在他的选择之一中建议的那样,您可以看到我的答案here

一个简单的示例(没有 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.scala中定义了AppConfig的地方

package com.example.config

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

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

此外,您的配置可能是根据supported types构建的,例如String,Boolean,Int等,也可能是根据受支持的类型构建的其他case类(这是因为case类表示一个值对象,其中包含数据)以及受支持类型的列表和选项。
这使您可以“分类”复杂的配置文件并完成代码。例如,在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
}

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

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]

答案 1 :(得分:3)

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

  1. 使用运行时依赖注入框架,例如 guice 。您可以使用extension for scala

  2. 使用隐式对其进行处理。您只需要创建一个对象,该对象将保留您的隐式配置:

    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. 配置类的一个字段(称为构造函数注入)。

    class Foo(config: Config).
    

    然后,您可以手动连接依赖项,例如:

    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的模式。它适用于小型应用程序,但是您的应用程序越大,这种方法就越笨拙。

什么是?一种好的方法是使用全局单例,如下所示:

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

然后像这样使用它:

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

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

我认为,如果您的应用程序不是很大,则应使用隐式。

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

答案 2 :(得分:0)

您试图实现的模式称为“依赖注入”。摘自Martin Fowler的enter image description here,关于这个主题

  

“依赖注入”的基本思想是拥有一个单独的对象,即一个汇编器,该对象将使用finder接口的适当实现填充lister类中的字段。

在诸如post之类的依赖注入工具中注册此配置实例。

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)

答案 3 :(得分:0)

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

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)
  } 
}

但是,处理带有案例类的配置的最简单方法是使用pureconfig

答案 4 :(得分:0)

  

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

只需将其放在object中,例如

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

我想拥有一个引导功能,它将一次解析此配置,使其可在整个应用程序中使用

一旦您调用MyConfig.config,它就会被加载一次-因为object Singleton 。因此,不需要特殊的引导程序。