19、利用 Scala 构建 Web 应用:Play 2 框架实战

利用 Scala 构建 Web 应用:Play 2 框架实战

1. 异步消息处理与 Scala 优势

在 Web 应用开发中,有这样一种机制:当组件接收到消息并更新自身时,它会通知监听器该组件新旧版本之间的差异。在“无变化”阶段,系统仅消耗内存和一个 NIO 连接,不涉及线程或栈。当 Actor 从 Comet 组件接收到更新(或 120 秒后),会为 Ajax 请求创建响应,然后调用延续操作并将响应发送到浏览器(可以是包含执行差异操作命令的 JavaScript 或空操作)。浏览器执行 JavaScript,等待 100 毫秒后重新启动该过程。

虽然可以用 Java 实现这一切,但 Scala 实现该方案所需的代码量与上述描述的字符数大致相同,体现了 Scala 在代码简洁性上的优势。

2. Play 2 框架概述

Play 2 借助 Akka 实现的 Actor 模型提供异步 HTTP API,用于处理高并发系统。Akka 为 Scala 和 Java 实现了 Actor 模型。Play 1 使用 Java 语言,并通过插件支持 Scala。而 Play 2.0 于 2012 年与 Typesafe 栈同时发布,以 Scala 为核心语言构建。

Play 2 具有以下关键特性:
| 特性 | 描述 |
| — | — |
| 异步 I/O | 使用 JBoss Netty 作为 Web 服务器异步处理长请求 |
| 内置 Web 服务器 | 自带 JBoss Netty Web 服务器,也可打包分发到 Java EE 应用服务器 |
| 依赖管理 | 使用 SBT 进行依赖管理 |
| 热重载 | 开发模式下,新请求时验证代码更新,自动重新编译修改的文件,错误直接显示在浏览器中 |
| 内存数据库 | 支持如 H2 这样的嵌入式数据库 |
| 原生 Scala 支持 | 原生支持 Scala,同时与 Java 完全互操作 |
| ORM | 使用 Ebean 作为 JPA 的替代 ORM 来访问数据库 |
| 无状态 | 完全 RESTful,每个连接无 Java EE 会话 |
| 模板引擎 | 使用 Scala 作为模板引擎 |
| 测试框架 | 内置 JUnit 和 Selenium 等测试框架进行单元和功能测试 |
| WebSocket | 自带 WebSocket 实现,实现客户端与服务器的双向连接 |

3. Play 2 发行版

Play 2 有两种发行版:经典的 Play 2 独立发行版和基于 Typesafe Activator 的发行版。由于经典的 Play 2 非常流行,在遗留应用中仍可能会遇到,所以会对两者都进行介绍。不过,你可以选择从经典的 Play 2 迁移到基于 Activator 的 Play 2。若想了解迁移到 Play 2.3 的相关信息,可查看迁移指南:https://www.playframework.com/documentation/2.3.x/Migration23 。

4. 独立发行版入门

要运行 Play 框架,需要 JDK 6 或更高版本。不同操作系统的准备工作如下:
- Linux :确保使用 Sun JDK 或 OpenJDK(而非许多 Linux 发行版的默认 Java 命令 gcj)。
- Windows :下载并安装最新的 JDK 包。
- MacOS :Java 是内置的。

下载最新的 Play 2.2 独立发行版:https://www.playframework.com/download#older-versions 。
将存档文件解压到指定位置,并将 play 脚本添加到系统路径:
- Windows :在环境变量中设置 PATH。
- UNIX 系统 :执行命令 export PATH=$PATH:/relativePath/to/play

在命令行工具中输入 play 检查 Play 是否正确安装。若安装正确,会在控制台看到相应输出。也可以使用 play help 命令获取帮助。

5. 创建第一个 Scala Web 应用

Play 安装正确后,就可以创建第一个 Scala Web 应用了。使用 play 命令行工具,加上 new 参数和新应用的名称(如 helloworld )来创建新应用。Play 2 会询问应用是 Scala 还是 Java 应用,创建 Scala 应用需指定 1 ,这将为 Scala 创建源文件和应用结构。

进入 helloworld 目录,输入 play 进入 Play 控制台,再输入 run 启动服务器运行应用。控制台会显示应用启动信息,表明 HTTP 服务器正在监听 9000 端口。访问 http://localhost:9000/ 会显示默认欢迎页面。

6. Play 应用结构

run 命令会在 helloworld 目录下创建应用结构,各文件夹内容如下:
- app :服务器端源文件的根目录,包含 Java 和 Scala 源代码、模板和编译资产源。默认创建 controllers views 子文件夹,可添加 app/models 用于 MVC 模式的模型组件,还有可选的 app/assets 目录用于编译资产。
- conf :包含应用的配置文件,主要有 application.conf (应用的主配置文件)和 routes (路由定义文件)。
- project :包含配置 Scala 构建工具 SBT 所需的所有文件。
- public :包含用于图片、CSS 样式表和 JavaScript 文件的标准子目录,其中的资源是静态资产,由 Web 服务器直接提供服务。
- target :包含构建系统生成的工件,如编译后的类、框架管理的类、生成的资源和源等。
- test :包含所有测试文件以及框架提供的一些示例。

7. Play 2 中的 MVC 架构

Play 2 应用遵循 MVC 架构模式,各层在 app 目录下的不同包中定义。请求流程如下:

graph LR
    A[Browser] -->|Request| B[Router]
    B -->|Determine action| C[Controller]
    C -->|Extract data, update model| D[Model]
    C -->|Render template| E[View]
    E -->|Generate response| C
    C -->|Send response| A
  • 路由器(Router) :通过 conf/routes 文件作为 Web 应用的主要入口点,定义应用所需的路由。每个路由包含 HTTP 方法和 URI 模式,将传入的 HTTP 请求转换为动作调用。该文件编译时若有错误,会直接在浏览器中显示,修改后会自动重新加载,即热重载功能。
  • 控制器(Controller) :响应请求,处理请求并对模型进行更改。在 Scala 中,控制器是扩展 play.api.mvc.Controller 类型的对象,包含处理请求参数并生成结果的动作函数。默认在 app/controllers 包中定义。在 Java 中,控制器是类,包含公共静态的动作方法。
  • 模型(Model) :是应用操作的信息的特定领域表示,常用 JavaBean 表示,但 Play 2 通过字节码增强为你生成 getter 和 setter,减少样板代码。模型对象可能包含持久化工件,如 JPA 注解。
  • 视图(View) :在 Java EE 应用中通常使用 JSP 开发,而 Play 不是以 Java EE 为中心,视图由包含 HTML 和 Scala 代码的模板组成。Play 1 的模板基于 Groovy,从 Play 2 开始基于 Scala。无论 Java 还是 Scala 应用,模板都是相同的。

以下是 Play 2 为 helloworld-scala 生成的控制器和模板代码:
控制器代码(Scala)

package controllers

import play.api._
import play.api.mvc._

object Application extends Controller {
  def index = Action {
    Ok(views.html.index("Your new application is ready."))
  }
}

模板代码

@(message: String)

@main("Welcome to Play") {
  @play20.welcome(message)
}

可以通过修改控制器中的响应来修改应用:

def index = Action {
  Ok("Hello world")
}

刷新浏览器主页即可看到修改后的效果。

8. 配置 Eclipse 用于 Scala 开发

Scala IDE 是一个 Eclipse 插件,可以从 Help ➤ Install New Software 安装。在 Work with 字段中输入插件路径:http://scala-ide.org/download/current.html 。详细配置说明可参考:http://scala-ide.org/documentation.html 。

使用 Eclipse IDE 与 Play 2 配合,需要让 Play 2 生成 Eclipse 项目配置。在 Play 控制台中调用 Eclipse 即可。然后通过 File ➤ Import ,选择 General ➤ Existing Projects into Workspace 导入项目。

9. 创建 Java 应用

创建 Java 应用的步骤与创建 Scala 应用类似。Play 2 询问应用类型时,指定 2 来创建 Java 应用。进入 helloworld 目录,输入 play 进入 Play 控制台,再输入 run 启动服务器。

以下是 Java 应用的控制器代码:

package controllers;

import play.*;
import play.mvc.*;

import views.html.*;

public class Application extends Controller {
  public static Result index() {
    return ok(index.render("Your new application is ready."));
  }
}

同样可以修改控制器中的响应:

public static Result index() {
  return ok("Hello world");
}

访问 http://localhost:9000/ 即可看到修改后的效果。

Play 2 在 play-2.2.0\samples\java\ 文件夹中提供了几个示例应用,你可以运行其中的 helloworld 应用,点击 Submit Query 会根据选择显示用户名称。你可以自行查看代码来改进应用。

利用 Scala 构建 Web 应用:Play 2 框架实战

10. 路由机制深入解析

路由在 Play 2 应用中起着至关重要的作用,它是 Web 应用的入口,负责将 HTTP 请求映射到相应的动作。路由规则定义在 conf/routes 文件中,该文件的每一行代表一个路由,由 HTTP 方法、URI 模式和对应的动作调用组成。

以下是一个简单的路由示例:

GET     /                   controllers.Application.index

这表示当浏览器发送一个 GET 请求到根路径 / 时,会调用 controllers.Application 中的 index 动作。

路由文件支持参数匹配,例如:

GET     /products/:id       controllers.Products.show(id: Long)

这里的 :id 是一个参数,它会被捕获并作为参数传递给 controllers.Products 中的 show 动作。

路由文件还支持正则表达式匹配,提高路由的灵活性:

GET     /images/[0-9]+      controllers.Images.show(id: Int)

这样只有当 URI 中 /images/ 后面跟着数字时,才会匹配该路由。

11. 控制器的高级应用

控制器在 Play 2 中负责处理请求和生成响应,除了基本的动作处理,还可以进行更多高级操作。

11.1 过滤器

过滤器可以在请求到达控制器之前或响应返回浏览器之前进行预处理或后处理。以下是一个简单的日志过滤器示例:

import javax.inject.Inject
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}

class LoggingFilter @Inject()(implicit ec: ExecutionContext) extends Filter {
  def apply(nextFilter: RequestHeader => Future[Result])(requestHeader: RequestHeader): Future[Result] = {
    val startTime = System.currentTimeMillis
    nextFilter(requestHeader).map { result =>
      val endTime = System.currentTimeMillis
      val requestTime = endTime - startTime
      play.api.Logger.info(s"${requestHeader.method} ${requestHeader.uri} took ${requestTime}ms and returned ${result.header.status}")
      result.withHeaders("Request-Time" -> requestTime.toString)
    }
  }
}

要使用这个过滤器,需要在 app/filters 目录下创建一个 Filters 类:

import javax.inject.Inject
import play.api.http.HttpFilters
import play.filters.csrf.CSRFFilter
import play.filters.headers.SecurityHeadersFilter
import play.filters.hosts.AllowedHostsFilter

class Filters @Inject()(csrfFilter: CSRFFilter, securityHeadersFilter: SecurityHeadersFilter, allowedHostsFilter: AllowedHostsFilter, loggingFilter: LoggingFilter) extends HttpFilters {
  override val filters = Seq(csrfFilter, securityHeadersFilter, allowedHostsFilter, loggingFilter)
}
11.2 异步动作

在处理耗时操作时,使用异步动作可以避免阻塞线程,提高应用的并发性能。以下是一个异步动作的示例:

import play.api.mvc._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

object AsyncController extends Controller {
  def asyncAction = Action.async {
    val futureResult = Future {
      // 模拟耗时操作
      Thread.sleep(1000)
      Ok("Async operation completed")
    }
    futureResult
  }
}
12. 模型的持久化操作

模型在 Play 2 中通常用于表示应用的数据和业务逻辑,并且需要与数据库进行交互。Play 2 使用 Ebean 作为 ORM 工具来简化数据库操作。

以下是一个简单的模型类示例:

import javax.persistence._
import play.db.ebean._
import com.avaje.ebean._

@Entity
class Product extends Model {
  @Id
  var id: Long = _
  var name: String = _
  var description: String = _

  def this(name: String, description: String) {
    this()
    this.name = name
    this.description = description
  }

  object Finder extends Finder[Long, Product](classOf[Product])
}

可以使用以下代码进行数据库操作:

// 创建一个新的产品
val product = new Product("New Product", "This is a new product")
product.save()

// 查询所有产品
val products = Product.Finder.all()

// 根据 ID 查询产品
val productById = Product.Finder.byId(1L)

// 更新产品信息
productById.name = "Updated Product"
productById.update()

// 删除产品
productById.delete()
13. 视图模板的高级特性

Play 2 的视图模板基于 Scala,除了基本的 HTML 和 Scala 代码混合,还有一些高级特性。

13.1 模板继承

模板可以继承其他模板,实现代码复用。以下是一个模板继承的示例:
base.scala.html

@(title: String)(content: Html)
<!DOCTYPE html>
<html>
<head>
  <title>@title</title>
</head>
<body>
  <header>
    <h1>My Application</h1>
  </header>
  <main>
    @content
  </main>
  <footer>
    <p>&copy; 2024 My Application</p>
  </footer>
</body>
</html>

index.scala.html

@extends("Welcome to My App") {
  <p>Welcome to my application!</p>
}
13.2 模板参数和循环

模板可以接受参数,并且可以使用循环来动态生成内容。以下是一个示例:

@(products: List[Product])
<table>
  <thead>
    <tr>
      <th>ID</th>
      <th>Name</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    @for(product <- products) {
      <tr>
        <td>@product.id</td>
        <td>@product.name</td>
        <td>@product.description</td>
      </tr>
    }
  </tbody>
</table>
14. 测试框架的实际应用

Play 2 内置了 JUnit 和 Selenium 等测试框架,方便进行单元测试和功能测试。

14.1 单元测试

以下是一个简单的控制器单元测试示例:

import org.scalatestplus.play._
import play.api.test._
import play.api.test.Helpers._

class ApplicationControllerSpec extends PlaySpec with OneAppPerTest {
  "ApplicationController" should {
    "return OK for index action" in {
      val result = route(app, FakeRequest(GET, "/")).get
      status(result) mustBe OK
      contentType(result) mustBe Some("text/html")
      contentAsString(result) must include ("Your new application is ready.")
    }
  }
}
14.2 功能测试

使用 Selenium 进行功能测试可以模拟用户在浏览器中的操作。以下是一个简单的功能测试示例:

import org.scalatestplus.play._
import play.api.test._
import play.api.test.Helpers._
import org.openqa.selenium._
import org.openqa.selenium.chrome._

class ApplicationFunctionalSpec extends PlaySpec with OneServerPerSuite with OneBrowserPerSuite with ChromeFactory {
  "Application" should {
    "display welcome page" in {
      go to ("http://localhost:" + port)
      pageSource must include ("Your new application is ready.")
    }
  }
}
总结

通过以上对 Play 2 框架的详细介绍,我们了解了其在 Web 应用开发中的各个方面,包括异步消息处理、MVC 架构、路由机制、控制器、模型、视图模板和测试框架等。Play 2 框架以其简洁的代码、强大的功能和良好的性能,为 Scala 和 Java 开发者提供了一个优秀的 Web 应用开发平台。无论是初学者还是有经验的开发者,都可以利用 Play 2 快速构建出高质量的 Web 应用。在实际开发中,可以根据具体需求进一步深入学习和应用这些知识,不断优化和完善应用。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值