本文是我们名为“ 使用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 )到非常复杂的(例如sbt或Scala 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