从Typesafe Config实例化Case Class

11

假设我有一个Scala的case class,它具备将其序列化为JSON的能力(使用json4s或其他库):

case class Weather(zip : String, temp : Double, isRaining : Boolean)

如果我正在使用一个HOCON配置文件:

allWeather {

   BeverlyHills {
    zip : 90210
    temp : 75.0
    isRaining : false
  }

  Cambridge {
    zip : 10013
    temp : 32.0
    isRainging : true
  }

}

有没有办法使用 typesafe config 自动实例化一个Weather对象?

我在寻找以下形式的内容:

val config : Config = ConfigFactory.parseFile(new java.io.File("weather.conf"))

val bevHills : Weather = config.getObject("allWeather.BeverlyHills").as[Weather]

这个解决方案可以利用被引用的值 "allWeather.BeverlyHills" 是一个 JSON "blob" 的事实。

我显然可以编写自己的解析器:

def configToWeather(config : Config) = 
  Weather(config.getString("zip"), 
          config.getDouble("temp"), 
          config.getBoolean("isRaining"))

val bevHills = configToWeather(config.getConfig("allWeather.BeverlyHills"))

但这似乎不够优雅,因为对天气定义的任何更改都需要更改 configToWeather

提前感谢您的审阅和回复。

6个回答

17

typesafe配置库有一个API来实例化从使用Java bean约定的配置中的对象。但我理解case class不遵循这些规则。

几个Scala库包装了typesafe配置,并提供了您所寻找的Scala特定功能。

例如,使用pureconfig读取配置可能如下所示

val weather:Try[Weather] = loadConfig[Weather]

其中Weather是一个用于配置值的case class。


4
对于纯配置(pureconfig)我要点赞。在熟悉了Spring Boot的优秀配置管理后,我发现纯配置是在Scala中最接近它的东西。 - Abhijit Sarkar
我知道 case class 不遵循那些规则。但是你可以使用 Scala 注解来允许它:https://www.scala-lang.org/api/current/scala/beans/BeanProperty.html - angelcervera
从文档中并不清楚,它可能经常用于创建ConfigObjectSource并将其传递给解析器函数以作为案例类加载;这样,单元测试等可以提供获取此对象的不同方式(从文件、URL、字符串或经典的堆栈application.conf/reference.conf/systemProperties方法)。 - soMuchToLearnAndShare

9

在Nazarii的回答基础上,以下内容对我有效:

import scala.beans.BeanProperty

//The @BeanProperty and var are both necessary
case class Weather(@BeanProperty var zip : String,
                   @BeanProperty var temp : Double,
                   @BeanProperty var isRaining : Boolean) {

  //needed by configfactory to conform to java bean standard
  def this() = this("", 0.0, false)
}

import com.typesafe.config.ConfigFactory

val config = ConfigFactory.parseFile(new java.io.File("allWeather.conf"))

import com.typesafe.config.ConfigBeanFactory

val bevHills = 
  ConfigBeanFactory.create(config.getConfig("allWeather.BeverlyHills"), classOf[Weather])

跟进:根据下面的评论,可能只有Java Collections而不是Scala Collections可以作为案例类参数的可行选项(例如,Seq[T]将无法使用)。


3
请注意,为使此功能正常工作,需要三个无参构造函数、@BeanPropertyvar。如果忘记使用var,则会悄悄地得到一个空值(在构造函数中分配的值),而不是配置的值。 - Stefan L
显然,如果成员变量之一是 Seq[T],则此方法无法正常工作。 - Niko
1
回复上面的评论:你应该只使用Java集合。 - Niko

2

受playframework的启发,这是一种没有外部库的简单解决方案:Configuration.scala

trait ConfigLoader[A] { self =>
  def load(config: Config, path: String = ""): A
  def map[B](f: A => B): ConfigLoader[B] = (config, path) => f(self.load(config, path))
}
object ConfigLoader {
  def apply[A](f: Config => String => A): ConfigLoader[A] = f(_)(_)
  implicit val stringLoader: ConfigLoader[String] = ConfigLoader(_.getString)
  implicit val booleanLoader: ConfigLoader[Boolean] = ConfigLoader(_.getBoolean)
  implicit val doubleLoader: ConfigLoader[Double] = ConfigLoader(_.getDouble)
}
object Implicits {
  implicit class ConfigOps(private val config: Config) extends AnyVal {
    def apply[A](path: String)(implicit loader: ConfigLoader[A]): A = loader.load(config, path)
  }
  implicit def configLoader[A](f: Config => A): ConfigLoader[A] = ConfigLoader(_.getConfig).map(f)
}

使用方法:

import Implicits._

case class Weather(zip: String, temp: Double, isRaining: Boolean)
object Weather {
  implicit val loader: ConfigLoader[Weather] = (c: Config) => Weather(
    c("zip"), c("temp"), c("isRaining")
  )
}

val config: Config = ???
val bevHills: Weather = config("allWeather.BeverlyHills")

在Scastie中运行代码


1
另一个经过验证的解决方案是使用com.fasterxml.jackson.databind.ObjectMapper。您不需要为任何情况类参数打标签@BeanProperty,但您必须定义一个无参构造函数。
case class Weather(zip : String, temp : Double, isRaining : Boolean) {
  def this() = this(null, 0, false)
}

val mapper = new ObjectMapper().registerModule(DefaultScalaModule)
val bevHills = mapper.convertValue(config.getObject("allWeather.BeverlyHills").unwrapped, classOf[Weather])

对我来说,这是最易读和易维护的解决方案。 - undefined

0

另一个选项是使用以下代码和circe.config。请参见https://github.com/circe/circe-config

import io.circe.generic.auto._
import io.circe.config.syntax._

def configToWeather(conf: Config): Weather = {
  conf.as[Weather]("allWeather.BeverlyHills") match {
    case Right(c) => c
    case _ => throw new Exception("invalid configuration")
  }
}

我们在项目中使用过这个,它运行得非常好。 - soMuchToLearnAndShare

-1
使用配置加载器
implicit val configLoader: ConfigLoader[Weather] = (rootConfig: Config, path: String) => {

  val config = rootConfig.getConfig(path)

  Weather(
    config.getString("zip"),
    config.getDouble("temp"),
    config.getBoolean("isRaining")
  )
}

非常感谢您提供的答案。然而,在问题中,我明确表示了要避免这种类型的解决方案。 - Ramón J Romero y Vigil

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接