40、Scala Web开发全解析:从邮件通知到Web服务搭建

Scala Web开发全解析:从邮件通知到Web服务搭建

1. 邮件通知程序示例

运行某程序会产生如下输出:

Account: imap.yahoo.com, USERNAME, PASSWORD
  Users: barney,betty,wilma
Account: imap.gmail.com, USER, PASS
  Users: pebbles,bam-bam

在某个应用(如SARAH)中,使用类似代码来在收到感兴趣用户列表中的邮件时进行通知。SARAH会定期扫描收件箱,当发现来自该列表中的邮件时,会语音提示“你有来自Barney和Betty的新邮件”。

示例从存储在名为 jsonString 的字符串中的示例JSON开始。通过 parse 函数将该字符串转换为名为 json JValue 对象。然后使用 \\ 方法在 json 对象中搜索所有名为 emailAccount 的元素,这种语法与Scala的XML库中类似XPath的方法一致。

通过 for 循环遍历找到的元素,将每个元素提取为 EmailAccount 对象,并打印该对象中的数据。 EmailAccount 类有一个 usersOfInterest 字段,定义为 List[String] ,Lift - JSON库可以轻松转换该序列,无需额外编码。

2. 使用Scalatra创建Web服务

2.1 问题描述

希望使用Scalatra(一个轻量级的Scala Web框架,类似于Ruby的Sinatra库)构建新的Web服务。

2.2 解决方案

推荐使用Giter8工具为新的Scalatra项目创建SBT目录结构。假设已安装Giter8,使用以下命令创建一个基于Scalatra模板的新项目:

$ g8 scalatra/scalatra-sbt
organization [com.example]: com.alvinalexander
package [com.example.app]: com.alvinalexander.app
name [My Scalatra Web App]:
scalatra_version [2.2.0]:
servlet_name [MyScalatraServlet]:
scala_version [2.10.0]:
version [0.1.0-SNAPSHOT]:
Template applied in ./my-scalatra-web-app

Giter8完成后,进入新创建的目录:

$ cd my-scalatra-web-app

在该目录中启动SBT,并使用 container:start 命令启动Jetty服务器:

$ sbt
> container:start
// 大量输出...
[info] Started SelectChannelConnector@0.0.0.0:8080
[success] Total time: 11 s, completed May 13, 2013 4:32:08 PM

使用以下命令启用连续编译:

> ~ ;copy-resources;aux-compile
1. Waiting for source changes... (press enter to interrupt)

此命令会在源代码更改时自动重新编译。Jetty服务器默认在端口8080上启动。在浏览器中访问 http://localhost:8080/ ,应看到默认的“Hello, world”输出,表明Scalatra正在运行。该URL显示的内容来自位于项目 src/main/scala/com/alvinalexander/app 目录下的 MyScalatraServlet 类:

package com.alvinalexander.app
import org.scalatra._
import scalate.ScalateSupport
class MyScalatraServlet extends MyScalatraWebAppStack {
  get("/") {
    <html>
      <body>
        <h1>Hello, world!</h1>
        Say <a href="hello-scalate">hello to Scalate</a>.
      </body>
    </html>
  }
}

get 方法表明它在监听 / URI的GET请求。如果尝试访问 http://localhost:8080/foo ,浏览器会显示如下输出:

Requesting "GET /foo" on servlet "" but only have:
GET /

这是因为 MyScalatraServlet 只有一个方法,且只监听 / URI的GET请求。

2.3 添加新服务

为演示添加新Web服务的过程,添加一个监听 /hello URI的GET请求的新方法。只需在 servlet 中添加以下方法:

get("/hello") {
  <p>Hello, world!</p>
}

保存对 MyScalatraServlet 的更改后,SBT控制台会显示一些输出,简略输出如下:

[info] Compiling 1 Scala source to target/scala-2.10/classes...
[success] Total time: 8 s
[info] Generating target/scala-2.10/resource_managed/main/rebel.xml.
[info] Compiling Templates in Template Directory:
  src/main/webapp/WEB-INF/templates
[success] Total time: 1 s, completed May 28, 2013 1:56:36 PM
2. Waiting for source changes... (press enter to interrupt)

由于 ~ aux-compile 命令,SBT会自动重新编译源代码。编译完成后,在浏览器中访问 http://localhost:8080/hello ,即可看到新的输出。

2.4 项目重要文件

除了 MyScalatraServlet 类,项目中还有一些重要文件:
| 文件路径 | 说明 |
| — | — |
| project/build.scala | 包含Scalatra的依赖信息 |
| project/plugins.sbt | 插件配置文件 |
| src/main/resources/logback.xml | 日志配置文件 |
| src/main/scala/com/alvinalexander/app/MyScalatraServlet.scala | 主Servlet类 |
| src/main/scala/com/alvinalexander/app/MyScalatraWebAppStack.scala | 相关栈类 |
| src/main/scala/ScalatraBootstrap.scala | 启动配置类 |
| src/main/webapp/WEB-INF/web.xml | Web配置文件 |
| src/main/webapp/WEB-INF/templates/layouts/default.jade | 模板布局文件 |
| src/main/webapp/WEB-INF/templates/views/hello-scalate.jade | 视图模板文件 |
| src/test/scala/com/alvinalexander/app/MyScalatraServletSpec.scala | 测试文件 |

其中, WEB-INF/web.xml 文件内容如下:

<listener>
  <listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
</listener>

通常很少需要编辑该文件。

2.5 流程图

graph TD;
    A[使用g8命令创建项目] --> B[进入项目目录];
    B --> C[启动SBT并启动Jetty服务器];
    C --> D[启用连续编译];
    D --> E[访问默认URL测试];
    E --> F[添加新服务方法];
    F --> G[保存更改,SBT自动编译];
    G --> H[访问新服务URL测试];

3. 用Scalatra替换XML Servlet映射

3.1 问题描述

想要向Scalatra应用程序中添加新的servlet,并定义它们的URI命名空间。

3.2 解决方案

Scalatra提供了一种避免在 web.xml 文件中声明servlet和servlet映射的方法。在 src/main/webapp/WEB-INF 目录下创建一个样板 web.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0">
    <listener>
      <listener-class>org.scalatra.servlet.ScalatraListener</listener-class>
    </listener>
    <servlet-mapping>
      <servlet-name>default</servlet-name>
      <url-pattern>/img/*</url-pattern>
      <url-pattern>/css/*</url-pattern>
      <url-pattern>/js/*</url-pattern>
      <url-pattern>/assets/*</url-pattern>
    </servlet-mapping>
</web-app>

编辑 src/main/scala/ScalatraBootstrap.scala 文件,内容如下:

import org.scalatra._
import javax.servlet.ServletContext
import com.alvinalexander.app._
class ScalatraBootstrap extends LifeCycle {
  override def init(context: ServletContext) {
    // 默认创建
    context.mount(new MyScalatraServlet, "/*")
    // 新增
    context.mount(new StockServlet, "/stocks/*")
    context.mount(new BondServlet, "/bonds/*")
  }
}

这两行新的 context.mount 代码告诉Scalatra, StockServlet 类应处理所有以 /stocks/ 开头的URI请求, BondServlet 类应处理所有以 /bonds/ 开头的URI请求。

src/main/scala/com/alvinalexander/app/OtherServlets.scala 文件中定义 StockServlet BondServlet 类:

package com.alvinalexander.app
import org.scalatra._
import scalate.ScalateSupport
class StockServlet extends MyScalatraWebAppStack {
  get("/") {
    <p>Hello from StockServlet</p>
  }
}
class BondServlet extends MyScalatraWebAppStack {
  get("/") {
    <p>Hello from BondServlet</p>
  }
}

假设项目仍配置为自动重新编译,访问 http://localhost:8080/stocks/ http://localhost:8080/bonds/ URL时,应看到新servlet的内容。

3.3 讨论

Scalatra将此配置过程称为“挂载”servlet,类似于使用NFS等文件系统技术挂载远程文件系统的过程。配置后, StockServlet BondServlet 中的新方法将在 /stocks/ /bonds/ URIs下可用。例如,在 StockServlet 中定义一个新方法:

get("/foo") {
  <p>Foo!</p>
}

可以通过 /stocks/foo URI(如 http://localhost:8080/stocks/foo )访问该方法。这种方法提供了与servlet映射相同的功能,但更简洁,且在Scala代码中工作,而不是在XML中,初始配置后通常可以忽略 web.xml 文件。

4. 访问Scalatra Web服务的GET参数

4.1 问题描述

创建Scalatra Web服务时,希望能够处理通过GET请求传递给方法的参数。

4.2 解决方案

4.2.1 传统参数传递方式

如果希望通过使用传统的 ? & 字符分隔数据元素的URI传递参数,如 http://localhost:8080/saveName?fname=Alvin&lname=Alexander ,可以在 get 方法中通过隐式的 params 变量访问它们:

/**
* The URL
* http://localhost:8080/saveName?fname=Alvin&lname=Alexander
* prints: Some(Alvin), Some(Alexander)
*/
get("/saveName") {
  val firstName = params.get("fname")
  val lastName = params.get("lname")
  <p>{firstName}, {lastName}</p>
}
4.2.2 命名参数方式

Scalatra还支持“命名参数”方式,更方便且能记录方法期望接收的参数。调用者可以访问类似 http://localhost:8080/hello/Alvin/Alexander 的URL,在 get 方法中处理这些参数:

get("/hello/:fname/:lname") {
  <p>Hello, {params("fname")}, {params("lname")}</p>
}

这种方式的优点是方法签名记录了期望的参数。

4.2.3 通配符使用

可以使用通配符处理其他需求,例如客户端传递文件名路径,而事先不知道路径深度的情况:

get("/getFilename/*.*") {
  val data = multiParams("splat")
  <p>{data.mkString("[", ", ", "]")}</p>
}

http://localhost:8080/getFilename/Users/Al/tmp/file.txt 为例,代码处理过程如下:

/**
* (1) GET http://localhost:8080/getFilename/Users/Al/tmp/file.txt
*/
get("/getFilename/*.*") {
  // (2) creates a Vector(Users/Al/tmp/file, txt)
  val data = multiParams("splat")
  // (3) prints: [Users/Al/tmp/file, txt]
  <pre>{data.mkString("[", ", ", "]")}</pre>
}

multiParams 方法使用 splat 参数创建一个包含两个元素的 Vector ,然后通过 data.mkString 将信息打印回浏览器。在实际程序中,可以将文件名重新组合并使用。Scalatra还有更多解析GET请求参数的方法,可查看最新文档获取更多信息。

5. 用Scalatra访问POST请求数据

5.1 问题描述

希望编写一个Scalatra Web服务方法来处理POST数据,例如处理作为POST请求发送的JSON数据。

5.2 解决方案

在Scalatra servlet中编写 post 方法,指定该方法应监听的URI:

post("/saveJsonStock") {
  val jsonString = request.body
  // deserialize the JSON ...
}

通过调用 request.body 方法访问传递给POST请求的数据。

5.3 示例代码

以下是 StockServlet 中的 post 方法示例,展示如何将接收到的JSON字符串反序列化为 Stock 对象:

package com.alvinalexander.app
import org.scalatra._
import scalate.ScalateSupport
import net.liftweb.json._
class StockServlet extends MyScalatraWebAppStack {
  /**
* Expects an incoming JSON string like this:
* {"symbol":"GOOG","price":"600.00"}
*/
  post("/saveJsonStock") {
    // get the POST request data
    val jsonString = request.body
    // needed for Lift-JSON
    implicit val formats = DefaultFormats
    // convert the JSON string to a JValue object
    val jValue = parse(jsonString)
    // deserialize the string into a Stock object
    val stock = jValue.extract[Stock]
    // for debugging
    println(stock)
    // you can send information back to the client
    // in the response header
    response.addHeader("ACK", "GOT IT")
  }
}
// a simple Stock class
class Stock (var symbol: String, var price: Double) {
  override def toString = symbol + ", " + price
}

最后,需要将Lift - JSON依赖添加到项目中。假设项目是按照之前的方式创建的SBT项目,在 project/build.scala 文件的 libraryDependencies 中添加以下依赖:

"net.liftweb" %% "lift-json" % "2.5+"

5.4 测试POST方法

5.4.1 使用Scala代码测试
import net.liftweb.json._
import net.liftweb.json.Serialization.write
import org.apache.http.client.methods.HttpPost
import org.apache.http.entity.StringEntity
import org.apache.http.impl.client.DefaultHttpClient
object PostTester extends App {
  // create a Stock and convert it to a JSON string
  val stock = new Stock("AAPL", 500.00)
  implicit val formats = DefaultFormats
  val stockAsJsonString = write(stock)
  // add the JSON string as a StringEntity to a POST request
  val post = new HttpPost("http://localhost:8080/stocks/saveJsonStock")
  post.setHeader("Content-type", "application/json")
  post.setEntity(new StringEntity(stockAsJsonString))
  // send the POST request
  val response = (new DefaultHttpClient).execute(post)
  // print the response
  println("--- HEADERS ---")
  response.getAllHeaders.foreach(arg => println(arg))
}
class Stock (var symbol: String, var price: Double)

运行该测试对象时,会得到如下输出:

--- HEADERS ---
ACK: GOT IT
Content-Type: text/html;charset=UTF-8
Content-Length: 0
Server: Jetty(8.1.8.v20121106)
5.4.2 使用curl命令测试
curl \
  --header "Content-type: application/json" \
  --request POST \
  --data '{"symbol":"GOOG", "price":600.00}' \
  http://localhost:8080/stocks/saveJsonStock

在Unix系统上,将此命令保存到 postJson.sh 文件中,使其可执行并运行:

$ chmod +x postJson.sh
$ ./postJson.sh

虽然该命令没有输出,但应该能在 StockServlet 的输出窗口中看到正确的调试输出。

5.5 注意事项

Scalatra的最新版本使用Json4s库来反序列化JSON,该库目前基于Lift - JSON,反序列化代码类似。需要将相应库作为依赖添加到项目中。处理POST请求的关键要点包括:
- 使用 post 方法处理POST请求。
- 使用 request.body 获取POST数据。
- 可使用 response.addHeader("ACK", "GOT IT") 向客户端返回成功或失败消息(可选)。
- 拥有可用于测试POST请求的客户端程序。

6. 创建简单的GET请求客户端

6.1 问题描述

需要一个HTTP客户端来进行GET请求调用。

6.2 解决方案

6.2.1 简单使用 scala.io.Source.fromURL 方法
/**
* Returns the text (content) from a URL as a String.
* Warning: This method does not time out when the service is non-responsive.
*/
def get(url: String) = scala.io.Source.fromURL(url).mkString

此方法可用于调用RESTful URL并检索其内容,但在服务无响应时不会超时。由于 Source.fromURL 方法使用 java.net.URL java.io.InputStream 类,可能会抛出 java.io.IOException 异常,可对方法进行注解:

@throws(classOf[java.io.IOException])
def get(url: String) = io.Source.fromURL(url).mkString
6.2.2 添加超时包装
/**
* Returns the text (content) from a REST URL as a String.
* Inspired by http://matthewkwong.blogspot.com/2009/09/
scala-scalaiosource-fromurl-blockshangs.html
 * and http://alvinalexander.com/blog/post/java/how-open-url-
read-contents-httpurl-connection-java
 *
 * The `connectTimeout` and `readTimeout` comes from the Java URLConnection
 * class Javadoc.
 * @param url The full URL to connect to.
 * @param connectTimeout Sets a specified timeout value, in milliseconds,
 * to be used when opening a communications link to the resource referenced
 * by this URLConnection. If the timeout expires before the connection can
 * be established, a java.net.SocketTimeoutException
 * is raised. A timeout of zero is interpreted as an infinite timeout.
 * Defaults to 5000 ms.
 * @param readTimeout If the timeout expires before there is data available
 * for read, a java.net.SocketTimeoutException is raised. A timeout of zero
 * is interpreted as an infinite timeout. Defaults to 5000 ms.
 * @param requestMethod Defaults to "GET". (Other methods have not been tested.)
 *
 * @example get("http://www.example.com/getInfo")
 * @example get("http://www.example.com/getInfo", 5000)
 * @example get("http://www.example.com/getInfo", 5000, 5000)
 */
@throws(classOf[java.io.IOException])
@throws(classOf[java.net.SocketTimeoutException])
def get(url: String,
        connectTimeout:Int =5000,
        readTimeout:Int =5000,
        requestMethod: String = "GET") = {
  import java.net.{URL, HttpURLConnection}
  val connection = (new URL(url)).openConnection.asInstanceOf[HttpURLConnection]
  connection.setConnectTimeout(connectTimeout)
  connection.setReadTimeout(readTimeout)
  connection.setRequestMethod(requestMethod)
  val inputStream = connection.getInputStream
  val content = io.Source.fromInputStream(inputStream).mkString
  if (inputStream != null) inputStream.close
  content
}

该方法可设置连接和读取超时值,调用示例如下:

try {
  val content = get("http://localhost:8080/waitForever")
  println(content)
} catch {
  case ioe: java.io.IOException =>  // handle this
  case ste: java.net.SocketTimeoutException => // handle this
}
6.2.3 使用Apache HttpClient库
import java.io._
import org.apache.http.{HttpEntity, HttpResponse}
import org.apache.http.client._
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.DefaultHttpClient
import scala.collection.mutable.StringBuilder
import scala.xml.XML
import org.apache.http.params.HttpConnectionParams
import org.apache.http.params.HttpParams
/**
* Returns the text (content) from a REST URL as a String.
* Returns a blank String if there was a problem.
* This function will also throw exceptions if there are problems trying
* to connect to the url.
*
* @param url A complete URL, such as "http://foo.com/bar"
* @param connectionTimeout The connection timeout, in ms.
* @param socketTimeout The socket timeout, in ms.
*/
def getRestContent(url: String,
                   connectionTimeout: Int,
                   socketTimeout: Int): String = {
  val httpClient = buildHttpClient(connectionTimeout, socketTimeout)
  val httpResponse = httpClient.execute(new HttpGet(url))
  val entity = httpResponse.getEntity
  var content = ""
  // 后续处理代码...
}

综上所述,通过以上多种方法,可以在Scala中实现从邮件通知到Web服务搭建、参数处理以及请求客户端创建等一系列功能,为Scala Web开发提供了丰富的解决方案。

6. 创建简单的GET请求客户端(续)

6.2.3 使用Apache HttpClient库(续)

import java.io._
import org.apache.http.{HttpEntity, HttpResponse}
import org.apache.http.client._
import org.apache.http.client.methods.HttpGet
import org.apache.http.impl.client.DefaultHttpClient
import scala.collection.mutable.StringBuilder
import scala.xml.XML
import org.apache.http.params.HttpConnectionParams
import org.apache.http.params.HttpParams

/**
* Returns the text (content) from a REST URL as a String.
* Returns a blank String if there was a problem.
* This function will also throw exceptions if there are problems trying
* to connect to the url.
*
* @param url A complete URL, such as "http://foo.com/bar"
* @param connectionTimeout The connection timeout, in ms.
* @param socketTimeout The socket timeout, in ms.
*/
def getRestContent(url: String,
                   connectionTimeout: Int,
                   socketTimeout: Int): String = {
  val httpClient = buildHttpClient(connectionTimeout, socketTimeout)
  val httpResponse = httpClient.execute(new HttpGet(url))
  val entity = httpResponse.getEntity
  var content = ""
  if (entity != null) {
    val inputStream = entity.getContent
    try {
      val reader = new BufferedReader(new InputStreamReader(inputStream))
      val stringBuilder = new StringBuilder()
      var line: String = null
      while ({line = reader.readLine(); line!= null}) {
        stringBuilder.append(line)
      }
      content = stringBuilder.toString
    } catch {
      case e: IOException =>
        e.printStackTrace()
    } finally {
      try {
        inputStream.close()
      } catch {
        case e: IOException =>
          e.printStackTrace()
      }
    }
  }
  content
}

private def buildHttpClient(connectionTimeout: Int, socketTimeout: Int): HttpClient = {
  val httpClient = new DefaultHttpClient()
  val params: HttpParams = httpClient.getParams()
  HttpConnectionParams.setConnectionTimeout(params, connectionTimeout)
  HttpConnectionParams.setSoTimeout(params, socketTimeout)
  httpClient
}

使用该方法时,可按如下方式调用:

val url = "http://example.com/api/data"
val connectionTimeout = 5000
val socketTimeout = 5000
val result = getRestContent(url, connectionTimeout, socketTimeout)
println(result)

6.3 三种方法对比

方法 优点 缺点 适用场景
scala.io.Source.fromURL 代码简单,使用方便 无超时控制,服务无响应时会阻塞 对响应时间要求不高,服务稳定性好的场景
添加超时包装 可设置连接和读取超时,增强了健壮性 代码相对复杂 需要控制超时时间,避免程序长时间阻塞的场景
Apache HttpClient库 功能强大,可定制性高 依赖第三方库,代码复杂 需要处理复杂请求,如设置请求头、处理响应状态码等场景

6.4 流程图

graph TD;
    A[选择GET请求方法] --> B{是否需要超时控制};
    B -- 否 --> C[使用scala.io.Source.fromURL];
    B -- 是 --> D{是否需要复杂请求处理};
    D -- 否 --> E[添加超时包装];
    D -- 是 --> F[使用Apache HttpClient库];

总结

7.1 关键技术点回顾

本文围绕Scala Web开发展开,涵盖了多个重要方面:
- 邮件通知 :通过解析JSON数据,结合Lift - JSON库,实现对感兴趣用户邮件的通知功能。
- Web服务搭建 :使用Scalatra框架,借助Giter8工具创建项目,配置Jetty服务器,轻松搭建Web服务。
- Servlet映射 :利用Scalatra的“挂载”机制,替代传统的XML Servlet映射,使代码更简洁。
- 参数处理 :支持传统和命名参数方式处理GET请求参数,使用通配符处理复杂情况;使用 post 方法和 request.body 处理POST请求数据,并进行JSON反序列化。
- 请求客户端 :介绍了三种创建GET请求客户端的方法,各有优缺点,可根据不同场景选择。

7.2 实践建议

  • 项目搭建 :在创建Scalatra项目时,使用Giter8工具能快速生成规范的项目结构,提高开发效率。
  • 参数处理 :根据实际需求选择合适的参数传递和处理方式,命名参数方式可提高代码的可读性和可维护性。
  • 请求客户端 :对于对响应时间要求不高的简单请求,可使用 scala.io.Source.fromURL ;对于需要控制超时的场景,使用添加超时包装的方法;对于复杂请求,建议使用Apache HttpClient库。

7.3 未来拓展

  • 性能优化 :可进一步研究Scalatra和相关库的性能优化技巧,如缓存机制、异步处理等。
  • 功能扩展 :结合数据库、消息队列等技术,拓展Web服务的功能,实现更复杂的业务逻辑。
  • 安全防护 :加强Web服务的安全防护,如身份验证、授权、防止SQL注入等。

通过本文的介绍,相信读者能够对Scala Web开发有更深入的了解,并在实际项目中灵活运用这些技术。不断实践和探索,将能开发出高效、稳定、安全的Scala Web应用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值