Статьи

Разработка современных приложений с помощью Scala: консольные приложения

Эта статья является частью нашего академического курса под названием « Разработка современных приложений с помощью Scala» .

В этом курсе мы предоставляем среду и набор инструментов, чтобы вы могли разрабатывать современные приложения Scala. Мы охватываем широкий спектр тем: от сборки SBT и реактивных приложений до тестирования и доступа к базе данных. С нашими простыми учебными пособиями вы сможете запустить и запустить собственные проекты за минимальное время. Проверьте это здесь !

1. Введение

Само собой разумеется, что Интернет и мобильные технологии очень глубоко проникли в нашу жизнь, что сильно повлияло на наши повседневные привычки и ожидания в отношении вещей. Таким образом, подавляющее большинство приложений, разрабатываемых в настоящее время, представляют собой либо мобильные приложения, либо веб-API, либо полноценные веб-сайты и порталы.

Классические, старые, консольные приложения в значительной степени исчезли. Они живут своей жизнью в основном в операционных системах Linux / Unix, будучи в основе своей философии. Тем не менее, консольные приложения чрезвычайно полезны для решения широкого спектра проблем и ни в коем случае не должны быть забыты.

В этом разделе руководства мы поговорим о разработке консольных (или, иначе говоря, командной строки) приложений с использованием языка программирования и экосистемы Scala . Пример приложения, которое мы собираемся начать, будет делать только одну простую вещь: получить данные с предоставленного URL-адреса. Чтобы сделать его немного более интересным, приложению потребуется предоставить тайм-аут и, необязательно, выходной файл для хранения содержимого ответа.

2. Без пользовательского интерфейса и командной строки

Хотя есть некоторые исключения, консольные приложения обычно не имеют какого-либо графического интерфейса или псевдографического интерфейса.

Их ввод — это либо аргументы командной строки, переданные для выполнения приложения, либо просто поток по каналу из другой команды или источника. Их выходные данные обычно распечатываются в консоли (именно поэтому эти приложения часто называют консольными приложениями), или, если быть более точным, может быть несколько потоков вывода, например, стандартный вывод (консоль) и вывод ошибок.

Одной из наиболее полезных возможностей консольных приложений является конвейеризация: выходные данные одного приложения могут передаваться в виде входных данных другому приложению. Такой чрезвычайно мощный состав позволяет легко выразить очень сложные технологические трубопроводы, например:

1
ps -ef | grep bash | awk '{ print "PID="$2; }'

3. Управляемый аргументами

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

Поскольку мы уже знаем все требования, давайте начнем с повторения того, что мы хотели бы достичь с точки зрения ввода в командной строке. Наше приложение потребовало бы, чтобы первый аргумент был URL-адресом для извлечения, время ожидания также требуется и должно быть указано с помощью аргумента -t (или альтернативно --timeout ), в то время как выходной файл является необязательным и может быть предоставлен с использованием -o (или альтернативно --out аргумент. Итак, полная командная строка выглядит так:

1
java –jar console-cli.jar <url> -t <timeout> [-o <file>]

Или вот так, при использовании подробных имен аргументов (обратите внимание, что комбинация обоих полностью допустима):

1
java –jar console-cli.jar <url> --timeout <timeout> [--out <file>]

Используя потрясающую библиотеку scopt, эта задача становится довольно тривиальной. Во-первых, давайте введем простой класс конфигурации, который отражает аргументы командной строки (и их семантику):

1
case class Configuration(url: String = "", output: Option[File] = None, timeout: Int = 0)

Теперь проблема, с которой мы сталкиваемся, состоит в том, как перейти от аргументов командной строки к экземпляру конфигурации, в которой мы нуждаемся? С scopt мы начинаем с создания экземпляра OptionParser, где описаны все наши параметры командной строки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
val parser = new OptionParser[Configuration]("java -jar console-cli.jar") {
  override def showUsageOnError = true
 
  arg[String]("")
    .required()
    .validate { url => Right(new URL(url)) }
    .action { (url, config) => config.copy(url = url) }
    .text("URL to fetch")
     
  opt[File]('o', "out")
    .optional()
    .valueName("")
    .action((file, config) => config.copy(output = Some(file)))
    .text("optionally, the file to store the output (printed in console by default)")
       
  opt[Int]('t', "timeout")
    .required()
    .valueName("")
    .validate { _ match {
        case t if t > 0 => Right(Unit)
        case _ => Left("timeout should be positive")
      }
    }
    .action((timeout, config) => config.copy(timeout = timeout))
    .text("timeout (in seconds) for waiting HTTP response")
       
  help("help").text("prints the usage")
}

Давайте пройдемся по этому определению парсера и сопоставим каждый фрагмент кода с соответствующим аргументом командной строки. Первая запись, arg[String]("<url>") , описывает параметр <url> , у нее нет имени и она идет сразу после имени приложения. Обратите внимание, что это требуется и должно представлять действительный URL-адрес в соответствии с логикой проверки.

Вторая запись, opt[File]('o', "out") , используется для указания файла для хранения ответа. Он имеет короткие ( o ) и длинные ( out ) варианты и помечен как необязательный (поэтому его можно опустить). Аналогичным образом, opt[Int]('t', "timeout") позволяет указать время ожидания и является обязательным аргументом, причем оно должно быть больше нуля. И последнее, но не менее важное: специальная запись help("help") выводит подробности о параметрах командной строки и аргументах.

Получив определение синтаксического анализатора, мы можем применить его к аргументам командной строки, используя метод синтаксического анализа класса OptionParser .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
parser.parse(args, Configuration()) match {
  case Some(config) => {
    val result = Await.result(Http(url(config.url) OK as.String),
      config.timeout seconds)
         
    config.output match {
      case Some(f) => new PrintWriter(f) {
        write(result)
        close
      }
      case None => println(result)
    }
       
    Http.shutdown()
  
  case _ => /* Do nothing, just terminate the application */
}

Результатом синтаксического анализа является либо допустимый экземпляр класса Configuration либо приложение завершает работу, выводя обнаруженные ошибки в консоль. Например, если мы не указываем никаких аргументов, вот что будет напечатано:

01
02
03
04
05
06
07
08
09
10
$ java -jar console-cli-assembly-0.0.1-SNAPSHOT.jar
 
Error: Missing option --timeout
Error: Missing argument
Usage: console-cli [options]
 
  <url>                     URL to fetch
  -o, --out <file>          optionally, the file to store the output (printed on the console by default)
  -t, --timeout <seconds>   timeout (in seconds) for waiting HTTP response
  --help                    prints the usage

Из любопытства вы можете попробовать запустить эту команду, предоставив только некоторые аргументы командной строки или передав недопустимые значения, Скопт выяснит это и будет жаловаться. Тем не менее, он будет хранить молчание, если все в порядке, и позволить приложению получить URL-адрес и распечатать ответ в консоли, например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
$ java -jar console-cli-assembly-0.0.1-SNAPSHOT.jar http://freegeoip.net/json/www.google.com -t 1
 
{
    "ip":"216.58.219.196",
    "country_code":"US",
    "country_name":"United States",
    "region_code":"CA",
    "region_name":"California",
    "city":"Mountain View",
    "zip_code":"94043",
    "time_zone":"America/Los_Angeles",
    "latitude":37.4192,"longitude":-122.0574,
    "metro_code":807
}

Круто, не правда ли? Хотя мы работали только с базовыми вариантами использования, стоит отметить, что scopt способен поддерживать довольно сложные комбинации параметров и аргументов командной строки, сохраняя при этом определения синтаксического анализатора удобочитаемыми и поддерживаемыми.

4. Сила интерактивности

Другой класс консольных приложений — это те, которые предлагают интерактивную командно-управляемую оболочку, которая может варьироваться от несколько тривиальных (например, ftp ) до довольно сложных (например, sbt или Scala REPL ).

Удивительно, но все необходимые строительные блоки уже доступны как часть инструмента sbt (который сам по себе предлагает очень мощную интерактивную оболочку). Дистрибутив sbt предоставляет основу, а также специальный лаунчер для запуска ваших приложений из любой точки мира. Следуя требованиям, которые мы установили для себя, давайте воплотим их в виде интерактивных консольных приложений, используя скаффолдинг sbt .

В основе sbt- приложений лежит интерфейс xsbti.AppMain, который должны реализовывать другие (в нашем примере, класс ConsoleApp ). Давайте посмотрим на типичную реализацию.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
class ConsoleApp extends AppMain {
  def run(configuration: AppConfiguration) =
    MainLoop.runLogged(initialState(configuration))
   
  val logFile = File.createTempFile("console-interactive", "log")
  val console = ConsoleOut.systemOut
 
  def initialState(configuration: AppConfiguration): State = {
    ...
  }
   
  def globalLogging: GlobalLogging =
    GlobalLogging.initial(MainLogging.globalDefault(console), logFile, console)
 
  class Exit(val code: Int) extends xsbti.Exit
}

Наиболее важной функцией в приведенном выше коде является initialState . Мы оставили это поле пустым, но не волнуйтесь, как только мы поймем основы, он будет довольно быстро наполнен кодом.

State — это контейнер всей доступной информации в sbt . Выполнение некоторых действий может потребовать внесения изменений в текущее состояние , после чего создается новое состояние . Одним из классов таких действий в sbt является команда (хотя их больше ).

Звучит как хорошая идея иметь специальную команду, которая выбирает URL и распечатывает ответ, поэтому давайте представим его:

1
2
3
4
5
6
7
8
9
val FetchCommand = "fetch"
val FetchCommandHelp = s"""$FetchCommand
 
       Fetches the  and prints out the response
"""
   
val fetch = Command(FetchCommand, Help.more(FetchCommand, FetchCommandHelp)) {
    ...
}

Выглядит просто, но нам нужно как-то указать URL для получения. К счастью, команды в sbt могут иметь собственные аргументы, но команда должна сказать, какова ее аргументация, путем определения экземпляра класса Parser . Исходя из этого, sbt позаботится о том, чтобы передать входные данные парсеру и либо извлечь действительные аргументы, либо завершиться с ошибкой. В случае нашей команды fetch нам нужно предоставить парсер для URL (однако sbt значительно упрощает нашу задачу, определяя парсер basicUri в объекте sbt.complete.DefaultParsers, который мы можем использовать повторно).

1
lazy val url = (token(Space) ~> token(basicUri, "")) <~ SpaceClass.*

Отлично, теперь нам нужно немного изменить экземпляр нашей команды, чтобы намекнуть sbt, что мы ожидаем, что будут переданы некоторые аргументы, и, по сути, обеспечить реализацию команды.

1
2
3
4
5
6
7
val fetch = Command(FetchCommand, Help.more(FetchCommand, FetchCommandHelp))
  (_ => mapOrFail(url)(_.toURL()) !!! "URL is not valid") { (state, url) =>
    val result = Await.result(Http(dispatch.url(url.toString()) OK as.String),
      state get timeout getOrElse 5 seconds)
    state.log.info(s"${result}")
    state
  }

Отлично, мы только что определили нашу собственную команду! Однако внимательный читатель может заметить наличие переменной timeout в приведенном выше коде. Состояние в sbt может содержать дополнительные атрибуты, которыми можно поделиться. Вот как определяется время timeout :

1
2
val timeout = AttributeKey[Int]("timeout",
  "The timeout (in seconds) to wait for HTTP response")

После этого мы рассмотрели последнюю часть головоломки и готовы предоставить реализацию функции initialState .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
def initialState(configuration: AppConfiguration): State = {
  val commandDefinitions = fetch +: BasicCommands.allBasicCommands
  val commandsToRun = "iflast shell" +: configuration.arguments.map(_.trim)
      
  State(
    configuration,
    commandDefinitions,
    Set.empty,
    None,
    commandsToRun,
    State.newHistory,
    AttributeMap(AttributeEntry(timeout, 1)),
    globalLogging,
    State.Continue
  )
}

Обратите внимание, как мы включили нашу команду fetch в начальное состояние ( fetch +: BasicCommands.allBasicCommands ) и указали значение времени ожидания по умолчанию, равное 1 секунде ( AttributeEntry(timeout, 1) ).

Последняя тема, которая нуждается в пояснениях: как запустить наше приложение для интерактивной консоли? Для этого sbt предоставляет лаунчер . В его минимальной форме это всего лишь один файл sbt-launch.jar который необходимо загрузить и использовать для запуска приложений путем разрешения их с помощью управления зависимостями Apache Ivy . Каждое приложение отвечает за предоставление своей конфигурации запуска, которая для нашего простого примера может выглядеть следующим образом (хранится в файле console.boot.properties ):

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
[app]
  org: com.javacodegeeks
  name: console-interactive
  version: 0.0.1-SNAPSHOT
  class: com.javacodegeeks.console.ConsoleApp
  components: xsbti
  cross-versioned: binary
 
[scala]
  version: 2.11.8
  
[boot]
  directory: ${sbt.boot.directory-${sbt.global.base-${user.home}/.sbt}/boot/}
 
[log]
  level: info
 
[repositories]
  local
  maven-central
  typesafe-ivy-releases: http://repo.typesafe.com/typesafe/ivy-releases
  typesafe-releases: http://repo.typesafe.com/typesafe/releases

Ничто не мешает нам запустить пример приложения, поэтому давайте сделаем это, сначала опубликовав его в локальном репозитории Apache Ivy :

1
$ sbt publishLocal

И работает через sbt launcher сразу после:

1
2
3
4
5
6
$ java -Dsbt.boot.properties=console.boot.properties -jar sbt-launch.jar
Getting com.javacodegeeks console-interactive_2.11 0.0.1-SNAPSHOT ...
:: retrieving :: org.scala-sbt#boot-app
        confs: [default]
        22 artifacts copied, 0 already retrieved (8605kB/333ms)
>

Круто, мы сейчас в интерактивной оболочке нашего приложения! Давайте напечатаем help fetch чтобы убедиться, что наша собственная команда есть.

1
2
3
4
5
6
> help fetch
fetch <url>
 
         Fetches the <url> and prints out the response
 
>

Как насчет получения данных с некоторых реальных URL-адресов?

1
2
3
> fetch http://freegeoip.net/json/www.google.com
[info] {"ip":"216.58.219.196","country_code":"US","country_name":"United States","region_code":"CA","region_name":"California","city":"Mountain View","zip_code":"94043","time_zone":"America/Los_Angeles","latitude":37.4192,"longitude":-122.0574,"metro_code":807}
>

Работает отлично! Но что если мы сделаем опечатку и наш URL-адрес недействителен? Наша команда fetch это? Покажи нам …

1
2
3
4
5
> fetch htp://freegeoip.net/json/www.google.com
[error] URL is not valid
[error] fetch htp://freegeoip.net/json/www.google.com
[error]                                              ^
>

Как и ожидалось, он сделал и своевременно сообщил об ошибке. Хорошо, но можем ли мы выполнить команду fetch без необходимости запуска интерактивной оболочки? Ответ «Да, конечно!», Нам просто нужно передать команду для выполнения в качестве аргумента sbt launcher, заключенную в двойные кавычки, например:

1
2
3
$ java -Dsbt.boot.properties=console.boot.properties -jar sbt-launch.jar "fetch http://freegeoip.net/json/www.google.com"
 
[info] {"ip":"172.217.4.68","country_code":"US","country_name":"United States","region_code":"CA","region_name":"California","city":"Mountain View","zip_code":"94043","time_zone":"America/Los_Angeles","latitude":37.4192,"longitude":-122.0574,"metro_code":807}

Результаты, напечатанные в консоли, точно такие же. Вы можете заметить, что мы не реализовали дополнительную поддержку записи вывода в файл, однако для консольных приложений мы можем использовать простой прием с перенаправлением потока:

1
$ java -Dsbt.boot.properties=console.boot.properties -jar sbt-launch.jar "fetch http://freegeoip.net/json/www.google.com" > response.json

Несмотря на то, что наше простое интерактивное приложение полностью функционально, оно использует лишь небольшую часть мощности sbt . Пожалуйста, не стесняйтесь изучать раздел документации этого замечательного инструмента, связанного с созданием приложений командной строки.

5. Выводы

В этом разделе руководства мы говорили о создании консольных приложений с использованием потрясающего языка программирования Scala и библиотек. Ценность и полезность простых старых приложений командной строки, безусловно, сильно недооцениваются, и, надеюсь, этот раздел подтверждает это. Либо вы разрабатываете простой инструмент, управляемый из командной строки, либо интерактивный инструмент — экосистема Scala готова предложить вам полную поддержку.

6. Что дальше

В следующем разделе мы много поговорим о параллелизме и параллелизме, точнее обсудим идеи и концепции модели Actor и продолжим наше знакомство с другими частями замечательного инструментария Akka .

Полные проекты доступны для скачивания .