欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用Scala开发现代应用程序:控制台应用程序

程序员文章站 2024-01-16 21:53:46
...

本文是我们名为“ 使用Scala开发现代应用程序 ”的学院课程的一部分。

在本课程中,我们提供一个框架和工具集,以便您可以开发现代的Scala应用程序。 我们涵盖了广泛的主题,从SBT构建和响应式应用程序到测试和数据库访问。 通过我们简单易懂的教程,您将能够在最短的时间内启动并运行自己的项目。 在这里查看

1.简介

毋庸置疑,Web和移动已经深入我们的生活,影响了我们的日常习惯和对事物的期望。 因此,目前正在开发的绝大多数应用程序是移动应用程序,Web API或功能完善的网站和门户。

基于控制台的经典,旧式应用程序已基本消失。 他们的生活主要是在Linux / Unix操作系统上,这是他们哲学的核心。 但是,基于控制台的应用程序在解决各种各样的问题中非常有用,决不能忘记。

在本教程的这一部分中,我们将讨论使用Scala编程语言和生态系统开发控制台应用程序(或换句话说,命令行)。 我们将要开始构建的示例应用程序只会做一件简单的事情:从提供的URL地址中获取数据。 为了使其更加有趣,该应用程序将需要提供超时,并可选地提供输出文件来存储响应的内容。

2.无UI和命令行导向

尽管有一些例外,控制台应用程序通常没有任何类型的图形UI或伪图形界面。

它们的输入或者是传递给执行应用程序的命令行参数,或者仅仅是来自另一个命令或源的管道流。 它们的输出通常在控制台中打印出来(这就是为什么这些应用程序通常被称为控制台应用程序的原因),或者更确切地说,可能会有多个输出流,例如标准输出(控制台)和错误输出。

控制台应用程序最有用的功能之一就是管道传输:可以将一个应用程序的输出作为管道输入到另一个应用程序。 如此强大的功能允许轻松表达非常复杂的处理管道,例如:

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

3.受论点驱动

在本节中,我们将介绍两个强大的Scala框架,并开发我们之前概述的示例控制台应用程序的两个版本。 第一个库scopt旨在通过分析和解释命令行参数(及其组合)来为我们提供很多帮助,因此让我们仔细研究一下。

既然我们已经知道所有要求,那么让我们从重新陈述我们希望在命令行输入方面实现的目标开始。 我们的应用程序将要求第一个参数是要获取的URL,超时也是必需的,并且应使用-t (或--timeout )参数指定,而输出文件是可选的,可以使用-o (或--out )参数。 因此,完整的命令行如下所示:

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

或类似地,在使用冗长的参数名称时(请注意,两者的组合完全合法):

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

使用出色的scopt库,此任务变得相当简单。 首先,让我们介绍一个反映命令行参数(及其语义)的简单配置类:

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

现在,我们面临的问题是如何从命令行参数获取所需的配置实例? 使用scopt ,我们从创建OptionParser实例开始,该实例描述了所有命令行选项:

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类的parse方法将其应用于命令行参数。

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类的有效实例,或者应用程序将终止,并在控制台中输出遇到的错误。 例如,如果我们不指定任何参数,则将打印以下内容:

$ 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

出于好奇,您可以尝试运行仅提供一些命令行参数或传递无效值的命令, scopt会找出此错误并进行投诉。 但是,如果一切正常,它将保持沉默,并让应用程序获取URL并在控制台中打印出响应,例如:

$ 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 )到非常复杂的(例如sbtScala REPL )。

令人惊讶的是,所有必要的构建基块都已经作为sbt工具的一部分提供了(它本身提供了非常强大的交互式外壳)。 sbt发行版提供了基础以及专用的启动器 ,可从任何地方运行您的应用程序。 按照我们为自己设置的要求,让我们使用sbt脚手架将它们体现为交互式控制台应用程序。

在核心SBT基于应用程序奠定xsbti.AppMain其人(在我们的例子中,接口ConsoleApp类)应该实现。 让我们看一下典型的实现。

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的代码中最重要的函数是initialState 我们暂时将其留空,但是不用担心,一旦我们了解了基础知识,它将很快充满代码。

状态sbt中所有可用信息的容器 采取某些行动可能需要对当前国家进行修改,然后再产生一个新国家 sbt中的此类操作的一类是命令 (尽管还有更多 )。

最好有一个专用命令来获取URL并打印出响应,这听起来是个好主意,因此让我们对其进行介绍:

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通过在sbt.complete.DefaultParsers对象中定义basicUri解析器可以重用)大大简化了我们的任务。

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

太好了,现在我们必须稍微修改一下命令实例化,以暗示sbt我们希望传递一些参数,并且本质上也提供命令实现。

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中State可能包含可以共享的其他属性 以下是定义timeout方法:

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

到此为止,我们已经涵盖了难题的最后一部分,并准备提供initialState函数的实现。

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文件中):

[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存储库中来完成此操作:

$ sbt publishLocal

然后通过sbt启动器运行:

$ 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以确保我们自己的命令在那里。

> help fetch
fetch <url>

         Fetches the <url> and prints out the response

>

如何从某些真实的URL地址获取数据?

> 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命令会解决这一问题吗? 让我们来看看 …

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

如预期的那样,它确实并Swift报告了该错误。 很好,但是我们可以执行fetch命令而不需要运行交互式shell吗? 答案是“是的,肯定!”,我们只需要将要执行的命令作为参数传递给sbt启动器,并用双引号引起来即可,例如:

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

在控制台中打印出的结果完全相同。 您可能会注意到,我们没有实现将输出写入文件的可选支持,但是对于基于控制台的应用程序,我们可以使用流重定向的简单技巧:

$ 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令人敬畏的Akka工具包的其他部分相识。

完整的项目可下载

翻译自: https://www.javacodegeeks.com/2016/09/developing-modern-applications-scala-console-applications.html