Scala: Как использовать класс дел Global Config в приложении - PullRequest
3 голосов
/ 20 мая 2019

Я новичок в 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 ]

4 голосов
/ 20 мая 2019

Вы можете сделать это автоматически с PureConfig .

Добавить Pure Config к вам build.sbt с:

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

и перезагрузите оболочку sbt и обновите ваши зависимости.

Теперь предположим, что у вас есть следующий 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, если хотите получить AppConfig вместо Either.

После того, как вы загрузите этот конфиг один раз в начале вашего приложения (как можно ближе к вашей основной функции), вы можете использовать внедрение зависимостей, чтобы передать его всем остальным классам, просто передав AppConfig в конструктор.

Если вы хотите подключить свой класс логики (и другие сервисы) к классу конфигурации с использованием MacWire , как предложил Кшиштоф в одном из своих вариантов, вы можете увидеть мой ответ здесь .

Простой пример (без 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, вы получите завершение кода.

Кроме того, ваша конфигурация может быть построена из поддерживаемых типов , таких как 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]
3 голосов
/ 20 мая 2019

Есть несколько вариантов решения вашей проблемы:

  1. Использовать среду внедрения зависимостей во время выполнения, например guice . Вы можете использовать расширение для 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. Сделайте config полем ваших классов (это называется инжектором конструктора).

    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. Вы можете использовать шаблон под названием торт-шаблон . Он отлично работает для небольших приложений, но чем крупнее ваше приложение, тем труднее этот подход.

Что такое НЕ . Хороший подход заключается в использовании глобального синглтона, подобного этому:

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

А затем используйте его как:

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

Это плохо, потому что это очень сильно мешает проверке вашей конфигурации для тестов, и весь процесс тестирования становится неуклюжим.

По моему мнению, если ваше приложение не очень большое, вы должны использовать импликации.

Если вы хотите узнать больше по этой теме, отметьте это замечательное руководство .

0 голосов
/ 20 мая 2019

Я хочу использовать эту переменную конфигурации в приложении, а не загружать файл application.conf каждый раз.

Просто поместите его в object, например

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

Я хочу иметь одну функцию начальной загрузки, которая будет анализировать эту конфигурацию один раз, делая ее доступной для всего приложения

Как только вы вызываете MyConfig.config, она загружается только один раз - какobject - это Синглтон .Так что никакой специальной загрузки не требуется.

0 голосов
/ 20 мая 2019

Вы должны определить поля как параметры вашего класса дел.

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 .

0 голосов
/ 20 мая 2019

Паттерн, который вы пытаетесь достичь, называется инъекцией зависимости. Из сообщения Мартина Фаулера на эту тему

Основная идея внедрения зависимостей состоит в том, чтобы иметь отдельный объект, ассемблер, который заполняет поле в классе lister соответствующей реализацией для интерфейса поиска.

Зарегистрируйте этот экземпляр конфигурации в инструменте внедрения зависимостей, например 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)
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...