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

本文是我们名为“ 使用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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值